diff --git a/src/administration/Administration.Service/BusinessLogic/UserBusinessLogic.cs b/src/administration/Administration.Service/BusinessLogic/UserBusinessLogic.cs index a611a29dfb..baf79f70eb 100644 --- a/src/administration/Administration.Service/BusinessLogic/UserBusinessLogic.cs +++ b/src/administration/Administration.Service/BusinessLogic/UserBusinessLogic.cs @@ -293,7 +293,8 @@ public async Task AddOwnCompanyUsersBusinessPartnerNumbe } return acc; }, - acc => (acc.SuccessfulBpns.ToImmutable(), acc.UnsuccessfulBpns.ToImmutable()) + acc => (acc.SuccessfulBpns.ToImmutable(), acc.UnsuccessfulBpns.ToImmutable()), + cancellationToken ).ConfigureAwait(ConfigureAwaitOptions.None); if (successfulBpns.Count != 0) diff --git a/src/framework/Framework.Async/AsyncAggregateExtensions.cs b/src/framework/Framework.Async/AsyncAggregateExtensions.cs index f265d841ed..fc5214c84b 100644 --- a/src/framework/Framework.Async/AsyncAggregateExtensions.cs +++ b/src/framework/Framework.Async/AsyncAggregateExtensions.cs @@ -21,9 +21,61 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Framework.Async; public static class AsyncAggregateExtensions { - public static Task AggregateAwait(this IEnumerable source, TAccumulate seed, Func> accumulate, Func select) => - source.Aggregate( - Task.FromResult(seed), - async (accTask, item) => await accumulate(await accTask.ConfigureAwait(ConfigureAwaitOptions.None), item).ConfigureAwait(ConfigureAwaitOptions.None), - async (accTask) => select(await accTask.ConfigureAwait(ConfigureAwaitOptions.None))); + public static Task AggregateAwait(this IEnumerable source, TAccumulate seed, Func> accumulate, CancellationToken cancellationToken = default) + { + using var enumerator = source.GetEnumerator(); + return AggregateAwait(enumerator, seed, accumulate, cancellationToken); + } + + public static Task AggregateAwait(this IEnumerable source, TAccumulate seed, Func> accumulate, CancellationToken cancellationToken = default) + { + using var enumerator = source.GetEnumerator(); + return AggregateAwait(enumerator, seed, accumulate, cancellationToken); + } + + public static async Task AggregateAwait(this IEnumerable source, TAccumulate seed, Func> accumulate, Func result, CancellationToken cancellationToken = default) => + result(await AggregateAwait(source, seed, accumulate, cancellationToken)); + + public static async Task AggregateAwait(this IEnumerable source, TAccumulate seed, Func> accumulate, Func result, CancellationToken cancellationToken = default) => + result(await AggregateAwait(source, seed, accumulate, cancellationToken)); + + public static Task AggregateAwait(this IEnumerable source, Func> accumulate, CancellationToken cancellationToken = default) + { + using var enumerator = source.GetEnumerator(); + if (!enumerator.MoveNext()) + throw new InvalidOperationException("source must not be empty"); + + return AggregateAwait(enumerator, enumerator.Current, accumulate, cancellationToken); + } + + public static Task AggregateAwait(this IEnumerable source, Func> accumulate, CancellationToken cancellationToken = default) + { + using var enumerator = source.GetEnumerator(); + if (!enumerator.MoveNext()) + throw new InvalidOperationException("source must not be empty"); + + return AggregateAwait(enumerator, enumerator.Current, accumulate, cancellationToken); + } + + private static async Task AggregateAwait(IEnumerator enumerator, TAccumulate seed, Func> accumulate, CancellationToken cancellationToken) + { + var accumulator = seed; + while (enumerator.MoveNext()) + { + cancellationToken.ThrowIfCancellationRequested(); + accumulator = await accumulate(accumulator, enumerator.Current, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + } + return accumulator; + } + + private static async Task AggregateAwait(IEnumerator enumerator, TAccumulate seed, Func> accumulate, CancellationToken cancellationToken) + { + var accumulator = seed; + while (enumerator.MoveNext()) + { + cancellationToken.ThrowIfCancellationRequested(); + accumulator = await accumulate(accumulator, enumerator.Current).ConfigureAwait(ConfigureAwaitOptions.None); + } + return accumulator; + } } diff --git a/src/framework/Framework.Async/Directory.Build.props b/src/framework/Framework.Async/Directory.Build.props index e909f97822..e23d9cab37 100644 --- a/src/framework/Framework.Async/Directory.Build.props +++ b/src/framework/Framework.Async/Directory.Build.props @@ -19,7 +19,7 @@ - 2.8.0 + 2.9.0 diff --git a/src/framework/Framework.Cors/Directory.Build.props b/src/framework/Framework.Cors/Directory.Build.props index e909f97822..e23d9cab37 100644 --- a/src/framework/Framework.Cors/Directory.Build.props +++ b/src/framework/Framework.Cors/Directory.Build.props @@ -19,7 +19,7 @@ - 2.8.0 + 2.9.0 diff --git a/src/framework/Framework.DBAccess/Directory.Build.props b/src/framework/Framework.DBAccess/Directory.Build.props index e909f97822..e23d9cab37 100644 --- a/src/framework/Framework.DBAccess/Directory.Build.props +++ b/src/framework/Framework.DBAccess/Directory.Build.props @@ -19,7 +19,7 @@ - 2.8.0 + 2.9.0 diff --git a/src/framework/Framework.DateTimeProvider/Directory.Build.props b/src/framework/Framework.DateTimeProvider/Directory.Build.props index e909f97822..e23d9cab37 100644 --- a/src/framework/Framework.DateTimeProvider/Directory.Build.props +++ b/src/framework/Framework.DateTimeProvider/Directory.Build.props @@ -19,7 +19,7 @@ - 2.8.0 + 2.9.0 diff --git a/src/framework/Framework.DependencyInjection/Directory.Build.props b/src/framework/Framework.DependencyInjection/Directory.Build.props index e909f97822..e23d9cab37 100644 --- a/src/framework/Framework.DependencyInjection/Directory.Build.props +++ b/src/framework/Framework.DependencyInjection/Directory.Build.props @@ -19,7 +19,7 @@ - 2.8.0 + 2.9.0 diff --git a/src/framework/Framework.ErrorHandling.Controller/Directory.Build.props b/src/framework/Framework.ErrorHandling.Controller/Directory.Build.props index e909f97822..e23d9cab37 100644 --- a/src/framework/Framework.ErrorHandling.Controller/Directory.Build.props +++ b/src/framework/Framework.ErrorHandling.Controller/Directory.Build.props @@ -19,7 +19,7 @@ - 2.8.0 + 2.9.0 diff --git a/src/framework/Framework.ErrorHandling.Web/Directory.Build.props b/src/framework/Framework.ErrorHandling.Web/Directory.Build.props index e909f97822..e23d9cab37 100644 --- a/src/framework/Framework.ErrorHandling.Web/Directory.Build.props +++ b/src/framework/Framework.ErrorHandling.Web/Directory.Build.props @@ -19,7 +19,7 @@ - 2.8.0 + 2.9.0 diff --git a/src/framework/Framework.ErrorHandling/Directory.Build.props b/src/framework/Framework.ErrorHandling/Directory.Build.props index e909f97822..e23d9cab37 100644 --- a/src/framework/Framework.ErrorHandling/Directory.Build.props +++ b/src/framework/Framework.ErrorHandling/Directory.Build.props @@ -19,7 +19,7 @@ - 2.8.0 + 2.9.0 diff --git a/src/framework/Framework.HttpClientExtensions/Directory.Build.props b/src/framework/Framework.HttpClientExtensions/Directory.Build.props index e909f97822..e23d9cab37 100644 --- a/src/framework/Framework.HttpClientExtensions/Directory.Build.props +++ b/src/framework/Framework.HttpClientExtensions/Directory.Build.props @@ -19,7 +19,7 @@ - 2.8.0 + 2.9.0 diff --git a/src/framework/Framework.IO/Directory.Build.props b/src/framework/Framework.IO/Directory.Build.props index e909f97822..e23d9cab37 100644 --- a/src/framework/Framework.IO/Directory.Build.props +++ b/src/framework/Framework.IO/Directory.Build.props @@ -19,7 +19,7 @@ - 2.8.0 + 2.9.0 diff --git a/src/framework/Framework.Linq/Directory.Build.props b/src/framework/Framework.Linq/Directory.Build.props index e909f97822..e23d9cab37 100644 --- a/src/framework/Framework.Linq/Directory.Build.props +++ b/src/framework/Framework.Linq/Directory.Build.props @@ -19,7 +19,7 @@ - 2.8.0 + 2.9.0 diff --git a/src/framework/Framework.Linq/IfAnyExtension.cs b/src/framework/Framework.Linq/IfAnyExtension.cs index 118acf5f61..d9f7639d0e 100644 --- a/src/framework/Framework.Linq/IfAnyExtension.cs +++ b/src/framework/Framework.Linq/IfAnyExtension.cs @@ -109,4 +109,17 @@ public static bool IfAny(this IEnumerable source, Func, returnValue = default; return false; } + + public static async ValueTask IfAnyAwait(this IEnumerable source, Func, Task> process) + { + var enumerator = source.GetEnumerator(); + + if (enumerator.MoveNext()) + { + await process(new IfAnyEnumerable(source, enumerator)).ConfigureAwait(ConfigureAwaitOptions.None); + return true; + } + + return false; + } } diff --git a/src/framework/Framework.Linq/NullOrSequenceEqualExtension.cs b/src/framework/Framework.Linq/NullOrSequenceEqualExtension.cs index 4f0b3e0788..c3777e34ec 100644 --- a/src/framework/Framework.Linq/NullOrSequenceEqualExtension.cs +++ b/src/framework/Framework.Linq/NullOrSequenceEqualExtension.cs @@ -23,12 +23,12 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Framework.Linq; public static class NullOrSequenceEqualExtensions { - public static bool NullOrContentEqual(this IEnumerable? items, IEnumerable? others, IEqualityComparer? comparer = null) where T : IComparable => + public static bool NullOrContentEqual(this IEnumerable? items, IEnumerable? others, IEqualityComparer? comparer = null) where T : IComparable? => items == null && others == null || items != null && others != null && - items.OrderBy(x => x).SequenceEqual(others.OrderBy(x => x), comparer); + items.Order().SequenceEqual(others.Order(), comparer); - public static bool NullOrContentEqual(this IEnumerable>? items, IEnumerable>? others, IEqualityComparer>? comparer = null) where K : IComparable where V : IComparable => + public static bool NullOrContentEqual(this IEnumerable>? items, IEnumerable>? others, IEqualityComparer>? comparer = null) where K : IComparable where V : IComparable? => items == null && others == null || items != null && others != null && items.OrderBy(x => x.Key).SequenceEqual(others.OrderBy(x => x.Key), comparer); @@ -37,6 +37,11 @@ public static bool NullOrContentEqual(this IEnumerable x.Key).SequenceEqual(others.OrderBy(x => x.Key), comparer ?? new EnumerableValueKeyValuePairEqualityComparer()); + + public static bool NullOrNullableContentEqual(this IEnumerable?>>? items, IEnumerable?>>? others, IEqualityComparer?>>? comparer = null) where V : IComparable => + items == null && others == null || + items != null && others != null && + items.OrderBy(x => x.Key).SequenceEqual(others.OrderBy(x => x.Key), comparer ?? new NullableEnumerableValueKeyValuePairEqualityComparer()); } public class KeyValuePairEqualityComparer : IEqualityComparer> where K : IComparable where V : IComparable @@ -50,7 +55,16 @@ public class EnumerableValueKeyValuePairEqualityComparer : IEqualityCompar { public bool Equals(KeyValuePair> source, KeyValuePair> other) => Equals(source.Key, other.Key) && - source.Value.NullOrContentEqual(other.Value); + source.Value.Order().SequenceEqual(other.Value.Order()); public int GetHashCode([DisallowNull] KeyValuePair> obj) => throw new NotImplementedException(); } + +public class NullableEnumerableValueKeyValuePairEqualityComparer : IEqualityComparer?>> where V : IComparable +{ + public bool Equals(KeyValuePair?> source, KeyValuePair?> other) => + Equals(source.Key, other.Key) && + source.Value.NullOrContentEqual(other.Value); + + public int GetHashCode([DisallowNull] KeyValuePair?> obj) => throw new NotImplementedException(); +} diff --git a/src/framework/Framework.Linq/NullableExtensions.cs b/src/framework/Framework.Linq/NullableExtensions.cs index c9ad19ab01..578d367ba8 100644 --- a/src/framework/Framework.Linq/NullableExtensions.cs +++ b/src/framework/Framework.Linq/NullableExtensions.cs @@ -23,4 +23,10 @@ public static class NullableExtensions { public static bool IsNullOrEmpty(this IEnumerable? collection) => collection == null || !collection.Any(); + + public static IEnumerable> FilterNotNullValues(this IEnumerable> source) where TValue : notnull => + source.Where(x => x.Value is not null).Cast>(); + + public static IEnumerable FilterNotNull(this IEnumerable source) where T : notnull => + source.Where(x => x is not null).Cast(); } diff --git a/src/framework/Framework.Logging/Directory.Build.props b/src/framework/Framework.Logging/Directory.Build.props index e909f97822..e23d9cab37 100644 --- a/src/framework/Framework.Logging/Directory.Build.props +++ b/src/framework/Framework.Logging/Directory.Build.props @@ -19,7 +19,7 @@ - 2.8.0 + 2.9.0 diff --git a/src/framework/Framework.Models/Directory.Build.props b/src/framework/Framework.Models/Directory.Build.props index e909f97822..e23d9cab37 100644 --- a/src/framework/Framework.Models/Directory.Build.props +++ b/src/framework/Framework.Models/Directory.Build.props @@ -19,7 +19,7 @@ - 2.8.0 + 2.9.0 diff --git a/src/framework/Framework.Seeding/Directory.Build.props b/src/framework/Framework.Seeding/Directory.Build.props index e909f97822..e23d9cab37 100644 --- a/src/framework/Framework.Seeding/Directory.Build.props +++ b/src/framework/Framework.Seeding/Directory.Build.props @@ -19,7 +19,7 @@ - 2.8.0 + 2.9.0 diff --git a/src/framework/Framework.Swagger/Directory.Build.props b/src/framework/Framework.Swagger/Directory.Build.props index e909f97822..e23d9cab37 100644 --- a/src/framework/Framework.Swagger/Directory.Build.props +++ b/src/framework/Framework.Swagger/Directory.Build.props @@ -19,7 +19,7 @@ - 2.8.0 + 2.9.0 diff --git a/src/framework/Framework.Token/Directory.Build.props b/src/framework/Framework.Token/Directory.Build.props index e909f97822..e23d9cab37 100644 --- a/src/framework/Framework.Token/Directory.Build.props +++ b/src/framework/Framework.Token/Directory.Build.props @@ -19,7 +19,7 @@ - 2.8.0 + 2.9.0 diff --git a/src/framework/Framework.Web/Directory.Build.props b/src/framework/Framework.Web/Directory.Build.props index e909f97822..e23d9cab37 100644 --- a/src/framework/Framework.Web/Directory.Build.props +++ b/src/framework/Framework.Web/Directory.Build.props @@ -19,7 +19,7 @@ - 2.8.0 + 2.9.0 diff --git a/src/keycloak/Keycloak.ErrorHandling/FlurlErrorHandler.cs b/src/keycloak/Keycloak.ErrorHandling/FlurlErrorHandler.cs index 8774f1f03a..1a9d7fb61a 100644 --- a/src/keycloak/Keycloak.ErrorHandling/FlurlErrorHandler.cs +++ b/src/keycloak/Keycloak.ErrorHandling/FlurlErrorHandler.cs @@ -21,45 +21,54 @@ using Microsoft.Extensions.Logging; using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; using System.Net; +using System.Text.Json; +using System.Text.Json.Serialization; namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.ErrorHandling; public static class FlurlErrorHandler { - public static void ConfigureErrorHandler(ILogger logger, bool isDevelopment) + public static void ConfigureErrorHandler(ILogger logger) { FlurlHttp.Configure(settings => settings.OnError = (call) => { var message = $"{call.HttpResponseMessage?.ReasonPhrase ?? "ReasonPhrase is null"}: {call.HttpRequestMessage.RequestUri}"; - if (isDevelopment) + if (logger.IsEnabled(LogLevel.Debug)) { - LogDevelopmentError(logger, call); + LogDebug(logger, call); } - else - { - logger.LogError(call.Exception, "{Message}", message); - } - if (call.HttpResponseMessage != null) { + var errorContent = JsonSerializer.Deserialize(call.HttpResponseMessage.Content.ReadAsStream())?.ErrorMessage; + if (!string.IsNullOrWhiteSpace(errorContent)) + { + message = errorContent; + } throw call.HttpResponseMessage.StatusCode switch { HttpStatusCode.NotFound => new KeycloakEntityNotFoundException(message, call.Exception), HttpStatusCode.Conflict => new KeycloakEntityConflictException(message, call.Exception), - HttpStatusCode.BadRequest => new ArgumentException(message, call.Exception), + HttpStatusCode.BadRequest => new KeycloakNoSuccessException(message, call.Exception), _ => new ServiceException(message, call.Exception, call.HttpResponseMessage.StatusCode), }; } + throw new ServiceException(message, call.Exception); }); } - private static void LogDevelopmentError(ILogger logger, FlurlCall call) + private static void LogDebug(ILogger logger, FlurlCall call) { var request = call.HttpRequestMessage == null ? "" : $"{call.HttpRequestMessage.Method} {call.HttpRequestMessage.RequestUri} HTTP/{call.HttpRequestMessage.Version}\n{call.HttpRequestMessage.Headers}\n"; var requestBody = call.RequestBody == null ? "\n" : call.RequestBody + "\n\n"; var response = call.HttpResponseMessage == null ? "" : call.HttpResponseMessage.ReasonPhrase + "\n"; var responseContent = call.HttpResponseMessage?.Content == null ? "" : call.HttpResponseMessage.Content.ReadAsStringAsync().Result + "\n"; - logger.LogError(call.Exception, "{Request}{Body}{Response}{Content}", request, requestBody, response, responseContent); + logger.LogDebug(call.Exception, "{Request}{Body}{Response}{Content}", request, requestBody, response, responseContent); + } + + public class KeycloakErrorResponse + { + [JsonPropertyName("errorMessage")] + public string? ErrorMessage { get; set; } } } diff --git a/src/keycloak/Keycloak.Factory/KeycloakFactory.cs b/src/keycloak/Keycloak.Factory/KeycloakFactory.cs index 74a5b20ef1..85bf144767 100644 --- a/src/keycloak/Keycloak.Factory/KeycloakFactory.cs +++ b/src/keycloak/Keycloak.Factory/KeycloakFactory.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2022 BMW Group AG * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -18,7 +17,10 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ +using Flurl.Http.Configuration; using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library; @@ -28,6 +30,18 @@ public class KeycloakFactory : IKeycloakFactory { private readonly KeycloakSettingsMap _settings; + private static readonly JsonSerializerSettings SerializerSettings = new() + { + NullValueHandling = NullValueHandling.Ignore, + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy + { + ProcessDictionaryKeys = false + } + } + }; + public KeycloakFactory(IOptions settings) { _settings = settings.Value; @@ -41,9 +55,13 @@ public KeycloakClient CreateKeycloakClient(string instance) } var settings = _settings.Single(x => x.Key.Equals(instance, StringComparison.InvariantCultureIgnoreCase)).Value; - return settings.ClientSecret == null + + var keycloakClient = settings.ClientSecret == null ? new KeycloakClient(settings.ConnectionString, settings.User, settings.Password, settings.AuthRealm, settings.UseAuthTrail) : KeycloakClient.CreateWithClientId(settings.ConnectionString, settings.ClientId, settings.ClientSecret, settings.UseAuthTrail, settings.AuthRealm); + keycloakClient.SetSerializer(new NewtonsoftJsonSerializer(SerializerSettings)); + + return keycloakClient; } public KeycloakClient CreateKeycloakClient(string instance, string clientId, string secret) @@ -54,6 +72,9 @@ public KeycloakClient CreateKeycloakClient(string instance, string clientId, str } var settings = _settings.Single(x => x.Key.Equals(instance, StringComparison.InvariantCultureIgnoreCase)).Value; - return KeycloakClient.CreateWithClientId(settings.ConnectionString, clientId, secret, settings.UseAuthTrail, settings.AuthRealm); + var keycloakClient = KeycloakClient.CreateWithClientId(settings.ConnectionString, clientId, secret, settings.UseAuthTrail, settings.AuthRealm); + keycloakClient.SetSerializer(new NewtonsoftJsonSerializer(SerializerSettings)); + + return keycloakClient; } } diff --git a/src/keycloak/Keycloak.Factory/KeycloakSettingData.cs b/src/keycloak/Keycloak.Factory/KeycloakSettingData.cs index 9fa4b34523..bdbdcc3bcb 100644 --- a/src/keycloak/Keycloak.Factory/KeycloakSettingData.cs +++ b/src/keycloak/Keycloak.Factory/KeycloakSettingData.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2023 BMW Group AG * Copyright (c) 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -65,7 +64,7 @@ public sealed class KeycloakSettingsMap : Dictionary { public bool Validate() { - if (!Values.Any()) + if (Values.Count == 0) { throw new ConfigurationException(); } diff --git a/src/keycloak/Keycloak.Library/Clients/KeycloakClient.cs b/src/keycloak/Keycloak.Library/Clients/KeycloakClient.cs index 4059521d94..6a7ea6a358 100644 --- a/src/keycloak/Keycloak.Library/Clients/KeycloakClient.cs +++ b/src/keycloak/Keycloak.Library/Clients/KeycloakClient.cs @@ -122,38 +122,38 @@ public async Task GetClientSecretAsync(string realm, string clientI .GetJsonAsync() .ConfigureAwait(ConfigureAwaitOptions.None); - public async Task> GetDefaultClientScopesAsync(string realm, string clientId) => - await (await GetBaseUrlAsync(realm).ConfigureAwait(ConfigureAwaitOptions.None)) + public async Task> GetDefaultClientScopesAsync(string realm, string clientId, CancellationToken cancellationToken = default) => + await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)) .AppendPathSegment("/admin/realms/") .AppendPathSegment(realm, true) .AppendPathSegment("/clients/") .AppendPathSegment(clientId, true) .AppendPathSegment("/default-client-scopes") - .GetJsonAsync>() + .GetJsonAsync>(cancellationToken) .ConfigureAwait(ConfigureAwaitOptions.None); - public async Task UpdateDefaultClientScopeAsync(string realm, string clientId, string clientScopeId) + public async Task UpdateDefaultClientScopeAsync(string realm, string clientId, string clientScopeId, CancellationToken cancellationToken = default) { using var stringContent = new StringContent(""); - await (await GetBaseUrlAsync(realm).ConfigureAwait(ConfigureAwaitOptions.None)) + await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)) .AppendPathSegment("/admin/realms/") .AppendPathSegment(realm, true) .AppendPathSegment("/clients/") .AppendPathSegment(clientId, true) .AppendPathSegment("/default-client-scopes/") .AppendPathSegment(clientScopeId, true) - .PutAsync(stringContent) + .PutAsync(stringContent, cancellationToken) .ConfigureAwait(ConfigureAwaitOptions.None); } - public async Task DeleteDefaultClientScopeAsync(string realm, string clientId, string clientScopeId) => - await (await GetBaseUrlAsync(realm).ConfigureAwait(ConfigureAwaitOptions.None)) + public async Task DeleteDefaultClientScopeAsync(string realm, string clientId, string clientScopeId, CancellationToken cancellationToken = default) => + await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)) .AppendPathSegment("/admin/realms/") .AppendPathSegment(realm, true) .AppendPathSegment("/clients/") .AppendPathSegment(clientId, true) .AppendPathSegment("/default-client-scopes/") .AppendPathSegment(clientScopeId, true) - .DeleteAsync() + .DeleteAsync(cancellationToken) .ConfigureAwait(ConfigureAwaitOptions.None); [Obsolete("Not working yet")] @@ -320,39 +320,39 @@ public async Task> GetClientOfflineSessionsAsync(string .ConfigureAwait(ConfigureAwaitOptions.None); } - public async Task> GetOptionalClientScopesAsync(string realm, string clientId) => - await (await GetBaseUrlAsync(realm).ConfigureAwait(ConfigureAwaitOptions.None)) + public async Task> GetOptionalClientScopesAsync(string realm, string clientId, CancellationToken cancellationToken = default) => + await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)) .AppendPathSegment("/admin/realms/") .AppendPathSegment(realm, true) .AppendPathSegment("/clients/") .AppendPathSegment(clientId, true) .AppendPathSegment("/optional-client-scopes") - .GetJsonAsync>() + .GetJsonAsync>(cancellationToken) .ConfigureAwait(ConfigureAwaitOptions.None); - public async Task UpdateOptionalClientScopeAsync(string realm, string clientId, string clientScopeId) + public async Task UpdateOptionalClientScopeAsync(string realm, string clientId, string clientScopeId, CancellationToken cancellationToken = default) { using var stringContent = new StringContent(""); - await (await GetBaseUrlAsync(realm).ConfigureAwait(ConfigureAwaitOptions.None)) + await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)) .AppendPathSegment("/admin/realms/") .AppendPathSegment(realm, true) .AppendPathSegment("/clients/") .AppendPathSegment(clientId, true) .AppendPathSegment("/optional-client-scopes/") .AppendPathSegment(clientScopeId, true) - .PutAsync(stringContent) + .PutAsync(stringContent, cancellationToken) .ConfigureAwait(ConfigureAwaitOptions.None); } - public async Task DeleteOptionalClientScopeAsync(string realm, string clientId, string clientScopeId) => - await (await GetBaseUrlAsync(realm).ConfigureAwait(ConfigureAwaitOptions.None)) + public async Task DeleteOptionalClientScopeAsync(string realm, string clientId, string clientScopeId, CancellationToken cancellationToken = default) => + await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)) .AppendPathSegment("/admin/realms/") .AppendPathSegment(realm, true) .AppendPathSegment("/clients/") .AppendPathSegment(clientId, true) .AppendPathSegment("/optional-client-scopes/") .AppendPathSegment(clientScopeId, true) - .DeleteAsync() + .DeleteAsync(cancellationToken) .ConfigureAwait(ConfigureAwaitOptions.None); public async Task PushClientRevocationPolicyAsync(string realm, string clientId) diff --git a/src/keycloak/Keycloak.Library/Models/Clients/Client.cs b/src/keycloak/Keycloak.Library/Models/Clients/Client.cs index 5eb6d4bdce..fa6be5e3a2 100644 --- a/src/keycloak/Keycloak.Library/Models/Clients/Client.cs +++ b/src/keycloak/Keycloak.Library/Models/Clients/Client.cs @@ -24,7 +24,7 @@ ********************************************************************************/ using Newtonsoft.Json; - +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.ProtocolMappers; namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Clients; public class Client @@ -82,7 +82,7 @@ public class Client [JsonProperty("nodeReRegistrationTimeout")] public int? NodeReregistrationTimeout { get; set; } [JsonProperty("protocolMappers")] - public IEnumerable? ProtocolMappers { get; set; } + public IEnumerable? ProtocolMappers { get; set; } [JsonProperty("defaultClientScopes")] public IEnumerable? DefaultClientScopes { get; set; } [JsonProperty("optionalClientScopes")] @@ -93,4 +93,6 @@ public class Client public string? Secret { get; set; } [JsonProperty("authorizationServicesEnabled")] public bool? AuthorizationServicesEnabled { get; set; } + [JsonProperty("adminUrl")] + public string? AdminUrl { get; set; } } diff --git a/src/keycloak/Keycloak.Library/Models/Clients/ClientProtocolMapper.cs b/src/keycloak/Keycloak.Library/Models/Clients/ClientProtocolMapper.cs index 392faf1a8e..828c40c4f2 100644 --- a/src/keycloak/Keycloak.Library/Models/Clients/ClientProtocolMapper.cs +++ b/src/keycloak/Keycloak.Library/Models/Clients/ClientProtocolMapper.cs @@ -25,6 +25,7 @@ ********************************************************************************/ using Newtonsoft.Json; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.ProtocolMappers; namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Clients; @@ -41,5 +42,5 @@ public class ClientProtocolMapper [JsonProperty("consentRequired")] public bool? ConsentRequired { get; set; } [JsonProperty("config")] - public ClientConfig? Config { get; set; } + public Config? Config { get; set; } } diff --git a/src/keycloak/Keycloak.Library/Models/ProtocolMappers/Config.cs b/src/keycloak/Keycloak.Library/Models/ProtocolMappers/Config.cs index 66eb91cd27..0407e72c71 100644 --- a/src/keycloak/Keycloak.Library/Models/ProtocolMappers/Config.cs +++ b/src/keycloak/Keycloak.Library/Models/ProtocolMappers/Config.cs @@ -44,6 +44,10 @@ public class Config public string? IdTokenClaim { get; set; } [JsonProperty("access.token.claim")] public string? AccessTokenClaim { get; set; } + [JsonProperty("introspection.token.claim")] + public string? IntrospectionTokenClaim { get; set; } + [JsonProperty("lightweight.claim")] + public string? LightweightClaim { get; set; } [JsonProperty("claim.name")] public string? ClaimName { get; set; } [JsonProperty("jsonType.label")] @@ -62,9 +66,10 @@ public class Config public string? UserAttributeLocality { get; set; } [JsonProperty("included.client.audience")] public string? IncludedClientAudience { get; set; } + [JsonProperty("included.custom.audience")] + public string? IncludedCustomAudience { get; set; } [JsonProperty("multivalued")] public string? Multivalued { get; set; } - [JsonProperty("user.session.note")] public string? UserSessionNote { get; set; } } diff --git a/src/keycloak/Keycloak.Library/Models/RealmsAdmin/PartialImport.cs b/src/keycloak/Keycloak.Library/Models/RealmsAdmin/PartialImport.cs index 10819578df..71f3b901fb 100644 --- a/src/keycloak/Keycloak.Library/Models/RealmsAdmin/PartialImport.cs +++ b/src/keycloak/Keycloak.Library/Models/RealmsAdmin/PartialImport.cs @@ -34,12 +34,12 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.RealmsAdmi public class PartialImport { - public IEnumerable Clients { get; set; } - public IEnumerable Groups { get; set; } - public IEnumerable IdentityProviders { get; set; } - public string IfResourceExists { get; set; } + public IEnumerable? Clients { get; set; } + public IEnumerable? Groups { get; set; } + public IEnumerable? IdentityProviders { get; set; } + public string? IfResourceExists { get; set; } [JsonConverter(typeof(PoliciesConverter))] - public Policies Policy { get; set; } - public Roles Roles { get; set; } - public IEnumerable Users { get; set; } + public Policies? Policy { get; set; } + public Roles? Roles { get; set; } + public IEnumerable? Users { get; set; } } diff --git a/src/keycloak/Keycloak.Library/Models/RealmsAdmin/PartialImportResponse.cs b/src/keycloak/Keycloak.Library/Models/RealmsAdmin/PartialImportResponse.cs new file mode 100644 index 0000000000..39f710640c --- /dev/null +++ b/src/keycloak/Keycloak.Library/Models/RealmsAdmin/PartialImportResponse.cs @@ -0,0 +1,36 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.RealmsAdmin; + +public class PartialImportResponse +{ + public int? Overwritten { get; set; } + public int? Added { get; set; } + public int? Skipped { get; set; } + public IEnumerable? Results { get; set; } +} + +public class PartialImportResult +{ + public string? Action { get; set; } + public string? ResourceType { get; set; } + public string? ResourceName { get; set; } + public string? Id { get; set; } +} diff --git a/src/keycloak/Keycloak.Library/Models/Users/Credentials.cs b/src/keycloak/Keycloak.Library/Models/Users/Credentials.cs index 20369fddd3..995bf5b58d 100644 --- a/src/keycloak/Keycloak.Library/Models/Users/Credentials.cs +++ b/src/keycloak/Keycloak.Library/Models/Users/Credentials.cs @@ -31,29 +31,29 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Users; public class Credentials { [JsonProperty("algorithm")] - public string Algorithm { get; set; } + public string? Algorithm { get; set; } [JsonProperty("config")] - public IDictionary Config { get; set; } + public IDictionary? Config { get; set; } [JsonProperty("counter")] public int? Counter { get; set; } [JsonProperty("createdDate")] public long? CreatedDate { get; set; } [JsonProperty("device")] - public string Device { get; set; } + public string? Device { get; set; } [JsonProperty("digits")] public int? Digits { get; set; } [JsonProperty("hashIterations")] public int? HashIterations { get; set; } [JsonProperty("hashSaltedValue")] - public string HashSaltedValue { get; set; } + public string? HashSaltedValue { get; set; } [JsonProperty("period")] public int? Period { get; set; } [JsonProperty("salt")] - public string Salt { get; set; } + public string? Salt { get; set; } [JsonProperty("temporary")] public bool? Temporary { get; set; } [JsonProperty("type")] - public string Type { get; set; } + public string? Type { get; set; } [JsonProperty("value")] - public string Value { get; set; } + public string? Value { get; set; } } diff --git a/src/keycloak/Keycloak.Library/Models/Users/UserConsent.cs b/src/keycloak/Keycloak.Library/Models/Users/UserConsent.cs index 20292ccdbe..e643238ca6 100644 --- a/src/keycloak/Keycloak.Library/Models/Users/UserConsent.cs +++ b/src/keycloak/Keycloak.Library/Models/Users/UserConsent.cs @@ -31,11 +31,13 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Users; public class UserConsent { [JsonProperty("clientId")] - public string ClientId { get; set; } + public string? ClientId { get; set; } [JsonProperty("grantedClientScopes")] - public IEnumerable GrantedClientScopes { get; set; } + public IEnumerable? GrantedClientScopes { get; set; } [JsonProperty("createdDate")] public long? CreatedDate { get; set; } [JsonProperty("lastUpdatedDate")] public long? LastUpdatedDate { get; set; } + [JsonProperty("additionalGrants")] + public IEnumerable? AdditionalGrants { get; set; } } diff --git a/src/keycloak/Keycloak.Library/RealmsAdmin/KeycloakClient.cs b/src/keycloak/Keycloak.Library/RealmsAdmin/KeycloakClient.cs index 149fc9aa1f..7dba5e88a7 100644 --- a/src/keycloak/Keycloak.Library/RealmsAdmin/KeycloakClient.cs +++ b/src/keycloak/Keycloak.Library/RealmsAdmin/KeycloakClient.cs @@ -331,12 +331,13 @@ public async Task RealmPartialExportAsync(string realm, bool? exportClien .ConfigureAwait(ConfigureAwaitOptions.None); } - public async Task RealmPartialImportAsync(string realm, PartialImport rep) => - await (await GetBaseUrlAsync(realm).ConfigureAwait(ConfigureAwaitOptions.None)) + public async Task RealmPartialImportAsync(string realm, PartialImport rep, CancellationToken cancellationToken = default) => + await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)) .AppendPathSegment("/admin/realms/") .AppendPathSegment(realm, true) .AppendPathSegment("/partialImport") - .PostJsonAsync(rep) + .PostJsonAsync(rep, cancellationToken) + .ReceiveJson() .ConfigureAwait(ConfigureAwaitOptions.None); public async Task PushRealmRevocationPolicyAsync(string realm) diff --git a/src/keycloak/Keycloak.Library/Users/KeycloakClient.cs b/src/keycloak/Keycloak.Library/Users/KeycloakClient.cs index c64d64b7e9..839c6fc6b3 100644 --- a/src/keycloak/Keycloak.Library/Users/KeycloakClient.cs +++ b/src/keycloak/Keycloak.Library/Users/KeycloakClient.cs @@ -107,26 +107,25 @@ public async Task DeleteUserAsync(string realm, string userId) => .DeleteAsync() .ConfigureAwait(ConfigureAwaitOptions.None); - [Obsolete("Not working yet")] - public async Task GetUserConsentsAsync(string realm, string userId) => - await (await GetBaseUrlAsync(realm).ConfigureAwait(ConfigureAwaitOptions.None)) + public async Task> GetUserConsentsAsync(string realm, string userId, CancellationToken cancellationToken = default) => + await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)) .AppendPathSegment("/admin/realms/") .AppendPathSegment(realm, true) .AppendPathSegment("/users/") .AppendPathSegment(userId, true) .AppendPathSegment("/consents") - .GetStringAsync() + .GetJsonAsync>(cancellationToken) .ConfigureAwait(ConfigureAwaitOptions.None); - public async Task RevokeUserConsentAndOfflineTokensAsync(string realm, string userId, string clientId) => - await (await GetBaseUrlAsync(realm).ConfigureAwait(ConfigureAwaitOptions.None)) + public async Task RevokeUserConsentAndOfflineTokensAsync(string realm, string userId, string clientId, CancellationToken cancellationToken = default) => + await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)) .AppendPathSegment("/admin/realms/") .AppendPathSegment(realm, true) .AppendPathSegment("/users/") .AppendPathSegment(userId, true) .AppendPathSegment("/consents/") .AppendPathSegment(clientId, true) - .DeleteAsync() + .DeleteAsync(cancellationToken) .ConfigureAwait(ConfigureAwaitOptions.None); public async Task DisableUserCredentialsAsync(string realm, string userId, IEnumerable credentialTypes) => diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/AuthenticationFlowsUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/AuthenticationFlowsUpdater.cs index b41330b041..af6f2d65f4 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/AuthenticationFlowsUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/AuthenticationFlowsUpdater.cs @@ -63,7 +63,7 @@ public AuthenticationFlowHandler(KeycloakClient keycloak, ISeedDataHandler seedD public async Task UpdateAuthenticationFlows(CancellationToken cancellationToken) { - var flows = (await _keycloak.GetAuthenticationFlowsAsync(_realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)); + var flows = await _keycloak.GetAuthenticationFlowsAsync(_realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); var seedFlows = _seedData.TopLevelCustomAuthenticationFlows; var topLevelCustomFlows = flows.Where(flow => !(flow.BuiltIn ?? false) && (flow.TopLevel ?? false)); @@ -275,7 +275,7 @@ await _keycloak.CreateAuthenticationExecutionConfigurationAsync( new AuthenticatorConfig { Alias = update.AuthenticatorConfig, - Config = _seedData.GetAuthenticatorConfig(update.AuthenticatorConfig).Config?.ToDictionary(x => x.Key, x => x.Value) + Config = _seedData.GetAuthenticatorConfig(update.AuthenticatorConfig).Config?.FilterNotNullValues().ToDictionary() }, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); break; @@ -292,7 +292,7 @@ await _keycloak.DeleteAuthenticatorConfigurationAsync( if (config == null) throw new UnexpectedConditionException("authenticatorConfig is null"); config.Alias = update.AuthenticatorConfig; - config.Config = updateConfig.Config?.ToDictionary(x => x.Key, x => x.Value); + config.Config = updateConfig.Config?.FilterNotNullValues().ToDictionary(); await _keycloak.UpdateAuthenticatorConfigurationAsync(_realm, execution.AuthenticationConfig, config, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); break; } @@ -324,7 +324,7 @@ private bool CompareFlowExecutions(AuthenticationFlowExecution execution, Authen private static bool CompareAuthenticatorConfig(AuthenticatorConfig config, AuthenticatorConfigModel update) => config.Alias == update.Alias && - config.Config.NullOrContentEqual(update.Config); + config.Config.NullOrContentEqual(update.Config?.FilterNotNullValues()); private Task> GetExecutions(string alias, CancellationToken cancellationToken) => _keycloak.GetAuthenticationFlowExecutionsAsync(_realm, alias, cancellationToken); diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientScopeMapperUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientScopeMapperUpdater.cs index 636944f434..bb74869231 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientScopeMapperUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientScopeMapperUpdater.cs @@ -18,6 +18,7 @@ ********************************************************************************/ using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Linq; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Factory; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Roles; @@ -58,7 +59,7 @@ public async Task UpdateClientScopeMapper(string instanceName, CancellationToken throw new ConflictException($"No client id found with name {clientName}"); } var clientRoles = await keycloak.GetClientRolesScopeMappingsForClientAsync(realm, clientScope.Id, client.Id, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - var mappingModelRoles = mappingModel.Roles.Select(roleName => roles.SingleOrDefault(r => r.Name == roleName) ?? throw new ConflictException($"No role with name {roleName} found")); + var mappingModelRoles = mappingModel.Roles?.Select(roleName => roles.SingleOrDefault(r => r.Name == roleName) ?? throw new ConflictException($"No role with name {roleName} found")) ?? Enumerable.Empty(); await AddAndDeleteRoles(keycloak, realm, clientScope.Id, client.Id, clientRoles, mappingModelRoles, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } } @@ -66,16 +67,10 @@ public async Task UpdateClientScopeMapper(string instanceName, CancellationToken private static async Task AddAndDeleteRoles(KeycloakClient keycloak, string realm, string clientScopeId, string clientId, IEnumerable roles, IEnumerable updateRoles, CancellationToken cancellationToken) { - var rolesToAdd = updateRoles.ExceptBy(roles.Select(role => role.Name), roleModel => roleModel.Name).ToList(); - var rolesToDelete = roles.ExceptBy(updateRoles.Select(roleModel => roleModel.Name), role => role.Name).ToList(); - if (rolesToDelete.Any()) - { - await keycloak.RemoveClientRolesFromClientScopeForClientAsync(realm, clientScopeId, clientId, rolesToDelete, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - } + await updateRoles.ExceptBy(roles.Select(role => role.Name), roleModel => roleModel.Name).IfAnyAwait(rolesToAdd => + keycloak.AddClientRolesScopeMappingToClientAsync(realm, clientScopeId, clientId, rolesToAdd, cancellationToken)).ConfigureAwait(false); - if (rolesToAdd.Any()) - { - await keycloak.AddClientRolesScopeMappingToClientAsync(realm, clientScopeId, clientId, rolesToAdd, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - } + await roles.ExceptBy(updateRoles.Select(roleModel => roleModel.Name), role => role.Name).IfAnyAwait(rolesToDelete => + keycloak.RemoveClientRolesFromClientScopeForClientAsync(realm, clientScopeId, clientId, rolesToDelete, cancellationToken)).ConfigureAwait(false); } } diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientScopesUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientScopesUpdater.cs index 890c46ce35..6419a4f844 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientScopesUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientScopesUpdater.cs @@ -165,15 +165,15 @@ private static bool CompareClientScope(ClientScope scope, ClientScopeModel updat scope.Attributes != null && update.Attributes != null && CompareClientScopeAttributes(scope.Attributes, update.Attributes)); - private static Attributes CreateClientScopeAttributes(IReadOnlyDictionary update) => - new Attributes + private static Attributes CreateClientScopeAttributes(IReadOnlyDictionary update) => + new() { ConsentScreenText = update.GetValueOrDefault("consent.screen.text"), DisplayOnConsentScreen = update.GetValueOrDefault("display.on.consent.screen"), IncludeInTokenScope = update.GetValueOrDefault("include.in.token.scope") }; - private static bool CompareClientScopeAttributes(Attributes attributes, IReadOnlyDictionary update) => + private static bool CompareClientScopeAttributes(Attributes attributes, IReadOnlyDictionary update) => attributes.ConsentScreenText == update.GetValueOrDefault("consent.screen.text") && attributes.DisplayOnConsentScreen == update.GetValueOrDefault("display.on.consent.screen") && attributes.IncludeInTokenScope == update.GetValueOrDefault("include.in.token.scope"); diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientsUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientsUpdater.cs index e4b41861c9..92582fd248 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientsUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientsUpdater.cs @@ -19,10 +19,11 @@ using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; using Org.Eclipse.TractusX.Portal.Backend.Framework.Linq; -using Org.Eclipse.TractusX.Portal.Backend.Keycloak.ErrorHandling; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Factory; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Clients; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.ProtocolMappers; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.RealmsAdmin; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; using System.Runtime.CompilerServices; @@ -48,107 +49,161 @@ public Task UpdateClients(string keycloakInstanceName, CancellationToken cancell private async IAsyncEnumerable<(string ClientId, string Id)> UpdateClientsInternal(KeycloakClient keycloak, string realm, [EnumeratorCancellation] CancellationToken cancellationToken) { + var clientScopes = await keycloak.GetClientScopesAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + string GetClientScopeId(string scope) => clientScopes.SingleOrDefault(x => x.Name == scope)?.Id ?? throw new ConflictException($"id of clientScope {scope} is undefined"); + foreach (var update in _seedData.Clients) { if (update.ClientId == null) throw new ConflictException($"clientId must not be null {update.Id}"); + var client = (await keycloak.GetClientsAsync(realm, clientId: update.ClientId, cancellationToken: cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)).SingleOrDefault(x => x.ClientId == update.ClientId); if (client == null) { - var id = await keycloak.CreateClientAndRetrieveClientIdAsync(realm, CreateUpdateClient(null, update), cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - if (id == null) - throw new KeycloakNoSuccessException($"creation of client {update.ClientId} did not return the expected result"); - - // load newly created client as keycloak may create default protocolmappers on client-creation - client = await keycloak.GetClientAsync(realm, id).ConfigureAwait(ConfigureAwaitOptions.None); + client = await CreateClient(keycloak, realm, update, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } - - if (client.Id == null) - throw new ConflictException($"client.Id must not be null: clientId {update.ClientId}"); - - if (!CompareClient(client, update)) + else { - var updateClient = CreateUpdateClient(client, update); - await keycloak.UpdateClientAsync( + await UpdateClient( + keycloak, + realm, + client.Id ?? throw new ConflictException($"client.Id must not be null: clientId {update.ClientId}"), + client, + update, + cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + + await UpdateClientProtocolMappers( + keycloak, realm, client.Id, - updateClient, + client, + update, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } - await UpdateClientProtocollMappers(keycloak, realm, client.Id, client, update, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await UpdateDefaultClientScopes( + keycloak, + realm, + client.Id ?? throw new ConflictException($"client.Id must not be null: clientId {update.ClientId}"), + client, + update, + GetClientScopeId, + cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + + await UpdateOptionalClientScopes( + keycloak, + realm, + client.Id, + client, + update, + GetClientScopeId, + cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); yield return (update.ClientId, client.Id); } } - private static async Task UpdateClientProtocollMappers(KeycloakClient keycloak, string realm, string clientId, Client client, ClientModel update, CancellationToken cancellationToken) + private static async Task CreateClient(KeycloakClient keycloak, string realm, ClientModel update, CancellationToken cancellationToken) { - var clientProtocolMappers = client.ProtocolMappers ?? Enumerable.Empty(); - var updateProtocolMappers = update.ProtocolMappers ?? Enumerable.Empty(); - - await DeleteObsoleteClientProtocolMappers(keycloak, realm, clientId, clientProtocolMappers, updateProtocolMappers, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await CreateMissingClientProtocolMappers(keycloak, realm, clientId, clientProtocolMappers, updateProtocolMappers, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await UpdateExistingClientProtocolMappers(keycloak, realm, clientId, clientProtocolMappers, updateProtocolMappers, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + var result = await keycloak.RealmPartialImportAsync(realm, CreatePartialImportClient(update), cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + if (result.Overwritten != 0 || result.Added != 1 || result.Skipped != 0) + { + throw new ConflictException($"PartialImport failed to add client id: {update.Id}, clientId: {update.ClientId}"); + } + var client = (await keycloak.GetClientsAsync(realm, clientId: update.ClientId, cancellationToken: cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)).SingleOrDefault(x => x.ClientId == update.ClientId); + return client ?? throw new ConflictException($"failed to read newly created client {update.ClientId}"); } - private static async Task DeleteObsoleteClientProtocolMappers(KeycloakClient keycloak, string realm, string clientId, IEnumerable clientProtocolMappers, IEnumerable updateProtocolMappers, CancellationToken cancellationToken) + private static async Task UpdateClient(KeycloakClient keycloak, string realm, string idOfClient, Client client, ClientModel seedClient, CancellationToken cancellationToken) { - foreach (var mapper in clientProtocolMappers.ExceptBy(updateProtocolMappers.Select(x => x.Name), x => x.Name)) + if (!CompareClient(client, seedClient)) { - await keycloak.DeleteClientProtocolMapperAsync( + var updateClient = CreateUpdateClient(client, seedClient); + await keycloak.UpdateClientAsync( realm, - clientId, - mapper.Id ?? throw new ConflictException($"protocolMapper.Id is null {mapper.Name}"), + idOfClient, + updateClient, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } } - private static async Task CreateMissingClientProtocolMappers(KeycloakClient keycloak, string realm, string clientId, IEnumerable clientProtocolMappers, IEnumerable updateProtocolMappers, CancellationToken cancellationToken) + private static async Task UpdateClientProtocolMappers(KeycloakClient keycloak, string realm, string clientId, Client client, ClientModel update, CancellationToken cancellationToken) { - foreach (var update in updateProtocolMappers.ExceptBy(clientProtocolMappers.Select(x => x.Name), x => x.Name)) + var clientProtocolMappers = client.ProtocolMappers ?? Enumerable.Empty(); + var updateProtocolMappers = update.ProtocolMappers ?? Enumerable.Empty(); + + foreach (var mapperId in clientProtocolMappers.ExceptBy(updateProtocolMappers.Select(x => x.Name), x => x.Name).Select(x => x.Id ?? throw new ConflictException($"protocolMapper.Id is null {x.Name}"))) { - await keycloak.CreateClientProtocolMapperAsync( - realm, - clientId, - ProtocolMappersUpdater.CreateProtocolMapper(null, update), - cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await keycloak.DeleteClientProtocolMapperAsync(realm, clientId, mapperId, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } - } - private static async Task UpdateExistingClientProtocolMappers(KeycloakClient keycloak, string realm, string clientId, IEnumerable clientProtocolMappers, IEnumerable updateProtocolMappers, CancellationToken cancellationToken) - { - foreach (var (mapper, update) in clientProtocolMappers + foreach (var mapper in updateProtocolMappers.ExceptBy(clientProtocolMappers.Select(x => x.Name), x => x.Name).Select(x => ProtocolMappersUpdater.CreateProtocolMapper(null, x))) + { + await keycloak.CreateClientProtocolMapperAsync(realm, clientId, mapper, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + } + + foreach (var (mapperId, mapper) in clientProtocolMappers .Join( updateProtocolMappers, x => x.Name, x => x.Name, (mapper, update) => (Mapper: mapper, Update: update)) - .Where( - x => !CompareClientProtocolMapper(x.Mapper, x.Update))) + .Where(x => !ProtocolMappersUpdater.CompareProtocolMapper(x.Mapper, x.Update)) + .Select(x => ( + x.Mapper.Id ?? throw new ConflictException($"protocolMapper.Id is null {x.Mapper.Name}"), + ProtocolMappersUpdater.CreateProtocolMapper(x.Mapper.Id, x.Update)))) { - await keycloak.UpdateClientProtocolMapperAsync( - realm, - clientId, - mapper.Id ?? throw new ConflictException($"protocolMapper.Id is null {mapper.Name}"), - ProtocolMappersUpdater.CreateProtocolMapper(mapper.Id, update), - cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await keycloak.UpdateClientProtocolMapperAsync(realm, clientId, mapperId, mapper, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } } - private static Client CreateUpdateClient(Client? client, ClientModel update) => new() // secret is not updated as it cannot be read via the keycloak api + private static async Task UpdateOptionalClientScopes(KeycloakClient keycloak, string realm, string idOfClient, Client client, ClientModel update, Func getClientScopeId, CancellationToken cancellationToken) { + var optionalScopes = client.OptionalClientScopes ?? Enumerable.Empty(); + var updateScopes = update.OptionalClientScopes ?? Enumerable.Empty(); + + foreach (var scopeId in optionalScopes.Except(updateScopes).Select(getClientScopeId)) + { + await keycloak.DeleteOptionalClientScopeAsync(realm, idOfClient, scopeId, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + } + + foreach (var scopeId in updateScopes.Except(optionalScopes).Select(getClientScopeId)) + { + await keycloak.UpdateOptionalClientScopeAsync(realm, idOfClient, scopeId, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + } + } + + private static async Task UpdateDefaultClientScopes(KeycloakClient keycloak, string realm, string idOfClient, Client client, ClientModel update, Func getClientScopeId, CancellationToken cancellationToken) + { + var defaultScopes = client.DefaultClientScopes ?? Enumerable.Empty(); + var updateScopes = update.DefaultClientScopes ?? Enumerable.Empty(); + + foreach (var scopeId in defaultScopes.Except(updateScopes).Select(getClientScopeId)) + { + await keycloak.DeleteDefaultClientScopeAsync(realm, idOfClient, scopeId, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + } + + foreach (var scopeId in updateScopes.Except(defaultScopes).Select(getClientScopeId)) + { + await keycloak.UpdateDefaultClientScopeAsync(realm, idOfClient, scopeId, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + } + } + + private static Client CreateUpdateClient(Client? client, ClientModel update) => new() + { + // DefaultClientScopes and OptionalClientScopes are not in scope Id = client?.Id, ClientId = update.ClientId, - RootUrl = client?.RootUrl ?? update.RootUrl, // only set the root url if no url is already set + RootUrl = update.RootUrl, Name = update.Name, Description = update.Description, - BaseUrl = client?.BaseUrl ?? update.BaseUrl, // only set the base url if no url is already set + BaseUrl = update.BaseUrl, + AdminUrl = update.AdminUrl, SurrogateAuthRequired = update.SurrogateAuthRequired, Enabled = update.Enabled, AlwaysDisplayInConsole = update.AlwaysDisplayInConsole, ClientAuthenticatorType = update.ClientAuthenticatorType, - RedirectUris = client == null || client.RedirectUris.IsNullOrEmpty() ? update.RedirectUris : client.RedirectUris, // only set the redirect uris if there aren't any set + RedirectUris = update.RedirectUris, WebOrigins = update.WebOrigins, NotBefore = update.NotBefore, BearerOnly = update.BearerOnly, @@ -160,12 +215,10 @@ await keycloak.UpdateClientProtocolMapperAsync( PublicClient = update.PublicClient, FrontChannelLogout = update.FrontchannelLogout, Protocol = update.Protocol, - Attributes = update.Attributes?.ToDictionary(x => x.Key, x => x.Value), - AuthenticationFlowBindingOverrides = update.AuthenticationFlowBindingOverrides?.ToDictionary(x => x.Key, x => x.Value), + Attributes = update.Attributes?.FilterNotNullValues().ToDictionary(), + AuthenticationFlowBindingOverrides = update.AuthenticationFlowBindingOverrides?.FilterNotNullValues().ToDictionary(), FullScopeAllowed = update.FullScopeAllowed, NodeReregistrationTimeout = update.NodeReRegistrationTimeout, - DefaultClientScopes = update.DefaultClientScopes, - OptionalClientScopes = update.OptionalClientScopes, Access = update.Access == null ? null : new ClientAccess @@ -174,15 +227,17 @@ await keycloak.UpdateClientProtocolMapperAsync( Configure = update.Access.Configure, Manage = update.Access.Manage }, - AuthorizationServicesEnabled = update.AuthorizationServicesEnabled + AuthorizationServicesEnabled = update.AuthorizationServicesEnabled, + Secret = update.Secret }; - private static bool CompareClient(Client client, ClientModel update) => // secret is not compared as it cannot be read via the keycloak api + private static bool CompareClient(Client client, ClientModel update) => client.ClientId == update.ClientId && client.RootUrl == update.RootUrl && client.Name == update.Name && client.Description == update.Description && client.BaseUrl == update.BaseUrl && + client.AdminUrl == update.AdminUrl && client.SurrogateAuthRequired == update.SurrogateAuthRequired && client.Enabled == update.Enabled && client.AlwaysDisplayInConsole == update.AlwaysDisplayInConsole && @@ -199,14 +254,13 @@ private static bool CompareClient(Client client, ClientModel update) => // secre client.PublicClient == update.PublicClient && client.FrontChannelLogout == update.FrontchannelLogout && client.Protocol == update.Protocol && - client.Attributes.NullOrContentEqual(update.Attributes) && - client.AuthenticationFlowBindingOverrides.NullOrContentEqual(update.AuthenticationFlowBindingOverrides) && + client.Attributes.NullOrContentEqual(update.Attributes?.FilterNotNullValues()) && + client.AuthenticationFlowBindingOverrides.NullOrContentEqual(update.AuthenticationFlowBindingOverrides?.FilterNotNullValues()) && client.FullScopeAllowed == update.FullScopeAllowed && client.NodeReregistrationTimeout == update.NodeReRegistrationTimeout && - client.DefaultClientScopes.NullOrContentEqual(update.DefaultClientScopes) && - client.OptionalClientScopes.NullOrContentEqual(update.OptionalClientScopes) && CompareClientAccess(client.Access, update.Access) && - client.AuthorizationServicesEnabled == update.AuthorizationServicesEnabled; + client.AuthorizationServicesEnabled == update.AuthorizationServicesEnabled && + client.Secret == update.Secret; private static bool CompareClientAccess(ClientAccess? access, ClientAccessModel? updateAccess) => access == null && updateAccess == null || @@ -215,23 +269,54 @@ private static bool CompareClientAccess(ClientAccess? access, ClientAccessModel? access.Manage == updateAccess.Manage && access.View == updateAccess.View; - private static bool CompareClientProtocolMapper(ClientProtocolMapper mapper, ProtocolMapperModel update) => - mapper.Name == update.Name && - mapper.Protocol == update.Protocol && - mapper.ProtocolMapper == update.ProtocolMapper && - mapper.ConsentRequired == update.ConsentRequired && - (mapper.Config == null && update.Config == null || - mapper.Config != null && update.Config != null && - CompareClientProtocolMapperConfig(mapper.Config, update.Config)); - - private static bool CompareClientProtocolMapperConfig(ClientConfig config, IReadOnlyDictionary update) => - config.UserInfoTokenClaim == update.GetValueOrDefault("userinfo.token.claim") && - config.UserAttribute == update.GetValueOrDefault("user.attribute") && - config.IdTokenClaim == update.GetValueOrDefault("id.token.claim") && - config.AccessTokenClaim == update.GetValueOrDefault("access.token.claim") && - config.ClaimName == update.GetValueOrDefault("claim.name") && - config.JsonTypelabel == update.GetValueOrDefault("jsonType.label") && - config.FriendlyName == update.GetValueOrDefault("friendly.name") && - config.AttributeName == update.GetValueOrDefault("attribute.name") && - config.UserSessionNote == update.GetValueOrDefault("user.session.note"); + private static PartialImport CreatePartialImportClient(ClientModel update) => + new() + { + IfResourceExists = "FAIL", + Clients = [ + new() + { + Id = update.Id, + ClientId = update.ClientId, + RootUrl = update.RootUrl, + Name = update.Name, + Description = update.Description, + BaseUrl = update.BaseUrl, + AdminUrl = update.AdminUrl, + SurrogateAuthRequired = update.SurrogateAuthRequired, + Enabled = update.Enabled, + AlwaysDisplayInConsole = update.AlwaysDisplayInConsole, + ClientAuthenticatorType = update.ClientAuthenticatorType, + RedirectUris = update.RedirectUris, + WebOrigins = update.WebOrigins, + NotBefore = update.NotBefore, + BearerOnly = update.BearerOnly, + ConsentRequired = update.ConsentRequired, + StandardFlowEnabled = update.StandardFlowEnabled, + ImplicitFlowEnabled = update.ImplicitFlowEnabled, + DirectAccessGrantsEnabled = update.DirectAccessGrantsEnabled, + ServiceAccountsEnabled = update.ServiceAccountsEnabled, + PublicClient = update.PublicClient, + FrontChannelLogout = update.FrontchannelLogout, + Protocol = update.Protocol, + Attributes = update.Attributes?.FilterNotNullValues().ToDictionary(), + AuthenticationFlowBindingOverrides = update.AuthenticationFlowBindingOverrides?.FilterNotNullValues().ToDictionary(), + FullScopeAllowed = update.FullScopeAllowed, + NodeReregistrationTimeout = update.NodeReRegistrationTimeout, + ProtocolMappers = update.ProtocolMappers?.Select(x => ProtocolMappersUpdater.CreateProtocolMapper(x.Id, x)), + DefaultClientScopes = update.DefaultClientScopes, + OptionalClientScopes = update.OptionalClientScopes, + Access = update.Access == null + ? null + : new ClientAccess + { + View = update.Access.View, + Configure = update.Access.Configure, + Manage = update.Access.Manage + }, + AuthorizationServicesEnabled = update.AuthorizationServicesEnabled, + Secret = update.Secret + } + ] + }; } diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/IAuthenticationFlowsUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/IAuthenticationFlowsUpdater.cs index 756a82a897..9e5788e7ae 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/IAuthenticationFlowsUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/IAuthenticationFlowsUpdater.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2023 BMW Group AG * Copyright (c) 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/IClientScopesUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/IClientScopesUpdater.cs index c9c32d60a2..b91f909c06 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/IClientScopesUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/IClientScopesUpdater.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2023 BMW Group AG * Copyright (c) 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/IClientsUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/IClientsUpdater.cs index 9857de8397..1f134f74a4 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/IClientsUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/IClientsUpdater.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2023 BMW Group AG * Copyright (c) 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/IIdentityProvidersUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/IIdentityProvidersUpdater.cs index 288053a751..c90655d01f 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/IIdentityProvidersUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/IIdentityProvidersUpdater.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2023 BMW Group AG * Copyright (c) 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/IKeecloakSeeder.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/IKeecloakSeeder.cs index de1da17e72..ba88977f05 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/IKeecloakSeeder.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/IKeecloakSeeder.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2023 BMW Group AG * Copyright (c) 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/IRealmUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/IRealmUpdater.cs index 74778e75f2..e924f10efc 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/IRealmUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/IRealmUpdater.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2023 BMW Group AG * Copyright (c) 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/IRolesUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/IRolesUpdater.cs index ff5fc69e7e..2fadf1e0b4 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/IRolesUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/IRolesUpdater.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2023 BMW Group AG * Copyright (c) 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/ISeedDataHandler.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/ISeedDataHandler.cs index e9685b35b7..3500896c30 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/ISeedDataHandler.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/ISeedDataHandler.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2023 BMW Group AG * Copyright (c) 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -24,7 +23,7 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic; public interface ISeedDataHandler { - Task Import(string path, CancellationToken cancellationToken); + Task Import(KeycloakRealmSettings realmSettings, CancellationToken cancellationToken); string Realm { get; } @@ -32,7 +31,7 @@ public interface ISeedDataHandler IEnumerable Clients { get; } - IReadOnlyDictionary> ClientRoles { get; } + IEnumerable<(string ClientId, IEnumerable RoleModels)> ClientRoles { get; } IEnumerable RealmRoles { get; } @@ -48,7 +47,7 @@ public interface ISeedDataHandler IReadOnlyDictionary ClientsDictionary { get; } - IReadOnlyDictionary> ClientScopeMappings { get; } + IEnumerable<(string ClientId, IEnumerable ClientScopeMappingModels)> ClientScopeMappings { get; } Task SetClientInternalIds(IAsyncEnumerable<(string ClientId, string Id)> clientInternalIds); diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/IUsersUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/IUsersUpdater.cs index 0aec0655cd..e6374da783 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/IUsersUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/IUsersUpdater.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2023 BMW Group AG * Copyright (c) 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -22,5 +21,5 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic; public interface IUsersUpdater { - Task UpdateUsers(string keycloakInstanceName, IEnumerable? excludedUserAttributes, CancellationToken cancellationToken); + Task UpdateUsers(string keycloakInstanceName, CancellationToken cancellationToken); } diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/IdentityProvidersUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/IdentityProvidersUpdater.cs index a38ee33689..a043c8c81e 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/IdentityProvidersUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/IdentityProvidersUpdater.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2023 BMW Group AG * Copyright (c) 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -154,15 +153,15 @@ private static void UpdateIdentityProvider(IdentityProvider provider, IdentityPr DisableUserInfo = update.Config.DisableUserInfo, ValidateSignature = update.Config.ValidateSignature, ClientId = update.Config.ClientId, - TokenUrl = provider.Config?.TokenUrl ?? update.Config.TokenUrl, - AuthorizationUrl = provider.Config?.AuthorizationUrl ?? update.Config.AuthorizationUrl, + TokenUrl = update.Config.TokenUrl, + AuthorizationUrl = update.Config.AuthorizationUrl, ClientAuthMethod = update.Config.ClientAuthMethod, - JwksUrl = provider.Config?.JwksUrl ?? update.Config.JwksUrl, - LogoutUrl = provider.Config?.LogoutUrl ?? update.Config.LogoutUrl, + JwksUrl = update.Config.JwksUrl, + LogoutUrl = update.Config.LogoutUrl, ClientAssertionSigningAlg = update.Config.ClientAssertionSigningAlg, SyncMode = update.Config.SyncMode, UseJwksUrl = update.Config.UseJwksUrl, - UserInfoUrl = provider.Config?.UserInfoUrl ?? update.Config.UserInfoUrl, + UserInfoUrl = update.Config.UserInfoUrl, Issuer = update.Config.Issuer, // for Saml: NameIDPolicyFormat = update.Config.NameIDPolicyFormat, @@ -182,7 +181,7 @@ private static void UpdateIdentityProvider(IdentityProvider provider, IdentityPr ForceAuthn = update.Config.ForceAuthn, SignSpMetadata = update.Config.SignSpMetadata, LoginHint = update.Config.LoginHint, - SingleSignOnServiceUrl = provider.Config?.SingleSignOnServiceUrl ?? update.Config.SingleSignOnServiceUrl, + SingleSignOnServiceUrl = update.Config.SingleSignOnServiceUrl, AllowedClockSkew = update.Config.AllowedClockSkew, AttributeConsumingServiceIndex = update.Config.AttributeConsumingServiceIndex }; @@ -245,12 +244,12 @@ private static bool CompareIdentityProviderConfig(Config? config, IdentityProvid private static IdentityProviderMapper UpdateIdentityProviderMapper(IdentityProviderMapper mapper, IdentityProviderMapperModel updateMapper) { mapper._IdentityProviderMapper = updateMapper.IdentityProviderMapper; - mapper.Config = updateMapper.Config?.ToDictionary(x => x.Key, x => x.Value); + mapper.Config = updateMapper.Config?.FilterNotNullValues().ToDictionary(); return mapper; } private static bool CompareIdentityProviderMapper(IdentityProviderMapper mapper, IdentityProviderMapperModel updateMapper) => mapper.IdentityProviderAlias == updateMapper.IdentityProviderAlias && mapper._IdentityProviderMapper == updateMapper.IdentityProviderMapper && - mapper.Config.NullOrContentEqual(updateMapper.Config); + mapper.Config.NullOrContentEqual(updateMapper.Config?.FilterNotNullValues()); } diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/KeecloakSeeder.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/KeecloakSeeder.cs index 3ec237ff34..aa86dbf5c7 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/KeecloakSeeder.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/KeecloakSeeder.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2023 BMW Group AG * Copyright (c) 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -51,19 +50,19 @@ public KeycloakSeeder(ISeedDataHandler seedDataHandler, IRealmUpdater realmUpdat public async Task Seed(CancellationToken cancellationToken) { - foreach (var dataPath in _settings.DataPathes) + foreach (var realm in _settings.Realms) { - await _seedData.Import(dataPath, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await _realmUpdater.UpdateRealm(_settings.InstanceName, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await _rolesUpdater.UpdateRealmRoles(_settings.InstanceName, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await _clientsUpdater.UpdateClients(_settings.InstanceName, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await _rolesUpdater.UpdateClientRoles(_settings.InstanceName, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await _rolesUpdater.UpdateCompositeRoles(_settings.InstanceName, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await _identityProvidersUpdater.UpdateIdentityProviders(_settings.InstanceName, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await _usersUpdater.UpdateUsers(_settings.InstanceName, _settings.ExcludedUserAttributes, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await _clientScopesUpdater.UpdateClientScopes(_settings.InstanceName, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await _clientScopeMapperUpdater.UpdateClientScopeMapper(_settings.InstanceName, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await _authenticationFlowsUpdater.UpdateAuthenticationFlows(_settings.InstanceName, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await _seedData.Import(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await _realmUpdater.UpdateRealm(realm.InstanceName, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await _rolesUpdater.UpdateRealmRoles(realm.InstanceName, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await _clientScopesUpdater.UpdateClientScopes(realm.InstanceName, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await _clientsUpdater.UpdateClients(realm.InstanceName, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await _rolesUpdater.UpdateClientRoles(realm.InstanceName, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await _rolesUpdater.UpdateCompositeRoles(realm.InstanceName, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await _identityProvidersUpdater.UpdateIdentityProviders(realm.InstanceName, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await _usersUpdater.UpdateUsers(realm.InstanceName, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await _clientScopeMapperUpdater.UpdateClientScopeMapper(realm.InstanceName, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await _authenticationFlowsUpdater.UpdateAuthenticationFlows(realm.InstanceName, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } } } diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/KeycloakSeederSettings.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/KeycloakSeederSettings.cs index e87f4b87d7..203082a006 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/KeycloakSeederSettings.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/KeycloakSeederSettings.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2023 BMW Group AG * Copyright (c) 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -21,6 +20,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Org.Eclipse.TractusX.Portal.Backend.Framework.Models.Validation; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; using System.ComponentModel.DataAnnotations; namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic; @@ -28,13 +28,8 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic; public class KeycloakSeederSettings { [Required] - [DistinctValues] - public IEnumerable DataPathes { get; set; } = null!; - - [Required] - public string InstanceName { get; set; } = null!; - - public IEnumerable? ExcludedUserAttributes { get; set; } + [DistinctValues("x => x.Realm")] + public IEnumerable Realms { get; set; } = null!; } public static class KeycloakSeederSettingsExtensions @@ -46,6 +41,7 @@ IConfigurationSection section { services.AddOptions() .Bind(section) + .ValidateDataAnnotations() .ValidateDistinctValues(section) .ValidateOnStart(); return services; diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/ProtocolMappersUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/ProtocolMappersUpdater.cs index e0d338faad..fad51072a1 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/ProtocolMappersUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/ProtocolMappersUpdater.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2023 BMW Group AG * Copyright (c) 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -44,7 +43,7 @@ public static bool CompareProtocolMapper(ProtocolMapper mapper, ProtocolMapperMo mapper.Config != null && update.Config != null && CompareProtocolMapperConfig(mapper.Config, update.Config)); - private static Config CreateProtocolMapperConfig(IReadOnlyDictionary update) => + private static Config CreateProtocolMapperConfig(IReadOnlyDictionary update) => new Config { Single = update.GetValueOrDefault("single"), @@ -54,6 +53,8 @@ private static Config CreateProtocolMapperConfig(IReadOnlyDictionary update) => + private static bool CompareProtocolMapperConfig(Config config, IReadOnlyDictionary update) => config.Single == update.GetValueOrDefault("single") && config.AttributeNameFormat == update.GetValueOrDefault("attribute.nameformat") && config.AttributeName == update.GetValueOrDefault("attribute.name") && @@ -75,6 +77,8 @@ private static bool CompareProtocolMapperConfig(Config config, IReadOnlyDictiona config.UserAttribute == update.GetValueOrDefault("user.attribute") && config.IdTokenClaim == update.GetValueOrDefault("id.token.claim") && config.AccessTokenClaim == update.GetValueOrDefault("access.token.claim") && + config.IntrospectionTokenClaim == update.GetValueOrDefault("introspection.token.name") && + config.LightweightClaim == update.GetValueOrDefault("lightweight.claim") && config.ClaimName == update.GetValueOrDefault("claim.name") && config.JsonTypelabel == update.GetValueOrDefault("jsonType.label") && config.UserAttributeFormatted == update.GetValueOrDefault("user.attribute.formated") && @@ -84,6 +88,7 @@ private static bool CompareProtocolMapperConfig(Config config, IReadOnlyDictiona config.UserAttributeRegion == update.GetValueOrDefault("user.attribute.region") && config.UserAttributeLocality == update.GetValueOrDefault("user.attribute.locality") && config.IncludedClientAudience == update.GetValueOrDefault("included.client.audience") && + config.IncludedCustomAudience == update.GetValueOrDefault("included.custom.audience") && config.Multivalued == update.GetValueOrDefault("multivalued") && config.UserSessionNote == update.GetValueOrDefault("user.session.note"); } diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/RealmUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/RealmUpdater.cs index 7723e63bdc..1d11cc06a6 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/RealmUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/RealmUpdater.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2023 BMW Group AG * Copyright (c) 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -126,7 +125,7 @@ public async Task UpdateRealm(string keycloakInstanceName, CancellationToken can keycloakRealm.ResetCredentialsFlow = seedRealm.ResetCredentialsFlow; keycloakRealm.ClientAuthenticationFlow = seedRealm.ClientAuthenticationFlow; keycloakRealm.DockerAuthenticationFlow = seedRealm.DockerAuthenticationFlow; - keycloakRealm.Attributes = seedRealm.Attributes?.ToDictionary(x => x.Key, x => x.Value); + keycloakRealm.Attributes = seedRealm.Attributes?.FilterNotNullValues().ToDictionary(); keycloakRealm.UserManagedAccessAllowed = seedRealm.UserManagedAccessAllowed; keycloakRealm.PasswordPolicy = seedRealm.PasswordPolicy; @@ -205,10 +204,8 @@ private static bool CompareRealm(Realm keycloakRealm, KeycloakRealm seedRealm) = keycloakRealm.UserManagedAccessAllowed == seedRealm.UserManagedAccessAllowed && keycloakRealm.PasswordPolicy == seedRealm.PasswordPolicy; - private static bool CompareRealmAttributes(IDictionary? attributes, IReadOnlyDictionary? updateAttributes) => - attributes == null && updateAttributes == null || - attributes != null && updateAttributes != null && - attributes.OrderBy(x => x.Key).SequenceEqual(updateAttributes.OrderBy(x => x.Key)); + private static bool CompareRealmAttributes(IEnumerable>? attributes, IEnumerable>? updateAttributes) => + attributes.NullOrContentEqual(updateAttributes?.FilterNotNullValues()); private static bool CompareBrowserSecurityHeaders(BrowserSecurityHeaders? securityHeaders, BrowserSecurityHeadersModel? updateSecurityHeaders) => securityHeaders == null && updateSecurityHeaders == null || diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/RolesUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/RolesUpdater.cs index 13ac7854fb..928aff5d80 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/RolesUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/RolesUpdater.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2023 BMW Group AG * Copyright (c) 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -136,6 +135,7 @@ async Task UpdateCompositeRolesInner( role => role.Composites?.Client?.Any() ?? false, role => role.ClientRole ?? false, roleModel => roleModel.Composites?.Client? + .FilterNotNullValues() .Select(x => ( Id: _seedData.GetIdOfClient(x.Key), Names: x.Value)) @@ -208,7 +208,7 @@ async Task RemoveAddCompositeRolesInner( private static bool CompareRole(Role role, RoleModel update) => role.Name == update.Name && role.Description == update.Description && - role.Attributes.NullOrContentEqual(update.Attributes); + role.Attributes.NullOrContentEqual(update.Attributes?.FilterNotNullValues()); private static Role CreateRole(RoleModel update) => new Role @@ -217,7 +217,7 @@ private static Role CreateRole(RoleModel update) => Description = update.Description, Composite = update.Composite, ClientRole = update.ClientRole, - Attributes = update.Attributes?.ToDictionary(x => x.Key, x => x.Value) + Attributes = update.Attributes?.FilterNotNullValues().ToDictionary() }; private static Role CreateUpdateRole(string id, string containerId, RoleModel update) diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/SeedDataHandler.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/SeedDataHandler.cs index 082fad1243..0c45684cf0 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/SeedDataHandler.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/SeedDataHandler.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2023 BMW Group AG * Copyright (c) 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -18,7 +17,9 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ +using Org.Eclipse.TractusX.Portal.Backend.Framework.Async; using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Linq; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; using System.Collections.Immutable; using System.Text.Json; @@ -34,71 +35,84 @@ public class SeedDataHandler : ISeedDataHandler PropertyNameCaseInsensitive = false }; - private KeycloakRealm? jsonRealm; + private KeycloakRealm? _keycloakRealm; private IReadOnlyDictionary? _idOfClients; - public async Task Import(string path, CancellationToken cancellationToken) + public async Task Import(KeycloakRealmSettings realmSettings, CancellationToken cancellationToken) { + _keycloakRealm = (await realmSettings.DataPaths + .AggregateAwait( + new KeycloakRealm(), + async (importRealm, path) => importRealm.Merge(await ReadJsonRealm(path, realmSettings.Realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)), + cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)) + .Merge(realmSettings.ToModel()); + + _idOfClients = null; + } + + private static async Task ReadJsonRealm(string path, string realm, CancellationToken cancellationToken) + { + KeycloakRealm jsonRealm; using (var stream = File.OpenRead(path)) { - jsonRealm = - await JsonSerializer.DeserializeAsync(stream, Options, cancellationToken) - .ConfigureAwait(false) ?? throw new ConfigurationException($"cannot deserialize realm from {path}"); + jsonRealm = await JsonSerializer.DeserializeAsync(stream, Options, cancellationToken) + .ConfigureAwait(false) ?? throw new ConfigurationException($"cannot deserialize realm from {path}"); } + if (jsonRealm.Realm != null && jsonRealm.Realm != realm) + throw new ConfigurationException($"json realm {jsonRealm.Realm} doesn't match the configured realm: {realm}"); - _idOfClients = null; + return jsonRealm; } public string Realm { - get => jsonRealm?.Realm ?? throw new ConflictException("realm must not be null"); + get => _keycloakRealm?.Realm ?? throw new ConflictException("realm must not be null"); } public KeycloakRealm KeycloakRealm { - get => jsonRealm ?? throw new InvalidOperationException("Import has not been called"); + get => _keycloakRealm ?? throw new InvalidOperationException("Import has not been called"); } public IEnumerable Clients { - get => jsonRealm?.Clients ?? Enumerable.Empty(); + get => _keycloakRealm?.Clients ?? Enumerable.Empty(); } - public IReadOnlyDictionary> ClientRoles + public IEnumerable<(string ClientId, IEnumerable RoleModels)> ClientRoles { - get => jsonRealm?.Roles?.Client ?? Enumerable.Empty<(string, IEnumerable)>() - .ToImmutableDictionary(x => x.Item1, x => x.Item2); + get => _keycloakRealm?.Roles?.Client?.FilterNotNullValues().Select(x => (x.Key, x.Value)) ?? Enumerable.Empty<(string, IEnumerable)>(); } public IEnumerable RealmRoles { - get => jsonRealm?.Roles?.Realm ?? Enumerable.Empty(); + get => _keycloakRealm?.Roles?.Realm ?? Enumerable.Empty(); } public IEnumerable IdentityProviders { - get => jsonRealm?.IdentityProviders ?? Enumerable.Empty(); + get => _keycloakRealm?.IdentityProviders ?? Enumerable.Empty(); } public IEnumerable IdentityProviderMappers { - get => jsonRealm?.IdentityProviderMappers ?? Enumerable.Empty(); + get => _keycloakRealm?.IdentityProviderMappers ?? Enumerable.Empty(); } public IEnumerable Users { - get => jsonRealm?.Users ?? Enumerable.Empty(); + get => _keycloakRealm?.Users ?? Enumerable.Empty(); } public IEnumerable TopLevelCustomAuthenticationFlows { - get => jsonRealm?.AuthenticationFlows?.Where(x => (x.TopLevel ?? false) && !(x.BuiltIn ?? false)) ?? + get => _keycloakRealm?.AuthenticationFlows?.Where(x => (x.TopLevel ?? false) && !(x.BuiltIn ?? false)) ?? Enumerable.Empty(); } public IEnumerable ClientScopes { - get => jsonRealm?.ClientScopes ?? Enumerable.Empty(); + get => _keycloakRealm?.ClientScopes ?? Enumerable.Empty(); } public IReadOnlyDictionary ClientsDictionary @@ -106,10 +120,9 @@ public IReadOnlyDictionary ClientsDictionary get => _idOfClients ?? throw new InvalidOperationException("ClientInternalIds have not been set"); } - public IReadOnlyDictionary> ClientScopeMappings + public IEnumerable<(string ClientId, IEnumerable ClientScopeMappingModels)> ClientScopeMappings { - get => jsonRealm?.ClientScopeMappings ?? Enumerable.Empty<(string, IEnumerable)>() - .ToImmutableDictionary(x => x.Item1, x => x.Item2); + get => _keycloakRealm?.ClientScopeMappings?.FilterNotNullValues().Select(x => (x.Key, x.Value)) ?? Enumerable.Empty<(string, IEnumerable)>(); } public async Task SetClientInternalIds(IAsyncEnumerable<(string ClientId, string Id)> clientInternalIds) @@ -127,11 +140,11 @@ public string GetIdOfClient(string clientId) => .GetValueOrDefault(clientId) ?? throw new ConflictException($"clientId is unknown or id of client is null {clientId}"); public AuthenticationFlowModel GetAuthenticationFlow(string? alias) => - jsonRealm?.AuthenticationFlows?.SingleOrDefault(x => x.Alias == (alias ?? throw new ConflictException("alias is null"))) ?? throw new ConflictException($"authenticationFlow {alias} does not exist in seeding-data"); + _keycloakRealm?.AuthenticationFlows?.SingleOrDefault(x => x.Alias == (alias ?? throw new ConflictException("alias is null"))) ?? throw new ConflictException($"authenticationFlow {alias} does not exist in seeding-data"); public IEnumerable GetAuthenticationExecutions(string? alias) => GetAuthenticationFlow(alias).AuthenticationExecutions ?? Enumerable.Empty(); public AuthenticatorConfigModel GetAuthenticatorConfig(string? alias) => - jsonRealm?.AuthenticatorConfig?.SingleOrDefault(x => x.Alias == (alias ?? throw new ConflictException("alias is null"))) ?? throw new ConflictException($"authenticatorConfig {alias} does not exist"); + _keycloakRealm?.AuthenticatorConfig?.SingleOrDefault(x => x.Alias == (alias ?? throw new ConflictException("alias is null"))) ?? throw new ConflictException($"authenticatorConfig {alias} does not exist"); } diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/UsersUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/UsersUpdater.cs index 14289c2b6c..f0d9a64ec3 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/UsersUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/UsersUpdater.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2023 BMW Group AG * Copyright (c) 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -20,9 +19,9 @@ using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; using Org.Eclipse.TractusX.Portal.Backend.Framework.Linq; -using Org.Eclipse.TractusX.Portal.Backend.Keycloak.ErrorHandling; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Factory; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.RealmsAdmin; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Roles; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Users; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; @@ -40,7 +39,7 @@ public UsersUpdater(IKeycloakFactory keycloakFactory, ISeedDataHandler seedDataH _seedData = seedDataHandler; } - public async Task UpdateUsers(string keycloakInstanceName, IEnumerable? excludedUserAttributes, CancellationToken cancellationToken) + public async Task UpdateUsers(string keycloakInstanceName, CancellationToken cancellationToken) { var realm = _seedData.Realm; var keycloak = _keycloakFactory.CreateKeycloakClient(keycloakInstanceName); @@ -51,55 +50,57 @@ public async Task UpdateUsers(string keycloakInstanceName, IEnumerable? if (seedUser.Username == null) throw new ConflictException($"username must not be null {seedUser.Id}"); - var userId = await CreateOrUpdateUserReturningId( - keycloak, - realm, - seedUser, - excludedUserAttributes ?? Enumerable.Empty(), - cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - - await UpdateClientAndRealmRoles( - keycloak, - realm, - userId, - seedUser, - clientsDictionary, - cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + var user = (await keycloak.GetUsersAsync(realm, username: seedUser.Username, cancellationToken: cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)).SingleOrDefault(x => x.UserName == seedUser.Username); - await UpdateFederatedIdentities( - keycloak, - realm, - userId, - seedUser.FederatedIdentities ?? Enumerable.Empty(), - cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + if (user == null) + { + var result = await keycloak.RealmPartialImportAsync(realm, CreatePartialImportUser(seedUser), cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + if (result.Overwritten != 0 || result.Added != 1 || result.Skipped != 0) + { + throw new ConflictException($"PartialImport failed to add user id: {seedUser.Id}, userName: {seedUser.Username}"); + } + } + else + { + await UpdateUser( + keycloak, + realm, + user, + seedUser, + clientsDictionary, + cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + } } } - private static async Task CreateOrUpdateUserReturningId(KeycloakClient keycloak, string realm, UserModel seedUser, IEnumerable excludedUserAttributes, CancellationToken cancellationToken) + private static async Task UpdateUser(KeycloakClient keycloak, string realm, User user, UserModel seedUser, IReadOnlyDictionary clientsDictionary, CancellationToken cancellationToken) { - var user = (await keycloak.GetUsersAsync(realm, username: seedUser.Username, cancellationToken: cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)).SingleOrDefault(x => x.UserName == seedUser.Username); + if (user.Id == null) + throw new ConflictException($"user.Id must not be null: userName {seedUser.Username}"); - if (user == null) + if (!CompareUser(user, seedUser)) { - return await keycloak.CreateAndRetrieveUserIdAsync( + await keycloak.UpdateUserAsync( realm, - CreateUpdateUser(null, seedUser, Enumerable.Empty()), - cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None) ?? throw new KeycloakNoSuccessException($"failed to retrieve id of newly created user {seedUser.Username}"); - } - else - { - if (user.Id == null) - throw new ConflictException($"user.Id must not be null: userName {seedUser.Username}"); - if (!CompareUser(user, seedUser)) - { - await keycloak.UpdateUserAsync( - realm, - user.Id, - CreateUpdateUser(user, seedUser, excludedUserAttributes), - cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - } - return user.Id; + user.Id, + CreateUpdateUser(user, seedUser), + cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } + + await UpdateClientAndRealmRoles( + keycloak, + realm, + user.Id, + seedUser, + clientsDictionary, + cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + + await UpdateFederatedIdentities( + keycloak, + realm, + user.Id, + seedUser.FederatedIdentities ?? Enumerable.Empty(), + cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } private static async Task UpdateClientAndRealmRoles(KeycloakClient keycloak, string realm, string userId, UserModel seedUser, IReadOnlyDictionary clientsDictionary, CancellationToken cancellationToken) @@ -127,53 +128,45 @@ private static async Task UpdateUserRoles(Func>> getUserR var userRoles = await getUserRoles().ConfigureAwait(ConfigureAwaitOptions.None); var seedRoles = getSeedRoles(); - if (userRoles.ExceptBy(seedRoles, x => x.Name).IfAny( - delete => deleteRoles(delete), - out var deleteRolesTask)) - { - await deleteRolesTask.ConfigureAwait(ConfigureAwaitOptions.None); - } + await userRoles.ExceptBy(seedRoles, x => x.Name).IfAnyAwait( + delete => deleteRoles(delete)).ConfigureAwait(false); - if (seedRoles.IfAny( + await seedRoles.IfAnyAwait( async seed => { var allRoles = await getAllRoles().ConfigureAwait(ConfigureAwaitOptions.None); seed.Except(allRoles.Select(x => x.Name)).IfAny(nonexisting => throw new ConflictException($"roles {string.Join(",", nonexisting)} does not exist")); - if (seed.Except(userRoles.Select(x => x.Name)).IfAny( - add => addRoles(allRoles.IntersectBy(add, x => x.Name)), - out var addRolesTask)) - { - await addRolesTask.ConfigureAwait(ConfigureAwaitOptions.None); - } - }, - out var updateRolesTask)) - { - await updateRolesTask.ConfigureAwait(ConfigureAwaitOptions.None); - } + await seed.Except(userRoles.Select(x => x.Name)).IfAnyAwait( + add => addRoles(allRoles.IntersectBy(add, x => x.Name))).ConfigureAwait(false); + }).ConfigureAwait(false); } - private static User CreateUpdateUser(User? user, UserModel update, IEnumerable excludedUserAttributes) => new() + private static User CreateUpdateUser(User? user, UserModel update) => new() { - // Access, ClientConsents, Credentials, FederatedIdentities, FederationLink, Origin, Self are not in scope + // Roles, ClientConsents, Credentials, FederatedIdentities are not in scope Id = user?.Id, CreatedTimestamp = update.CreatedTimestamp, UserName = update.Username, Enabled = update.Enabled, Totp = update.Totp, EmailVerified = update.EmailVerified, - FirstName = user?.FirstName ?? update.FirstName, - LastName = user?.LastName ?? update.LastName, - Email = user?.Email ?? update.Email, + FirstName = update.FirstName, + LastName = update.LastName, + Email = update.Email, DisableableCredentialTypes = update.DisableableCredentialTypes, RequiredActions = update.RequiredActions, NotBefore = update.NotBefore, - Attributes = UpdateAttributes(user?.Attributes, update.Attributes?.ToDictionary(x => x.Key, x => x.Value), excludedUserAttributes), + Attributes = update.Attributes?.FilterNotNullValues().ToDictionary(), Groups = update.Groups, - ServiceAccountClientId = update.ServiceAccountClientId + ServiceAccountClientId = update.ServiceAccountClientId, + Access = CreateUpdateUserAccess(update.Access), + FederationLink = update.FederationLink, + Origin = update.Origin, + Self = update.Self }; private static bool CompareUser(User user, UserModel update) => - // Access, ClientConsents, Credentials, FederatedIdentities, FederationLink, Origin, Self are not in scope + // Roles, ClientConsents, Credentials, FederatedIdentities, are not in scope user.CreatedTimestamp == update.CreatedTimestamp && user.UserName == update.Username && user.Enabled == update.Enabled && @@ -185,9 +178,32 @@ private static bool CompareUser(User user, UserModel update) => user.DisableableCredentialTypes.NullOrContentEqual(update.DisableableCredentialTypes) && user.RequiredActions.NullOrContentEqual(update.RequiredActions) && user.NotBefore == update.NotBefore && - user.Attributes.NullOrContentEqual(update.Attributes) && + user.Attributes.NullOrContentEqual(update.Attributes?.FilterNotNullValues()) && user.Groups.NullOrContentEqual(update.Groups) && - user.ServiceAccountClientId == update.ServiceAccountClientId; + user.ServiceAccountClientId == update.ServiceAccountClientId && + CompareUserAccess(user.Access, update.Access) && + user.FederationLink == update.FederationLink && + user.Origin == update.Origin && + user.Self == update.Self; + + private static UserAccess? CreateUpdateUserAccess(UserAccessModel? update) => + update == null ? null : new() + { + ManageGroupMembership = update.ManageGroupMembership, + View = update.View, + MapRoles = update.MapRoles, + Impersonate = update.Impersonate, + Manage = update.Manage + }; + + private static bool CompareUserAccess(UserAccess? userAccess, UserAccessModel? update) => + userAccess == null && update == null || + userAccess != null && update != null && + userAccess.ManageGroupMembership == update.ManageGroupMembership && + userAccess.View == update.View && + userAccess.MapRoles == update.MapRoles && + userAccess.Impersonate == update.Impersonate && + userAccess.Manage == update.Manage; private static bool CompareFederatedIdentity(FederatedIdentity identity, FederatedIdentityModel update) => identity.IdentityProvider == update.IdentityProvider && @@ -262,17 +278,62 @@ await keycloak.AddUserSocialLoginProviderAsync( } } - private static IDictionary>? UpdateAttributes(IDictionary>? existingAttributes, IDictionary>? updatedDictionary, IEnumerable excludedUserAttributes) - { - if (existingAttributes is null) - return updatedDictionary; + private static Credentials CreateUpdateCredentials(CredentialsModel update) => + new() + { + Algorithm = update.Algorithm, + Config = update.Config?.FilterNotNullValues().ToDictionary(), + Counter = update.Counter, + CreatedDate = update.CreatedDate, + Device = update.Device, + Digits = update.Digits, + HashIterations = update.HashIterations, + Period = update.Period, + Salt = update.Salt, + Temporary = update.Temporary, + Type = update.Type, + Value = update.Value, + }; - var attributesToKeep = existingAttributes.Where(x => excludedUserAttributes.Contains(x.Key)); - return updatedDictionary is null - ? attributesToKeep.ToDictionary(x => x.Key, x => x.Value) - : updatedDictionary - .Where(x => !excludedUserAttributes.Contains(x.Key)) - .Concat(attributesToKeep) - .ToDictionary(x => x.Key, x => x.Value); - } + private static FederatedIdentity CreateUpdateFederatedIdentity(FederatedIdentityModel update) => + new() + { + IdentityProvider = update.IdentityProvider, + UserId = update.UserId, + UserName = update.UserName + }; + + private static PartialImport CreatePartialImportUser(UserModel update) => + new() + { + IfResourceExists = "FAIL", + Users = [ + new() + { + // ClientConsents are not in scope + Id = update.Id, + CreatedTimestamp = update.CreatedTimestamp, + UserName = update.Username, + Enabled = update.Enabled, + Totp = update.Totp, + EmailVerified = update.EmailVerified, + FirstName = update.FirstName, + LastName = update.LastName, + Email = update.Email, + DisableableCredentialTypes = update.DisableableCredentialTypes, + RequiredActions = update.RequiredActions, + NotBefore = update.NotBefore, + Access = CreateUpdateUserAccess(update.Access), + Attributes = update.Attributes?.FilterNotNullValues().ToDictionary(), + ClientRoles = update.ClientRoles?.FilterNotNullValues().ToDictionary(), + Credentials = update.Credentials?.Select(CreateUpdateCredentials), + FederatedIdentities = update.FederatedIdentities?.Select(CreateUpdateFederatedIdentity), + FederationLink = update.FederationLink, + Groups = update.Groups, + Origin = update.Origin, + RealmRoles = update.RealmRoles, + Self = update.Self, + ServiceAccountClientId = update.ServiceAccountClientId + }] + }; } diff --git a/src/keycloak/Keycloak.Seeding/Keycloak.Seeding.csproj b/src/keycloak/Keycloak.Seeding/Keycloak.Seeding.csproj index 80c2cea6cd..635f1a93bd 100644 --- a/src/keycloak/Keycloak.Seeding/Keycloak.Seeding.csproj +++ b/src/keycloak/Keycloak.Seeding/Keycloak.Seeding.csproj @@ -38,6 +38,7 @@ + @@ -45,6 +46,7 @@ + diff --git a/src/keycloak/Keycloak.Seeding/Models/KeycloakRealm.cs b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealm.cs index d221272989..1d2fbc0309 100644 --- a/src/keycloak/Keycloak.Seeding/Models/KeycloakRealm.cs +++ b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealm.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2023 BMW Group AG * Copyright (c) 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -105,7 +104,7 @@ public class KeycloakRealm public IEnumerable? WebAuthnPolicyPasswordlessAcceptableAaguids { get; set; } public IEnumerable? Users { get; set; } public IEnumerable? ScopeMappings { get; set; } - public IReadOnlyDictionary>? ClientScopeMappings { get; set; } + public IReadOnlyDictionary?>? ClientScopeMappings { get; set; } public IEnumerable? Clients { get; set; } public IEnumerable? ClientScopes { get; set; } public IEnumerable? DefaultDefaultClientScopes { get; set; } @@ -123,7 +122,7 @@ public class KeycloakRealm public bool? AdminEventsDetailsEnabled { get; set; } public IEnumerable? IdentityProviders { get; set; } public IEnumerable? IdentityProviderMappers { get; set; } - public IReadOnlyDictionary>? Components { get; set; } + public IReadOnlyDictionary?>? Components { get; set; } public bool? InternationalizationEnabled { get; set; } public IEnumerable? SupportedLocales { get; set; } public string? DefaultLocale { get; set; } @@ -136,7 +135,7 @@ public class KeycloakRealm public string? ResetCredentialsFlow { get; set; } public string? ClientAuthenticationFlow { get; set; } public string? DockerAuthenticationFlow { get; set; } - public IReadOnlyDictionary? Attributes { get; set; } + public IReadOnlyDictionary? Attributes { get; set; } public string? KeycloakVersion { get; set; } public bool? UserManagedAccessAllowed { get; set; } public ClientProfilesModel? ClientProfiles { get; set; } @@ -145,12 +144,12 @@ public class KeycloakRealm public record RolesModel( IEnumerable? Realm, - IReadOnlyDictionary>? Client + IReadOnlyDictionary?>? Client ); public record CompositeRolesModel( IEnumerable? Realm, - IReadOnlyDictionary>? Client + IReadOnlyDictionary?>? Client ); public record RoleModel( @@ -160,7 +159,7 @@ public record RoleModel( bool? Composite, bool? ClientRole, string? ContainerId, - IReadOnlyDictionary>? Attributes, + IReadOnlyDictionary?>? Attributes, CompositeRolesModel? Composites ); @@ -174,16 +173,52 @@ public record UserModel( string? FirstName, string? LastName, string? Email, - IReadOnlyDictionary>? Attributes, - IEnumerable? Credentials, + IReadOnlyDictionary?>? Attributes, + IEnumerable? Credentials, IEnumerable? DisableableCredentialTypes, IEnumerable? RequiredActions, IEnumerable? FederatedIdentities, IEnumerable? RealmRoles, - IReadOnlyDictionary>? ClientRoles, + IReadOnlyDictionary?>? ClientRoles, int? NotBefore, IEnumerable? Groups, - string? ServiceAccountClientId + string? ServiceAccountClientId, + UserAccessModel? Access, + IEnumerable? ClientConsents, + string? FederationLink, + string? Origin, + string? Self +); + +public record UserAccessModel( + bool? ManageGroupMembership, + bool? View, + bool? MapRoles, + bool? Impersonate, + bool? Manage +); + +public record UserConsentModel( + string? ClientId, + IEnumerable? GrantedClientScopes, + long? CreatedDate, + long? LastUpdatedDate +); + +public record CredentialsModel( + string? Algorithm, + IReadOnlyDictionary? Config, + int? Counter, + long? CreatedDate, + string? Device, + int? Digits, + int? HashIterations, + string? HashSaltedValue, + int? Period, + string? Salt, + bool? Temporary, + string? Type, + string? Value ); public record FederatedIdentityModel( @@ -196,9 +231,9 @@ public record GroupModel( string? Id, string? Name, string? Path, - IReadOnlyDictionary>? Attributes, + IReadOnlyDictionary?>? Attributes, IEnumerable? RealmRoles, - IReadOnlyDictionary>? ClientRoles, + IReadOnlyDictionary?>? ClientRoles, IEnumerable? SubGroups ); @@ -234,8 +269,8 @@ public record ClientModel( bool? PublicClient, bool? FrontchannelLogout, string? Protocol, - IReadOnlyDictionary? Attributes, - IReadOnlyDictionary? AuthenticationFlowBindingOverrides, + IReadOnlyDictionary? Attributes, + IReadOnlyDictionary? AuthenticationFlowBindingOverrides, bool? FullScopeAllowed, int? NodeReRegistrationTimeout, IEnumerable? DefaultClientScopes, @@ -260,14 +295,59 @@ public record ProtocolMapperModel( string? Protocol, string? ProtocolMapper, bool? ConsentRequired, - IReadOnlyDictionary? Config + IReadOnlyDictionary? Config +); + +public record ProtocolMapperConfigModel( + [property: JsonPropertyName("single")] + string? Single, + [property: JsonPropertyName("attribute.nameformat")] + string? AttributeNameFormat, + [property: JsonPropertyName("attribute.name")] + string? AttributeName, + [property: JsonPropertyName("userinfo.token.claim")] + string? UserInfoTokenClaim, + [property: JsonPropertyName("user.attribute")] + string? UserAttribute, + [property: JsonPropertyName("id.token.claim")] + string? IdTokenClaim, + [property: JsonPropertyName("access.token.claim")] + string? AccessTokenClaim, + [property: JsonPropertyName("introspection.token.claim")] + string? IntrospectionTokenClaim, + [property: JsonPropertyName("lightweight.claim")] + string? LightweightClaim, + [property: JsonPropertyName("claim.name")] + string? ClaimName, + [property: JsonPropertyName("jsonType.label")] + string? JsonTypelabel, + [property: JsonPropertyName("user.attribute.formatted")] + string? UserAttributeFormatted, + [property: JsonPropertyName("user.attribute.country")] + string? UserAttributeCountry, + [property: JsonPropertyName("user.attribute.postal_code")] + string? UserAttributePostalCode, + [property: JsonPropertyName("user.attribute.street")] + string? UserAttributeStreet, + [property: JsonPropertyName("user.attribute.region")] + string? UserAttributeRegion, + [property: JsonPropertyName("user.attribute.locality")] + string? UserAttributeLocality, + [property: JsonPropertyName("included.client.audience")] + string? IncludedClientAudience, + [property: JsonPropertyName("included.custom.audience")] + string? IncludedCustomAudience, + [property: JsonPropertyName("multivalued")] + string? Multivalued, + [property: JsonPropertyName("user.session.note")] + string? UserSessionNote ); public record ClientScopeModel( string? Id, string? Name, string? Protocol, - IReadOnlyDictionary? Attributes, + IReadOnlyDictionary? Attributes, IEnumerable? ProtocolMappers, string? Description ); @@ -359,7 +439,7 @@ public record IdentityProviderMapperModel( string? Name, string? IdentityProviderAlias, string? IdentityProviderMapper, - IReadOnlyDictionary? Config + IReadOnlyDictionary? Config ); public record ComponentModel( @@ -368,7 +448,7 @@ public record ComponentModel( string? ProviderId, string? SubType, object? SubComponents, - IReadOnlyDictionary>? Config + IReadOnlyDictionary?>? Config ); public record AuthenticationFlowModel( @@ -395,7 +475,7 @@ public record AuthenticationExecutionModel( public record AuthenticatorConfigModel( string? Id, string? Alias, - IReadOnlyDictionary? Config + IReadOnlyDictionary? Config ); public record RequiredActionModel( diff --git a/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmExtensions.cs b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmExtensions.cs new file mode 100644 index 0000000000..32227e4d91 --- /dev/null +++ b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmExtensions.cs @@ -0,0 +1,410 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using System.Diagnostics.CodeAnalysis; + +namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; + +public static class KeycloakRealmExtensions +{ + public static KeycloakRealm Merge(this KeycloakRealm left, KeycloakRealm right) => + new() + { + Id = right.Id ?? left.Id, + Realm = right.Realm ?? left.Realm, + DisplayName = right.DisplayName ?? left.DisplayName, + DisplayNameHtml = right.DisplayNameHtml ?? left.DisplayNameHtml, + NotBefore = right.NotBefore ?? left.NotBefore, + DefaultSignatureAlgorithm = right.DefaultSignatureAlgorithm ?? left.DefaultSignatureAlgorithm, + RevokeRefreshToken = right.RevokeRefreshToken ?? left.RevokeRefreshToken, + RefreshTokenMaxReuse = right.RefreshTokenMaxReuse ?? left.RefreshTokenMaxReuse, + AccessTokenLifespan = right.AccessTokenLifespan ?? left.AccessTokenLifespan, + AccessTokenLifespanForImplicitFlow = right.AccessTokenLifespanForImplicitFlow ?? left.AccessTokenLifespanForImplicitFlow, + SsoSessionIdleTimeout = right.SsoSessionIdleTimeout ?? left.SsoSessionIdleTimeout, + SsoSessionMaxLifespan = right.SsoSessionMaxLifespan ?? left.SsoSessionMaxLifespan, + SsoSessionIdleTimeoutRememberMe = right.SsoSessionIdleTimeoutRememberMe ?? left.SsoSessionIdleTimeoutRememberMe, + SsoSessionMaxLifespanRememberMe = right.SsoSessionMaxLifespanRememberMe ?? left.SsoSessionMaxLifespanRememberMe, + OfflineSessionIdleTimeout = right.OfflineSessionIdleTimeout ?? left.OfflineSessionIdleTimeout, + OfflineSessionMaxLifespanEnabled = right.OfflineSessionMaxLifespanEnabled ?? left.OfflineSessionMaxLifespanEnabled, + OfflineSessionMaxLifespan = right.OfflineSessionMaxLifespan ?? left.OfflineSessionMaxLifespan, + ClientSessionIdleTimeout = right.ClientSessionIdleTimeout ?? left.ClientSessionIdleTimeout, + ClientSessionMaxLifespan = right.ClientSessionMaxLifespan ?? left.ClientSessionMaxLifespan, + ClientOfflineSessionIdleTimeout = right.ClientOfflineSessionIdleTimeout ?? left.ClientOfflineSessionIdleTimeout, + ClientOfflineSessionMaxLifespan = right.ClientOfflineSessionMaxLifespan ?? left.ClientOfflineSessionMaxLifespan, + AccessCodeLifespan = right.AccessCodeLifespan ?? left.AccessCodeLifespan, + AccessCodeLifespanUserAction = right.AccessCodeLifespanUserAction ?? left.AccessCodeLifespanUserAction, + AccessCodeLifespanLogin = right.AccessCodeLifespanLogin ?? left.AccessCodeLifespanLogin, + ActionTokenGeneratedByAdminLifespan = right.ActionTokenGeneratedByAdminLifespan ?? left.ActionTokenGeneratedByAdminLifespan, + ActionTokenGeneratedByUserLifespan = right.ActionTokenGeneratedByUserLifespan ?? left.ActionTokenGeneratedByUserLifespan, + Oauth2DeviceCodeLifespan = right.Oauth2DeviceCodeLifespan ?? left.Oauth2DeviceCodeLifespan, + Oauth2DevicePollingInterval = right.Oauth2DevicePollingInterval ?? left.Oauth2DevicePollingInterval, + Enabled = right.Enabled ?? left.Enabled, + SslRequired = right.SslRequired ?? left.SslRequired, + RegistrationAllowed = right.RegistrationAllowed ?? left.RegistrationAllowed, + RegistrationEmailAsUsername = right.RegistrationEmailAsUsername ?? left.RegistrationEmailAsUsername, + RememberMe = right.RememberMe ?? left.RememberMe, + VerifyEmail = right.VerifyEmail ?? left.VerifyEmail, + LoginWithEmailAllowed = right.LoginWithEmailAllowed ?? left.LoginWithEmailAllowed, + DuplicateEmailsAllowed = right.DuplicateEmailsAllowed ?? left.DuplicateEmailsAllowed, + ResetPasswordAllowed = right.ResetPasswordAllowed ?? left.ResetPasswordAllowed, + EditUsernameAllowed = right.EditUsernameAllowed ?? left.EditUsernameAllowed, + BruteForceProtected = right.BruteForceProtected ?? left.BruteForceProtected, + PermanentLockout = right.PermanentLockout ?? left.PermanentLockout, + MaxFailureWaitSeconds = right.MaxFailureWaitSeconds ?? left.MaxFailureWaitSeconds, + MinimumQuickLoginWaitSeconds = right.MinimumQuickLoginWaitSeconds ?? left.MinimumQuickLoginWaitSeconds, + WaitIncrementSeconds = right.WaitIncrementSeconds ?? left.WaitIncrementSeconds, + QuickLoginCheckMilliSeconds = right.QuickLoginCheckMilliSeconds ?? left.QuickLoginCheckMilliSeconds, + MaxDeltaTimeSeconds = right.MaxDeltaTimeSeconds ?? left.MaxDeltaTimeSeconds, + FailureFactor = right.FailureFactor ?? left.FailureFactor, + Roles = Merge(left.Roles, right.Roles, MergeRoles), + Groups = Merge(left.Groups, right.Groups, x => x.Name, MergeGroup), + DefaultRole = Merge(left.DefaultRole, right.DefaultRole, MergeRole), + DefaultGroups = right.DefaultGroups ?? left.DefaultGroups, + RequiredCredentials = right.RequiredCredentials ?? left.RequiredCredentials, + OtpPolicyType = right.OtpPolicyType ?? left.OtpPolicyType, + OtpPolicyAlgorithm = right.OtpPolicyAlgorithm ?? left.OtpPolicyAlgorithm, + OtpPolicyInitialCounter = right.OtpPolicyInitialCounter ?? left.OtpPolicyInitialCounter, + OtpPolicyDigits = right.OtpPolicyDigits ?? left.OtpPolicyDigits, + OtpPolicyLookAheadWindow = right.OtpPolicyLookAheadWindow ?? left.OtpPolicyLookAheadWindow, + OtpPolicyPeriod = right.OtpPolicyPeriod ?? left.OtpPolicyPeriod, + OtpSupportedApplications = right.OtpSupportedApplications ?? left.OtpSupportedApplications, + PasswordPolicy = right.PasswordPolicy ?? left.PasswordPolicy, + WebAuthnPolicyRpEntityName = right.WebAuthnPolicyRpEntityName ?? left.WebAuthnPolicyRpEntityName, + WebAuthnPolicySignatureAlgorithms = right.WebAuthnPolicySignatureAlgorithms ?? left.WebAuthnPolicySignatureAlgorithms, + WebAuthnPolicyRpId = right.WebAuthnPolicyRpId ?? left.WebAuthnPolicyRpId, + WebAuthnPolicyAttestationConveyancePreference = right.WebAuthnPolicyAttestationConveyancePreference ?? left.WebAuthnPolicyAttestationConveyancePreference, + WebAuthnPolicyAuthenticatorAttachment = right.WebAuthnPolicyAuthenticatorAttachment ?? left.WebAuthnPolicyAuthenticatorAttachment, + WebAuthnPolicyRequireResidentKey = right.WebAuthnPolicyRequireResidentKey ?? left.WebAuthnPolicyRequireResidentKey, + WebAuthnPolicyUserVerificationRequirement = right.WebAuthnPolicyUserVerificationRequirement ?? left.WebAuthnPolicyUserVerificationRequirement, + WebAuthnPolicyCreateTimeout = right.WebAuthnPolicyCreateTimeout ?? left.WebAuthnPolicyCreateTimeout, + WebAuthnPolicyAvoidSameAuthenticatorRegister = right.WebAuthnPolicyAvoidSameAuthenticatorRegister ?? left.WebAuthnPolicyAvoidSameAuthenticatorRegister, + WebAuthnPolicyAcceptableAaguids = right.WebAuthnPolicyAcceptableAaguids ?? left.WebAuthnPolicyAcceptableAaguids, + WebAuthnPolicyPasswordlessRpEntityName = right.WebAuthnPolicyPasswordlessRpEntityName ?? left.WebAuthnPolicyPasswordlessRpEntityName, + WebAuthnPolicyPasswordlessSignatureAlgorithms = right.WebAuthnPolicyPasswordlessSignatureAlgorithms ?? left.WebAuthnPolicyPasswordlessSignatureAlgorithms, + WebAuthnPolicyPasswordlessRpId = right.WebAuthnPolicyPasswordlessRpId ?? left.WebAuthnPolicyPasswordlessRpId, + WebAuthnPolicyPasswordlessAttestationConveyancePreference = right.WebAuthnPolicyPasswordlessAttestationConveyancePreference ?? left.WebAuthnPolicyPasswordlessAttestationConveyancePreference, + WebAuthnPolicyPasswordlessAuthenticatorAttachment = right.WebAuthnPolicyPasswordlessAuthenticatorAttachment ?? left.WebAuthnPolicyPasswordlessAuthenticatorAttachment, + WebAuthnPolicyPasswordlessRequireResidentKey = right.WebAuthnPolicyPasswordlessRequireResidentKey ?? left.WebAuthnPolicyPasswordlessRequireResidentKey, + WebAuthnPolicyPasswordlessUserVerificationRequirement = right.WebAuthnPolicyPasswordlessUserVerificationRequirement ?? left.WebAuthnPolicyPasswordlessUserVerificationRequirement, + WebAuthnPolicyPasswordlessCreateTimeout = right.WebAuthnPolicyPasswordlessCreateTimeout ?? left.WebAuthnPolicyPasswordlessCreateTimeout, + WebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister = right.WebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister ?? left.WebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister, + WebAuthnPolicyPasswordlessAcceptableAaguids = right.WebAuthnPolicyPasswordlessAcceptableAaguids ?? left.WebAuthnPolicyPasswordlessAcceptableAaguids, + Users = Merge(left.Users, right.Users, x => x.Username, MergeUser), + ScopeMappings = Merge(left.ScopeMappings, right.ScopeMappings, x => x.ClientScope, (left, right) => right), + ClientScopeMappings = Merge(left.ClientScopeMappings, right.ClientScopeMappings, x => x.Key, (left, right) => right)?.ToDictionary(), + Clients = Merge(left.Clients, right.Clients, x => x.ClientId, MergeClient), + ClientScopes = Merge(left.ClientScopes, right.ClientScopes, x => x.Name, MergeClientScope), + DefaultDefaultClientScopes = right.DefaultDefaultClientScopes ?? left.DefaultDefaultClientScopes, + DefaultOptionalClientScopes = right.DefaultOptionalClientScopes ?? left.DefaultOptionalClientScopes, + BrowserSecurityHeaders = Merge(left.BrowserSecurityHeaders, right.BrowserSecurityHeaders, MergeBrowserSecurityHeaders), + SmtpServer = Merge(left.SmtpServer, right.SmtpServer, MergeSmtpServer), + LoginTheme = right.LoginTheme ?? left.LoginTheme, + AccountTheme = right.AccountTheme ?? left.AccountTheme, + AdminTheme = right.AdminTheme ?? left.AdminTheme, + EmailTheme = right.EmailTheme ?? left.EmailTheme, + EventsEnabled = right.EventsEnabled ?? left.EventsEnabled, + EventsListeners = right.EventsListeners ?? left.EventsListeners, + EnabledEventTypes = right.EnabledEventTypes ?? left.EnabledEventTypes, + AdminEventsEnabled = right.AdminEventsEnabled ?? left.AdminEventsEnabled, + AdminEventsDetailsEnabled = right.AdminEventsDetailsEnabled ?? left.AdminEventsDetailsEnabled, + IdentityProviders = Merge(left.IdentityProviders, right.IdentityProviders, x => x.Alias, MergeIdentityProvider), + IdentityProviderMappers = Merge(left.IdentityProviderMappers, right.IdentityProviderMappers, x => x.Name, MergeIdentityProviderMapper), + Components = Merge(left.Components, right.Components, x => x.Key, (x, y) => KeyValuePair.Create(x.Key, Merge(x.Value, y.Value, z => z.Name, MergeComponent)))?.ToDictionary(), + InternationalizationEnabled = right.InternationalizationEnabled ?? left.InternationalizationEnabled, + SupportedLocales = right.SupportedLocales ?? left.SupportedLocales, + DefaultLocale = right.DefaultLocale ?? left.DefaultLocale, + AuthenticationFlows = Merge(left.AuthenticationFlows, right.AuthenticationFlows, x => x.Alias, MergeAuthenticationFlow), + AuthenticatorConfig = Merge(left.AuthenticatorConfig, right.AuthenticatorConfig, x => x.Alias, MergeAuthenticatorConfig), + RequiredActions = Merge(left.RequiredActions, right.RequiredActions, x => x.Alias, MergeRequiredAction), + BrowserFlow = right.BrowserFlow ?? left.BrowserFlow, + RegistrationFlow = right.RegistrationFlow ?? left.RegistrationFlow, + DirectGrantFlow = right.DirectGrantFlow ?? left.DirectGrantFlow, + ResetCredentialsFlow = right.ResetCredentialsFlow ?? left.ResetCredentialsFlow, + ClientAuthenticationFlow = right.ClientAuthenticationFlow ?? left.ClientAuthenticationFlow, + DockerAuthenticationFlow = right.DockerAuthenticationFlow ?? left.DockerAuthenticationFlow, + Attributes = Merge(left.Attributes, right.Attributes, x => x.Key, (left, right) => right)?.ToDictionary(), + KeycloakVersion = right.KeycloakVersion ?? left.KeycloakVersion, + UserManagedAccessAllowed = right.UserManagedAccessAllowed ?? left.UserManagedAccessAllowed, + ClientProfiles = Merge(left.ClientProfiles, right.ClientProfiles, MergeClientProfiles), + ClientPolicies = Merge(left.ClientPolicies, right.ClientPolicies, MergeClientPolicies) + }; + + [return: NotNullIfNotNull(nameof(left))] + [return: NotNullIfNotNull(nameof(right))] + private static TModel? Merge(TModel? left, TModel? right, Func merge) => + (left, right) switch + { + (null, null) => default, + (null, _) => right, + (_, null) => left, + (_, _) => merge(left, right) + }; + + [return: NotNullIfNotNull(nameof(left))] + [return: NotNullIfNotNull(nameof(right))] + private static IEnumerable? Merge(IEnumerable? left, IEnumerable? right, Func select, Func merge) => + Merge( + left, + right, + (left, right) => left.Join(right, select, select, merge) + .Concat(left.ExceptBy(right.Select(select), select)) + .Concat(right.ExceptBy(left.Select(select), select))); + + private static RolesModel MergeRoles(RolesModel left, RolesModel right) => + new(Merge(left.Realm, right.Realm, x => x.Name, MergeRole), + Merge(left.Client, right.Client, x => x.Key, (left, right) => KeyValuePair.Create(left.Key, Merge(left.Value, right.Value, x => x.Name, MergeRole)))?.ToDictionary()); + + private static RoleModel MergeRole(RoleModel left, RoleModel right) => + new(right.Id ?? left.Id, + right.Name ?? left.Name, + right.Description ?? left.Description, + right.Composite ?? left.Composite, + right.ClientRole ?? left.ClientRole, + right.ContainerId ?? left.ContainerId, + Merge(left.Attributes, right.Attributes, x => x.Key, (left, right) => right)?.ToDictionary(), + Merge(left.Composites, right.Composites, MergeCompositeRoles)); + + private static CompositeRolesModel MergeCompositeRoles(CompositeRolesModel left, CompositeRolesModel right) => + new(right.Realm ?? left.Realm, + Merge(left.Client, right.Client, x => x.Key, (left, right) => right)?.ToDictionary()); + + private static GroupModel MergeGroup(GroupModel left, GroupModel right) => + new(right.Id ?? left.Id, + right.Name ?? left.Name, + right.Path ?? left.Path, + Merge(left.Attributes, right.Attributes, x => x.Key, (left, right) => right)?.ToDictionary(), + right.RealmRoles ?? left.RealmRoles, + Merge(left.ClientRoles, right.ClientRoles, x => x.Key, (left, right) => right)?.ToDictionary(), + right.SubGroups ?? left.SubGroups); + + private static UserModel MergeUser(UserModel left, UserModel right) => + new(right.Id ?? left.Id, + right.CreatedTimestamp ?? left.CreatedTimestamp, + right.Username ?? left.Username, + right.Enabled ?? left.Enabled, + right.Totp ?? left.Totp, + right.EmailVerified ?? left.EmailVerified, + right.FirstName ?? left.FirstName, + right.LastName ?? left.LastName, + right.Email ?? left.Email, + Merge(left.Attributes, right.Attributes, x => x.Key, (left, right) => right)?.ToDictionary(), + right.Credentials ?? left.Credentials, + right.DisableableCredentialTypes ?? left.DisableableCredentialTypes, + right.RequiredActions ?? left.RequiredActions, + Merge(left.FederatedIdentities, right.FederatedIdentities, x => x.IdentityProvider, MergeFederatedIdentity), + right.RealmRoles ?? left.RealmRoles, + Merge(left.ClientRoles, right.ClientRoles, x => x.Key, (left, right) => right)?.ToDictionary(), + right.NotBefore ?? left.NotBefore, + right.Groups ?? left.Groups, + right.ServiceAccountClientId ?? left.ServiceAccountClientId, + right.Access ?? left.Access, + right.ClientConsents ?? left.ClientConsents, + right.FederationLink ?? left.FederationLink, + right.Origin ?? left.Origin, + right.Self ?? left.Self); + + private static FederatedIdentityModel MergeFederatedIdentity(FederatedIdentityModel left, FederatedIdentityModel right) => + new(right.IdentityProvider ?? left.IdentityProvider, + right.UserId ?? left.UserId, + right.UserName ?? left.UserName); + + private static ClientModel MergeClient(ClientModel left, ClientModel right) => + new(right.Id ?? left.Id, + right.ClientId ?? left.ClientId, + right.Name ?? left.Name, + right.RootUrl ?? left.RootUrl, + right.BaseUrl ?? left.BaseUrl, + right.SurrogateAuthRequired ?? left.SurrogateAuthRequired, + right.Enabled ?? left.Enabled, + right.AlwaysDisplayInConsole ?? left.AlwaysDisplayInConsole, + right.ClientAuthenticatorType ?? left.ClientAuthenticatorType, + right.RedirectUris ?? left.RedirectUris, + right.WebOrigins ?? left.WebOrigins, + right.NotBefore ?? left.NotBefore, + right.BearerOnly ?? left.BearerOnly, + right.ConsentRequired ?? left.ConsentRequired, + right.StandardFlowEnabled ?? left.StandardFlowEnabled, + right.ImplicitFlowEnabled ?? left.ImplicitFlowEnabled, + right.DirectAccessGrantsEnabled ?? left.DirectAccessGrantsEnabled, + right.ServiceAccountsEnabled ?? left.ServiceAccountsEnabled, + right.PublicClient ?? left.PublicClient, + right.FrontchannelLogout ?? left.FrontchannelLogout, + right.Protocol ?? left.Protocol, + Merge(left.Attributes, right.Attributes, x => x.Key, (left, right) => right)?.ToDictionary(), + Merge(left.AuthenticationFlowBindingOverrides, right.AuthenticationFlowBindingOverrides, x => x.Key, (left, right) => right)?.ToDictionary(), + right.FullScopeAllowed ?? left.FullScopeAllowed, + right.NodeReRegistrationTimeout ?? left.NodeReRegistrationTimeout, + right.DefaultClientScopes ?? left.DefaultClientScopes, + right.OptionalClientScopes ?? left.OptionalClientScopes, + Merge(left.ProtocolMappers, right.ProtocolMappers, x => x.Name, MergeProtocolMapper), + Merge(left.Access, right.Access, MergeClientAccess), + right.Secret ?? left.Secret, + right.AdminUrl ?? left.AdminUrl, + right.Description ?? left.Description, + right.AuthorizationServicesEnabled ?? left.AuthorizationServicesEnabled); + + private static ClientAccessModel MergeClientAccess(ClientAccessModel left, ClientAccessModel right) => + new(right.Configure ?? left.Configure, + right.Manage ?? left.Manage, + right.View ?? left.View); + + private static ClientScopeModel MergeClientScope(ClientScopeModel left, ClientScopeModel right) => + new(right.Id ?? left.Id, + right.Name ?? left.Name, + right.Protocol ?? left.Protocol, + Merge(left.Attributes, right.Attributes, x => x.Key, (left, right) => right)?.ToDictionary(), + Merge(left.ProtocolMappers, right.ProtocolMappers, x => x.Name, MergeProtocolMapper), + right.Description ?? left.Description); + + private static ProtocolMapperModel MergeProtocolMapper(ProtocolMapperModel left, ProtocolMapperModel right) => + new(right.Id ?? left.Id, + right.Name ?? left.Name, + right.Protocol ?? left.Protocol, + right.ProtocolMapper ?? left.ProtocolMapper, + right.ConsentRequired ?? left.ConsentRequired, + Merge(left.Config, right.Config, x => x.Key, (left, right) => right)?.ToDictionary()); + + private static BrowserSecurityHeadersModel MergeBrowserSecurityHeaders(BrowserSecurityHeadersModel left, BrowserSecurityHeadersModel right) => + new(right.ContentSecurityPolicyReportOnly ?? left.ContentSecurityPolicyReportOnly, + right.XContentTypeOptions ?? left.XContentTypeOptions, + right.XRobotsTag ?? left.XRobotsTag, + right.XFrameOptions ?? left.XFrameOptions, + right.ContentSecurityPolicy ?? left.ContentSecurityPolicy, + right.XXSSProtection ?? left.XXSSProtection, + right.StrictTransportSecurity ?? left.StrictTransportSecurity); + + private static SmtpServerModel MergeSmtpServer(SmtpServerModel left, SmtpServerModel right) => + new(right.Password ?? left.Password, + right.Starttls ?? left.Starttls, + right.Auth ?? left.Auth, + right.Port ?? left.Port, + right.Host ?? left.Host, + right.ReplyToDisplayName ?? left.ReplyToDisplayName, + right.ReplyTo ?? left.ReplyTo, + right.FromDisplayName ?? left.FromDisplayName, + right.From ?? left.From, + right.EnvelopeFrom ?? left.EnvelopeFrom, + right.Ssl ?? left.Ssl, + right.User ?? left.User); + + private static IdentityProviderModel MergeIdentityProvider(IdentityProviderModel left, IdentityProviderModel right) => + new(right.Alias ?? left.Alias, + right.DisplayName ?? left.DisplayName, + right.InternalId ?? left.InternalId, + right.ProviderId ?? left.ProviderId, + right.Enabled ?? left.Enabled, + right.UpdateProfileFirstLoginMode ?? left.UpdateProfileFirstLoginMode, + right.TrustEmail ?? left.TrustEmail, + right.StoreToken ?? left.StoreToken, + right.AddReadTokenRoleOnCreate ?? left.AddReadTokenRoleOnCreate, + right.AuthenticateByDefault ?? left.AuthenticateByDefault, + right.LinkOnly ?? left.LinkOnly, + right.FirstBrokerLoginFlowAlias ?? left.FirstBrokerLoginFlowAlias, + right.PostBrokerLoginFlowAlias ?? left.PostBrokerLoginFlowAlias, + Merge(left.Config, right.Config, MergeIdentityProviderConfig)); + + private static IdentityProviderConfigModel MergeIdentityProviderConfig(IdentityProviderConfigModel left, IdentityProviderConfigModel right) => + new(right.HideOnLoginPage ?? left.HideOnLoginPage, + right.ClientSecret ?? left.ClientSecret, + right.DisableUserInfo ?? left.DisableUserInfo, + right.ValidateSignature ?? left.ValidateSignature, + right.ClientId ?? left.ClientId, + right.TokenUrl ?? left.TokenUrl, + right.AuthorizationUrl ?? left.AuthorizationUrl, + right.ClientAuthMethod ?? left.ClientAuthMethod, + right.JwksUrl ?? left.JwksUrl, + right.LogoutUrl ?? left.LogoutUrl, + right.ClientAssertionSigningAlg ?? left.ClientAssertionSigningAlg, + right.SyncMode ?? left.SyncMode, + right.UseJwksUrl ?? left.UseJwksUrl, + right.UserInfoUrl ?? left.UserInfoUrl, + right.Issuer ?? left.Issuer, + right.NameIDPolicyFormat ?? left.NameIDPolicyFormat, + right.PrincipalType ?? left.PrincipalType, + right.SignatureAlgorithm ?? left.SignatureAlgorithm, + right.XmlSigKeyInfoKeyNameTransformer ?? left.XmlSigKeyInfoKeyNameTransformer, + right.AllowCreate ?? left.AllowCreate, + right.EntityId ?? left.EntityId, + right.AuthnContextComparisonType ?? left.AuthnContextComparisonType, + right.BackchannelSupported ?? left.BackchannelSupported, + right.PostBindingResponse ?? left.PostBindingResponse, + right.PostBindingAuthnRequest ?? left.PostBindingAuthnRequest, + right.PostBindingLogout ?? left.PostBindingLogout, + right.WantAuthnRequestsSigned ?? left.WantAuthnRequestsSigned, + right.WantAssertionsSigned ?? left.WantAssertionsSigned, + right.WantAssertionsEncrypted ?? left.WantAssertionsEncrypted, + right.ForceAuthn ?? left.ForceAuthn, + right.SignSpMetadata ?? left.SignSpMetadata, + right.LoginHint ?? left.LoginHint, + right.SingleSignOnServiceUrl ?? left.SingleSignOnServiceUrl, + right.AllowedClockSkew ?? left.AllowedClockSkew, + right.AttributeConsumingServiceIndex ?? left.AttributeConsumingServiceIndex); + + private static IdentityProviderMapperModel MergeIdentityProviderMapper(IdentityProviderMapperModel left, IdentityProviderMapperModel right) => + new(right.Id ?? left.Id, + right.Name ?? left.Name, + right.IdentityProviderAlias ?? left.IdentityProviderAlias, + right.IdentityProviderMapper ?? left.IdentityProviderMapper, + Merge(left.Config, right.Config, x => x.Key, (left, right) => right)?.ToDictionary()); + + private static ComponentModel MergeComponent(ComponentModel left, ComponentModel right) => + new(right.Id ?? left.Id, + right.Name ?? left.Name, + right.ProviderId ?? left.ProviderId, + right.SubType ?? left.SubType, + right.SubComponents ?? left.SubComponents, + Merge(left.Config, right.Config, x => x.Key, (left, right) => right)?.ToDictionary()); + + private static AuthenticationFlowModel MergeAuthenticationFlow(AuthenticationFlowModel left, AuthenticationFlowModel right) => + new(right.Id ?? left.Id, + right.Alias ?? left.Alias, + right.Description ?? left.Description, + right.ProviderId ?? left.ProviderId, + right.TopLevel ?? left.TopLevel, + right.BuiltIn ?? left.BuiltIn, + Merge(left.AuthenticationExecutions, right.AuthenticationExecutions, x => x.Authenticator, MergeAuthenticationExecution)); + + private static AuthenticationExecutionModel MergeAuthenticationExecution(AuthenticationExecutionModel left, AuthenticationExecutionModel right) => + new(right.Authenticator ?? left.Authenticator, + right.AuthenticatorFlow ?? left.AuthenticatorFlow, + right.Requirement ?? left.Requirement, + right.Priority ?? left.Priority, + right.UserSetupAllowed ?? left.UserSetupAllowed, + right.AutheticatorFlow ?? left.AutheticatorFlow, + right.FlowAlias ?? left.FlowAlias, + right.AuthenticatorConfig ?? left.AuthenticatorConfig); + + private static AuthenticatorConfigModel MergeAuthenticatorConfig(AuthenticatorConfigModel left, AuthenticatorConfigModel right) => + new(right.Id ?? left.Id, + right.Alias ?? left.Alias, + Merge(left.Config, right.Config, x => x.Key, (left, right) => right)?.ToDictionary()); + + private static RequiredActionModel MergeRequiredAction(RequiredActionModel left, RequiredActionModel right) => + new(right.Alias ?? left.Alias, + right.Name ?? left.Name, + right.ProviderId ?? left.ProviderId, + right.Enabled ?? left.Enabled, + right.DefaultAction ?? left.DefaultAction, + right.Priority ?? left.Priority, + right.Config ?? left.Config); + + private static ClientProfilesModel MergeClientProfiles(ClientProfilesModel left, ClientProfilesModel right) => right; + + private static ClientPoliciesModel MergeClientPolicies(ClientPoliciesModel left, ClientPoliciesModel right) => right; +} diff --git a/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettings.cs b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettings.cs new file mode 100644 index 0000000000..8c1a8592ee --- /dev/null +++ b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettings.cs @@ -0,0 +1,664 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Org.Eclipse.TractusX.Portal.Backend.Framework.Models.Validation; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; + +public class KeycloakRealmSettings +{ + [Required] + public string Realm { get; set; } = null!; + [Required] + public string InstanceName { get; set; } = null!; + [Required] + [DistinctValues] + public IEnumerable DataPaths { get; set; } = null!; + public string? Id { get; set; } + public string? DisplayName { get; set; } + public string? DisplayNameHtml { get; set; } + public int? NotBefore { get; set; } + public string? DefaultSignatureAlgorithm { get; set; } + public bool? RevokeRefreshToken { get; set; } + public int? RefreshTokenMaxReuse { get; set; } + public int? AccessTokenLifespan { get; set; } + public int? AccessTokenLifespanForImplicitFlow { get; set; } + public int? SsoSessionIdleTimeout { get; set; } + public int? SsoSessionMaxLifespan { get; set; } + public int? SsoSessionIdleTimeoutRememberMe { get; set; } + public int? SsoSessionMaxLifespanRememberMe { get; set; } + public int? OfflineSessionIdleTimeout { get; set; } + public bool? OfflineSessionMaxLifespanEnabled { get; set; } + public int? OfflineSessionMaxLifespan { get; set; } + public int? ClientSessionIdleTimeout { get; set; } + public int? ClientSessionMaxLifespan { get; set; } + public int? ClientOfflineSessionIdleTimeout { get; set; } + public int? ClientOfflineSessionMaxLifespan { get; set; } + public int? AccessCodeLifespan { get; set; } + public int? AccessCodeLifespanUserAction { get; set; } + public int? AccessCodeLifespanLogin { get; set; } + public int? ActionTokenGeneratedByAdminLifespan { get; set; } + public int? ActionTokenGeneratedByUserLifespan { get; set; } + public int? Oauth2DeviceCodeLifespan { get; set; } + public int? Oauth2DevicePollingInterval { get; set; } + public bool? Enabled { get; set; } + public string? SslRequired { get; set; } + public bool? RegistrationAllowed { get; set; } + public bool? RegistrationEmailAsUsername { get; set; } + public bool? RememberMe { get; set; } + public bool? VerifyEmail { get; set; } + public bool? LoginWithEmailAllowed { get; set; } + public bool? DuplicateEmailsAllowed { get; set; } + public bool? ResetPasswordAllowed { get; set; } + public bool? EditUsernameAllowed { get; set; } + public bool? BruteForceProtected { get; set; } + public bool? PermanentLockout { get; set; } + public int? MaxFailureWaitSeconds { get; set; } + public int? MinimumQuickLoginWaitSeconds { get; set; } + public int? WaitIncrementSeconds { get; set; } + public int? QuickLoginCheckMilliSeconds { get; set; } + public int? MaxDeltaTimeSeconds { get; set; } + public int? FailureFactor { get; set; } + public RolesSettings? Roles { get; set; } + [DistinctValues("x => x.Name")] + public IEnumerable? Groups { get; set; } + public RoleSettings? DefaultRole { get; set; } + [DistinctValues] + public IEnumerable? DefaultGroups { get; set; } + [DistinctValues] + public IEnumerable? RequiredCredentials { get; set; } + public string? OtpPolicyType { get; set; } + public string? OtpPolicyAlgorithm { get; set; } + public int? OtpPolicyInitialCounter { get; set; } + public int? OtpPolicyDigits { get; set; } + public int? OtpPolicyLookAheadWindow { get; set; } + public int? OtpPolicyPeriod { get; set; } + [DistinctValues] + public IEnumerable? OtpSupportedApplications { get; set; } + public string? PasswordPolicy { get; set; } + public string? WebAuthnPolicyRpEntityName { get; set; } + [DistinctValues] + public IEnumerable? WebAuthnPolicySignatureAlgorithms { get; set; } + public string? WebAuthnPolicyRpId { get; set; } + public string? WebAuthnPolicyAttestationConveyancePreference { get; set; } + public string? WebAuthnPolicyAuthenticatorAttachment { get; set; } + public string? WebAuthnPolicyRequireResidentKey { get; set; } + public string? WebAuthnPolicyUserVerificationRequirement { get; set; } + public int? WebAuthnPolicyCreateTimeout { get; set; } + public bool? WebAuthnPolicyAvoidSameAuthenticatorRegister { get; set; } + [DistinctValues] + public IEnumerable? WebAuthnPolicyAcceptableAaguids { get; set; } + public string? WebAuthnPolicyPasswordlessRpEntityName { get; set; } + [DistinctValues] + public IEnumerable? WebAuthnPolicyPasswordlessSignatureAlgorithms { get; set; } + public string? WebAuthnPolicyPasswordlessRpId { get; set; } + public string? WebAuthnPolicyPasswordlessAttestationConveyancePreference { get; set; } + public string? WebAuthnPolicyPasswordlessAuthenticatorAttachment { get; set; } + public string? WebAuthnPolicyPasswordlessRequireResidentKey { get; set; } + public string? WebAuthnPolicyPasswordlessUserVerificationRequirement { get; set; } + public int? WebAuthnPolicyPasswordlessCreateTimeout { get; set; } + public bool? WebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister { get; set; } + [DistinctValues] + public IEnumerable? WebAuthnPolicyPasswordlessAcceptableAaguids { get; set; } + [DistinctValues("x => x.Username")] + public IEnumerable? Users { get; set; } + [DistinctValues("x => x.ClientScope")] + public IEnumerable? ScopeMappings { get; set; } + [DistinctValues("x => x.ClientId")] + public IEnumerable? ClientScopeMappings { get; set; } + [DistinctValues("x => x.ClientId")] + public IEnumerable? Clients { get; set; } + [DistinctValues("x => x.Name")] + public IEnumerable? ClientScopes { get; set; } + [DistinctValues] + public IEnumerable? DefaultDefaultClientScopes { get; set; } + [DistinctValues] + public IEnumerable? DefaultOptionalClientScopes { get; set; } + public BrowserSecurityHeadersSettings? BrowserSecurityHeaders { get; set; } + public SmtpServerSettings? SmtpServer { get; set; } + public string? LoginTheme { get; set; } + public string? AccountTheme { get; set; } + public string? AdminTheme { get; set; } + public string? EmailTheme { get; set; } + public bool? EventsEnabled { get; set; } + [DistinctValues] + public IEnumerable? EventsListeners { get; set; } + [DistinctValues] + public IEnumerable? EnabledEventTypes { get; set; } + public bool? AdminEventsEnabled { get; set; } + public bool? AdminEventsDetailsEnabled { get; set; } + [DistinctValues("x => x.Alias")] + public IEnumerable? IdentityProviders { get; set; } + [DistinctValues("x => x.Name")] + public IEnumerable? IdentityProviderMappers { get; set; } + [DistinctValues("x => x.Name")] + public IEnumerable? Components { get; set; } + public bool? InternationalizationEnabled { get; set; } + [DistinctValues] + public IEnumerable? SupportedLocales { get; set; } + public string? DefaultLocale { get; set; } + [DistinctValues("x => x.Alias")] + public IEnumerable? AuthenticationFlows { get; set; } + [DistinctValues("x => x.Alias")] + public IEnumerable? AuthenticatorConfig { get; set; } + [DistinctValues("x => x.Alias")] + public IEnumerable? RequiredActions { get; set; } + public string? BrowserFlow { get; set; } + public string? RegistrationFlow { get; set; } + public string? DirectGrantFlow { get; set; } + public string? ResetCredentialsFlow { get; set; } + public string? ClientAuthenticationFlow { get; set; } + public string? DockerAuthenticationFlow { get; set; } + [DistinctValues("x => x.Name")] + public IEnumerable? Attributes { get; set; } + public string? KeycloakVersion { get; set; } + public bool? UserManagedAccessAllowed { get; set; } + public ClientProfilesSettings? ClientProfiles { get; set; } + public ClientPoliciesSettings? ClientPolicies { get; set; } +} + +public class AttributeSettings +{ + [Required] + public string? Name { get; set; } + public string? Value { get; set; } +} + +public class MultiValueAttributeSettings +{ + [Required] + public string? Name { get; set; } + [DistinctValues] + public IEnumerable? Values { get; set; } +} + +public class RolesSettings +{ + [DistinctValues("x => x.Name")] + public IEnumerable? Realm { get; set; } + [DistinctValues("x => x.ClientId")] + public IEnumerable? Client { get; set; } +}; + +public class ClientRoleSettings +{ + [Required] + public string? ClientId { get; set; } + [DistinctValues("x => x.Name")] + public IEnumerable? Roles { get; set; } +} + +public class CompositeRolesSettings +{ + public IEnumerable? Realm { get; set; } + [DistinctValues("x => x.ClientId")] + public IEnumerable? Client { get; set; } +}; + +public class CompositeClientRolesSettings +{ + [Required] + public string? ClientId { get; set; } + [DistinctValues] + public IEnumerable? Roles { get; set; } +} + +public class RoleSettings +{ + public string? Id { get; set; } + [Required] + public string? Name { get; set; } + public string? Description { get; set; } + public bool? Composite { get; set; } + public bool? ClientRole { get; set; } + public string? ContainerId { get; set; } + [DistinctValues("x => x.Name")] + public IEnumerable? Attributes { get; set; } + public CompositeRolesSettings? Composites { get; set; } +} + +public class UserSettings +{ + public string? Id { get; set; } + public long? CreatedTimestamp { get; set; } + [Required] + public string? Username { get; set; } + public bool? Enabled { get; set; } + public bool? Totp { get; set; } + public bool? EmailVerified { get; set; } + public string? FirstName { get; set; } + public string? LastName { get; set; } + public string? Email { get; set; } + [DistinctValues("x => x.Name")] + public IEnumerable? Attributes { get; set; } + public IEnumerable? Credentials { get; set; } + [DistinctValues] + public IEnumerable? DisableableCredentialTypes { get; set; } + [DistinctValues] + public IEnumerable? RequiredActions { get; set; } + [DistinctValues("x => x.IdentityProvider")] + public IEnumerable? FederatedIdentities { get; set; } + [DistinctValues] + public IEnumerable? RealmRoles { get; set; } + [DistinctValues("x => x.ClientId")] + public IEnumerable? ClientRoles { get; set; } + public int? NotBefore { get; set; } + [DistinctValues] + public IEnumerable? Groups { get; set; } + public string? ServiceAccountClientId { get; set; } + public UserAccessSettings? Access { get; set; } + public IEnumerable? ClientConsents { get; set; } + public string? FederationLink { get; set; } + public string? Origin { get; set; } + public string? Self { get; set; } +} + +public class CredentialsSettings +{ + public string? Algorithm { get; set; } + [DistinctValues("x => x.Name")] + public IEnumerable? Config { get; set; } + public int? Counter { get; set; } + public long? CreatedDate { get; set; } + public string? Device { get; set; } + public int? Digits { get; set; } + public int? HashIterations { get; set; } + public string? HashSaltedValue { get; set; } + public int? Period { get; set; } + public string? Salt { get; set; } + public bool? Temporary { get; set; } + public string? Type { get; set; } + public string? Value { get; set; } +} + +public class CredentialsConfigSettings +{ + [Required] + public string? Name { get; set; } + public string? Value { get; set; } +} + +public class UserClientRolesSettings +{ + [Required] + public string? ClientId { get; set; } + [DistinctValues] + public IEnumerable? Roles { get; set; } +} + +public class FederatedIdentitySettings +{ + [Required] + public string? IdentityProvider { get; set; } + [Required] + public string? UserId { get; set; } + [Required] + public string? UserName { get; set; } +} + +public class UserAccessSettings +{ + public bool? ManageGroupMembership { get; set; } + public bool? View { get; set; } + public bool? MapRoles { get; set; } + public bool? Impersonate { get; set; } + public bool? Manage { get; set; } +} + +public class ClientConsentSettings +{ + [Required] + public string? ClientId { get; set; } + public IEnumerable? GrantedClientScopes { get; set; } + public long? CreatedDate { get; set; } + public long? LastUpdatedDate { get; set; } +} + +public class GroupSettings +{ + public string? Id { get; set; } + [Required] + public string? Name { get; set; } + public string? Path { get; set; } + [DistinctValues("x => x.Name")] + public IEnumerable? Attributes { get; set; } + [DistinctValues] + public IEnumerable? RealmRoles { get; set; } + [DistinctValues("x => x.ClientId")] + public IEnumerable? ClientRoles { get; set; } + [DistinctValues] + public IEnumerable? SubGroups { get; set; } +} + +public class ScopeMappingSettings +{ + [Required] + public string? ClientScope { get; set; } + [DistinctValues] + public IEnumerable? Roles { get; set; } +} + +public class ClientScopeMappingSettings +{ + [Required] + public string? Client { get; set; } + [DistinctValues] + public IEnumerable? Roles { get; set; } +} + +public class ClientScopeMappingSettingsEntry +{ + [Required] + public string? ClientId { get; set; } + [DistinctValues("x => x.Client")] + public IEnumerable? ClientScopeMappings { get; set; } +} + +public class ClientSettings +{ + public string? Id { get; set; } + [Required] + public string? ClientId { get; set; } + public string? Name { get; set; } + public string? RootUrl { get; set; } + public string? BaseUrl { get; set; } + public bool? SurrogateAuthRequired { get; set; } + public bool? Enabled { get; set; } + public bool? AlwaysDisplayInConsole { get; set; } + public string? ClientAuthenticatorType { get; set; } + [DistinctValues] + public IEnumerable? RedirectUris { get; set; } + [DistinctValues] + public IEnumerable? WebOrigins { get; set; } + public int? NotBefore { get; set; } + public bool? BearerOnly { get; set; } + public bool? ConsentRequired { get; set; } + public bool? StandardFlowEnabled { get; set; } + public bool? ImplicitFlowEnabled { get; set; } + public bool? DirectAccessGrantsEnabled { get; set; } + public bool? ServiceAccountsEnabled { get; set; } + public bool? PublicClient { get; set; } + public bool? FrontchannelLogout { get; set; } + public string? Protocol { get; set; } + [DistinctValues("x => x.Name")] + public IEnumerable? Attributes { get; set; } + [DistinctValues("x => x.Name")] + public IEnumerable? AuthenticationFlowBindingOverrides { get; set; } + public bool? FullScopeAllowed { get; set; } + public int? NodeReRegistrationTimeout { get; set; } + [DistinctValues] + public IEnumerable? DefaultClientScopes { get; set; } + [DistinctValues] + public IEnumerable? OptionalClientScopes { get; set; } + [DistinctValues("x => x.Name")] + public IEnumerable? ProtocolMappers { get; set; } + public ClientAccessSettings? Access { get; set; } + public string? Secret { get; set; } + public string? AdminUrl { get; set; } + public string? Description { get; set; } + public bool? AuthorizationServicesEnabled { get; set; } +} + +public class ClientAttributeSettings +{ + [Required] + public string? Name { get; set; } + public string? Value { get; set; } +} + +public class AuthenticationFlowBindingOverrideSettings +{ + [Required] + public string? Name { get; set; } + public string? Value { get; set; } +} + +public class ClientAccessSettings +{ + public bool? Configure { get; set; } + public bool? Manage { get; set; } + public bool? View { get; set; } +} + +public class ProtocolMapperSettings +{ + public string? Id { get; set; } + [Required] + public string? Name { get; set; } + public string? Protocol { get; set; } + public string? ProtocolMapper { get; set; } + public bool? ConsentRequired { get; set; } + [DistinctValues("x => x.Name")] + public IEnumerable? Config { get; set; } +} + +public class ProtocolMapperConfigSettings +{ + [Required] + public string? Name { get; set; } + public string? Value { get; set; } +} + +public class ClientScopeSettings +{ + public string? Id { get; set; } + [Required] + public string? Name { get; set; } + public string? Protocol { get; set; } + [DistinctValues("x => x.Name")] + public IEnumerable? Attributes { get; set; } + [DistinctValues("x => x.Name")] + public IEnumerable? ProtocolMappers { get; set; } + public string? Description { get; set; } +} + +public class BrowserSecurityHeadersSettings +{ + public string? ContentSecurityPolicyReportOnly { get; set; } + public string? XContentTypeOptions { get; set; } + public string? XRobotsTag { get; set; } + public string? XFrameOptions { get; set; } + public string? ContentSecurityPolicy { get; set; } + [property: JsonPropertyName("xXSSProtection")] + public string? XXSSProtection { get; set; } + public string? StrictTransportSecurity { get; set; } +} + +public class SmtpServerSettings +{ + public string? Password { get; set; } + public string? Starttls { get; set; } + public string? Auth { get; set; } + public string? Port { get; set; } + public string? Host { get; set; } + public string? ReplyToDisplayName { get; set; } + public string? ReplyTo { get; set; } + public string? FromDisplayName { get; set; } + public string? From { get; set; } + public string? EnvelopeFrom { get; set; } + public string? Ssl { get; set; } + public string? User { get; set; } +} + +public class IdentityProviderSettings +{ + [Required] + public string? Alias { get; set; } + public string? DisplayName { get; set; } + public string? InternalId { get; set; } + public string? ProviderId { get; set; } + public bool? Enabled { get; set; } + public string? UpdateProfileFirstLoginMode { get; set; } + public bool? TrustEmail { get; set; } + public bool? StoreToken { get; set; } + public bool? AddReadTokenRoleOnCreate { get; set; } + public bool? AuthenticateByDefault { get; set; } + public bool? LinkOnly { get; set; } + public string? FirstBrokerLoginFlowAlias { get; set; } + public string? PostBrokerLoginFlowAlias { get; set; } + public IdentityProviderConfigSettings? Config { get; set; } +} + +public class IdentityProviderConfigSettings +{ + public string? HideOnLoginPage { get; set; } + public string? ClientSecret { get; set; } + public string? DisableUserInfo { get; set; } + public string? ValidateSignature { get; set; } + public string? ClientId { get; set; } + public string? TokenUrl { get; set; } + public string? AuthorizationUrl { get; set; } + public string? ClientAuthMethod { get; set; } + public string? JwksUrl { get; set; } + public string? LogoutUrl { get; set; } + public string? ClientAssertionSigningAlg { get; set; } + public string? SyncMode { get; set; } + public string? UseJwksUrl { get; set; } + public string? UserInfoUrl { get; set; } + public string? Issuer { get; set; } + // for Saml: + public string? NameIDPolicyFormat { get; set; } + public string? PrincipalType { get; set; } + public string? SignatureAlgorithm { get; set; } + public string? XmlSigKeyInfoKeyNameTransformer { get; set; } + public string? AllowCreate { get; set; } + public string? EntityId { get; set; } + public string? AuthnContextComparisonType { get; set; } + public string? BackchannelSupported { get; set; } + public string? PostBindingResponse { get; set; } + public string? PostBindingAuthnRequest { get; set; } + public string? PostBindingLogout { get; set; } + public string? WantAuthnRequestsSigned { get; set; } + public string? WantAssertionsSigned { get; set; } + public string? WantAssertionsEncrypted { get; set; } + public string? ForceAuthn { get; set; } + public string? SignSpMetadata { get; set; } + public string? LoginHint { get; set; } + public string? SingleSignOnServiceUrl { get; set; } + public string? AllowedClockSkew { get; set; } + public string? AttributeConsumingServiceIndex { get; set; } +} + +public class IdentityProviderMapperSettings +{ + public string? Id { get; set; } + public string? Name { get; set; } + public string? IdentityProviderAlias { get; set; } + public string? IdentityProviderMapper { get; set; } + [DistinctValues("x => x.Name")] + public IEnumerable? Config { get; set; } +} + +public class IdentityProviderMapperConfigSettings +{ + [Required] + public string? Name { get; set; } + public string? Value { get; set; } +} + +public class ComponentSettings +{ + public string? Id { get; set; } + [Required] + public string? Name { get; set; } + public string? ProviderId { get; set; } + public string? SubType { get; set; } + public object? SubComponents { get; set; } + [DistinctValues("x => x.Name")] + public IEnumerable? Config { get; set; } +} + +public class ComponentSettingsEntry +{ + [Required] + public string? Name { get; set; } + [DistinctValues("x => x.Name")] + public IEnumerable? ComponentSettings { get; set; } +} + +public class ComponentConfigSettings +{ + [Required] + public string? Name { get; set; } + [DistinctValues] + public IEnumerable? Values { get; set; } +} + +public class AuthenticationFlowSettings +{ + public string? Id { get; set; } + [Required] + public string? Alias { get; set; } + public string? Description { get; set; } + public string? ProviderId { get; set; } + public bool? TopLevel { get; set; } + public bool? BuiltIn { get; set; } + [DistinctValues("x => x.Authenticator")] + public IEnumerable? AuthenticationExecutions { get; set; } +} + +public class AuthenticationExecutionSettings +{ + public string? Authenticator { get; set; } + public bool? AuthenticatorFlow { get; set; } + public string? Requirement { get; set; } + public int? Priority { get; set; } + public bool? UserSetupAllowed { get; set; } + public bool? AutheticatorFlow { get; set; } + public string? FlowAlias { get; set; } + public string? AuthenticatorConfig { get; set; } +} + +public class AuthenticatorConfigSettings +{ + public string? Id { get; set; } + public string? Alias { get; set; } + [DistinctValues("x => x.Name")] + public IEnumerable? Config { get; set; } +} + +public class AuthenticatorConfigConfigSettings +{ + [Required] + public string? Name { get; set; } + public string? Value { get; set; } +} + +public class RequiredActionSettings +{ + public string? Alias { get; set; } + public string? Name { get; set; } + public string? ProviderId { get; set; } + public bool? Enabled { get; set; } + public bool? DefaultAction { get; set; } + public int? Priority { get; set; } + public object? Config { get; set; } +} + +public class ClientProfilesSettings +{ + public IEnumerable? Profiles { get; set; } +} + +public class ClientPoliciesSettings +{ + public IEnumerable? Policies { get; set; } +} diff --git a/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettingsExtentions.cs b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettingsExtentions.cs new file mode 100644 index 0000000000..a29dc5f4bc --- /dev/null +++ b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettingsExtentions.cs @@ -0,0 +1,497 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using System.Collections.Immutable; + +namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; + +public static class KeycloakRealmSettingsExtentions +{ + public static KeycloakRealm ToModel(this KeycloakRealmSettings keycloakRealmSettings) => + new() + { + Id = keycloakRealmSettings.Id, + Realm = keycloakRealmSettings.Realm, + DisplayName = keycloakRealmSettings.DisplayName, + DisplayNameHtml = keycloakRealmSettings.DisplayNameHtml, + NotBefore = keycloakRealmSettings.NotBefore, + DefaultSignatureAlgorithm = keycloakRealmSettings.DefaultSignatureAlgorithm, + RevokeRefreshToken = keycloakRealmSettings.RevokeRefreshToken, + RefreshTokenMaxReuse = keycloakRealmSettings.RefreshTokenMaxReuse, + AccessTokenLifespan = keycloakRealmSettings.AccessTokenLifespan, + AccessTokenLifespanForImplicitFlow = keycloakRealmSettings.AccessTokenLifespanForImplicitFlow, + SsoSessionIdleTimeout = keycloakRealmSettings.SsoSessionIdleTimeout, + SsoSessionMaxLifespan = keycloakRealmSettings.SsoSessionMaxLifespan, + SsoSessionIdleTimeoutRememberMe = keycloakRealmSettings.SsoSessionIdleTimeoutRememberMe, + SsoSessionMaxLifespanRememberMe = keycloakRealmSettings.SsoSessionMaxLifespanRememberMe, + OfflineSessionIdleTimeout = keycloakRealmSettings.OfflineSessionIdleTimeout, + OfflineSessionMaxLifespanEnabled = keycloakRealmSettings.OfflineSessionMaxLifespanEnabled, + OfflineSessionMaxLifespan = keycloakRealmSettings.OfflineSessionMaxLifespan, + ClientSessionIdleTimeout = keycloakRealmSettings.ClientSessionIdleTimeout, + ClientSessionMaxLifespan = keycloakRealmSettings.ClientSessionMaxLifespan, + ClientOfflineSessionIdleTimeout = keycloakRealmSettings.ClientOfflineSessionIdleTimeout, + ClientOfflineSessionMaxLifespan = keycloakRealmSettings.ClientOfflineSessionMaxLifespan, + AccessCodeLifespan = keycloakRealmSettings.AccessCodeLifespan, + AccessCodeLifespanUserAction = keycloakRealmSettings.AccessCodeLifespanUserAction, + AccessCodeLifespanLogin = keycloakRealmSettings.AccessCodeLifespanLogin, + ActionTokenGeneratedByAdminLifespan = keycloakRealmSettings.ActionTokenGeneratedByAdminLifespan, + ActionTokenGeneratedByUserLifespan = keycloakRealmSettings.ActionTokenGeneratedByUserLifespan, + Oauth2DeviceCodeLifespan = keycloakRealmSettings.Oauth2DeviceCodeLifespan, + Oauth2DevicePollingInterval = keycloakRealmSettings.Oauth2DevicePollingInterval, + Enabled = keycloakRealmSettings.Enabled, + SslRequired = keycloakRealmSettings.SslRequired, + RegistrationAllowed = keycloakRealmSettings.RegistrationAllowed, + RegistrationEmailAsUsername = keycloakRealmSettings.RegistrationEmailAsUsername, + RememberMe = keycloakRealmSettings.RememberMe, + VerifyEmail = keycloakRealmSettings.VerifyEmail, + LoginWithEmailAllowed = keycloakRealmSettings.LoginWithEmailAllowed, + DuplicateEmailsAllowed = keycloakRealmSettings.DuplicateEmailsAllowed, + ResetPasswordAllowed = keycloakRealmSettings.ResetPasswordAllowed, + EditUsernameAllowed = keycloakRealmSettings.EditUsernameAllowed, + BruteForceProtected = keycloakRealmSettings.BruteForceProtected, + PermanentLockout = keycloakRealmSettings.PermanentLockout, + MaxFailureWaitSeconds = keycloakRealmSettings.MaxFailureWaitSeconds, + MinimumQuickLoginWaitSeconds = keycloakRealmSettings.MinimumQuickLoginWaitSeconds, + WaitIncrementSeconds = keycloakRealmSettings.WaitIncrementSeconds, + QuickLoginCheckMilliSeconds = keycloakRealmSettings.QuickLoginCheckMilliSeconds, + MaxDeltaTimeSeconds = keycloakRealmSettings.MaxDeltaTimeSeconds, + FailureFactor = keycloakRealmSettings.FailureFactor, + Roles = keycloakRealmSettings.Roles?.ToModel(), + Groups = keycloakRealmSettings.Groups?.Select(ToModel), + DefaultRole = keycloakRealmSettings.DefaultRole?.ToModel(), + DefaultGroups = keycloakRealmSettings.DefaultGroups, + RequiredCredentials = keycloakRealmSettings.RequiredCredentials, + OtpPolicyType = keycloakRealmSettings.OtpPolicyType, + OtpPolicyAlgorithm = keycloakRealmSettings.OtpPolicyAlgorithm, + OtpPolicyInitialCounter = keycloakRealmSettings.OtpPolicyInitialCounter, + OtpPolicyDigits = keycloakRealmSettings.OtpPolicyDigits, + OtpPolicyLookAheadWindow = keycloakRealmSettings.OtpPolicyLookAheadWindow, + OtpPolicyPeriod = keycloakRealmSettings.OtpPolicyPeriod, + OtpSupportedApplications = keycloakRealmSettings.OtpSupportedApplications, + PasswordPolicy = keycloakRealmSettings.PasswordPolicy, + WebAuthnPolicyRpEntityName = keycloakRealmSettings.WebAuthnPolicyRpEntityName, + WebAuthnPolicySignatureAlgorithms = keycloakRealmSettings.WebAuthnPolicySignatureAlgorithms, + WebAuthnPolicyRpId = keycloakRealmSettings.WebAuthnPolicyRpId, + WebAuthnPolicyAttestationConveyancePreference = keycloakRealmSettings.WebAuthnPolicyAttestationConveyancePreference, + WebAuthnPolicyAuthenticatorAttachment = keycloakRealmSettings.WebAuthnPolicyAuthenticatorAttachment, + WebAuthnPolicyRequireResidentKey = keycloakRealmSettings.WebAuthnPolicyRequireResidentKey, + WebAuthnPolicyUserVerificationRequirement = keycloakRealmSettings.WebAuthnPolicyUserVerificationRequirement, + WebAuthnPolicyCreateTimeout = keycloakRealmSettings.WebAuthnPolicyCreateTimeout, + WebAuthnPolicyAvoidSameAuthenticatorRegister = keycloakRealmSettings.WebAuthnPolicyAvoidSameAuthenticatorRegister, + WebAuthnPolicyAcceptableAaguids = keycloakRealmSettings.WebAuthnPolicyAcceptableAaguids, + WebAuthnPolicyPasswordlessRpEntityName = keycloakRealmSettings.WebAuthnPolicyPasswordlessRpEntityName, + WebAuthnPolicyPasswordlessSignatureAlgorithms = keycloakRealmSettings.WebAuthnPolicyPasswordlessSignatureAlgorithms, + WebAuthnPolicyPasswordlessRpId = keycloakRealmSettings.WebAuthnPolicyPasswordlessRpId, + WebAuthnPolicyPasswordlessAttestationConveyancePreference = keycloakRealmSettings.WebAuthnPolicyPasswordlessAttestationConveyancePreference, + WebAuthnPolicyPasswordlessAuthenticatorAttachment = keycloakRealmSettings.WebAuthnPolicyPasswordlessAuthenticatorAttachment, + WebAuthnPolicyPasswordlessRequireResidentKey = keycloakRealmSettings.WebAuthnPolicyPasswordlessRequireResidentKey, + WebAuthnPolicyPasswordlessUserVerificationRequirement = keycloakRealmSettings.WebAuthnPolicyPasswordlessUserVerificationRequirement, + WebAuthnPolicyPasswordlessCreateTimeout = keycloakRealmSettings.WebAuthnPolicyPasswordlessCreateTimeout, + WebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister = keycloakRealmSettings.WebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister, + WebAuthnPolicyPasswordlessAcceptableAaguids = keycloakRealmSettings.WebAuthnPolicyPasswordlessAcceptableAaguids, + Users = keycloakRealmSettings.Users?.Select(ToModel), + ScopeMappings = keycloakRealmSettings.ScopeMappings?.Select(ToModel), + ClientScopeMappings = keycloakRealmSettings.ClientScopeMappings?.Select(ToModel).ToImmutableDictionary(), + Clients = keycloakRealmSettings.Clients?.Select(ToModel), + ClientScopes = keycloakRealmSettings.ClientScopes?.Select(ToModel), + DefaultDefaultClientScopes = keycloakRealmSettings.DefaultDefaultClientScopes, + DefaultOptionalClientScopes = keycloakRealmSettings.DefaultOptionalClientScopes, + BrowserSecurityHeaders = keycloakRealmSettings.BrowserSecurityHeaders?.ToModel(), + SmtpServer = keycloakRealmSettings.SmtpServer?.ToModel(), + LoginTheme = keycloakRealmSettings.LoginTheme, + AccountTheme = keycloakRealmSettings.AccountTheme, + AdminTheme = keycloakRealmSettings.AdminTheme, + EmailTheme = keycloakRealmSettings.EmailTheme, + EventsEnabled = keycloakRealmSettings.EventsEnabled, + EventsListeners = keycloakRealmSettings.EventsListeners, + EnabledEventTypes = keycloakRealmSettings.EnabledEventTypes, + AdminEventsEnabled = keycloakRealmSettings.AdminEventsEnabled, + AdminEventsDetailsEnabled = keycloakRealmSettings.AdminEventsDetailsEnabled, + IdentityProviders = keycloakRealmSettings.IdentityProviders?.Select(ToModel), + IdentityProviderMappers = keycloakRealmSettings.IdentityProviderMappers?.Select(ToModel), + Components = keycloakRealmSettings.Components?.Select(ToModel).ToImmutableDictionary(), + InternationalizationEnabled = keycloakRealmSettings.InternationalizationEnabled, + SupportedLocales = keycloakRealmSettings.SupportedLocales, + DefaultLocale = keycloakRealmSettings.DefaultLocale, + AuthenticationFlows = keycloakRealmSettings.AuthenticationFlows?.Select(ToModel), + AuthenticatorConfig = keycloakRealmSettings.AuthenticatorConfig?.Select(ToModel), + RequiredActions = keycloakRealmSettings.RequiredActions?.Select(ToModel), + BrowserFlow = keycloakRealmSettings.BrowserFlow, + RegistrationFlow = keycloakRealmSettings.RegistrationFlow, + DirectGrantFlow = keycloakRealmSettings.DirectGrantFlow, + ResetCredentialsFlow = keycloakRealmSettings.ResetCredentialsFlow, + ClientAuthenticationFlow = keycloakRealmSettings.ClientAuthenticationFlow, + DockerAuthenticationFlow = keycloakRealmSettings.DockerAuthenticationFlow, + Attributes = keycloakRealmSettings.Attributes?.Select(ToModel).ToImmutableDictionary(), + KeycloakVersion = keycloakRealmSettings.KeycloakVersion, + UserManagedAccessAllowed = keycloakRealmSettings.UserManagedAccessAllowed, + ClientProfiles = keycloakRealmSettings.ClientProfiles?.ToModel(), + ClientPolicies = keycloakRealmSettings.ClientPolicies?.ToModel() + }; + + private static KeyValuePair ToModel(AttributeSettings attributeSettings) => + KeyValuePair.Create( + attributeSettings.Name ?? throw new ConfigurationException(), + attributeSettings.Value); + + private static KeyValuePair?> ToModel(MultiValueAttributeSettings multiValueAttributeSettings) => + KeyValuePair.Create( + multiValueAttributeSettings.Name ?? throw new ConfigurationException("Attribute name must not be null"), + multiValueAttributeSettings.Values); + + private static KeyValuePair?> ToModel(CompositeClientRolesSettings compositeClientRolesSettings) => + KeyValuePair.Create( + compositeClientRolesSettings.ClientId ?? throw new ConfigurationException("CompositeClientRoles ClientId name must not be null"), + compositeClientRolesSettings.Roles); + + private static KeyValuePair?> ToModel(ClientRoleSettings clientRoleSettings) => + KeyValuePair.Create( + clientRoleSettings.ClientId ?? throw new ConfigurationException("clientRoles ClientId name must not be null"), + clientRoleSettings.Roles?.Select(x => x.ToModel())); + + private static CompositeRolesModel ToModel(this CompositeRolesSettings compositeRolesSettings) => + new(compositeRolesSettings.Realm, + compositeRolesSettings.Client?.Select(ToModel).ToImmutableDictionary()); + + private static RoleModel ToModel(this RoleSettings roleSettings) => + new(roleSettings.Id, + roleSettings.Name, + roleSettings.Description, + roleSettings.Composite, + roleSettings.ClientRole, + roleSettings.ContainerId, + roleSettings.Attributes?.Select(ToModel).ToImmutableDictionary(), + roleSettings.Composites?.ToModel()); + + private static RolesModel ToModel(this RolesSettings rolesSettings) => + new(rolesSettings.Realm?.Select(x => x.ToModel()), + rolesSettings.Client?.Select(ToModel).ToImmutableDictionary()); + + private static KeyValuePair?> ToModel(UserClientRolesSettings userClientRolesSettings) => + KeyValuePair.Create( + userClientRolesSettings.ClientId ?? throw new ConfigurationException("userClientRoles ClientId name must not be null"), + userClientRolesSettings.Roles); + + private static GroupModel ToModel(GroupSettings groupSettings) => + new(groupSettings.Id, + groupSettings.Name, + groupSettings.Path, + groupSettings.Attributes?.Select(ToModel).ToImmutableDictionary(), + groupSettings.RealmRoles, + groupSettings.ClientRoles?.Select(ToModel).ToImmutableDictionary(), + groupSettings.SubGroups); + + public static KeyValuePair ToModel(CredentialsConfigSettings credentialsConfigSettings) => + KeyValuePair.Create( + credentialsConfigSettings.Name ?? throw new ConfigurationException("credentialsConfig name must not be null"), + credentialsConfigSettings.Value); + + private static CredentialsModel ToModel(CredentialsSettings credentialsSettings) => + new(credentialsSettings.Algorithm, + credentialsSettings.Config?.Select(ToModel).ToImmutableDictionary(), + credentialsSettings.Counter, + credentialsSettings.CreatedDate, + credentialsSettings.Device, + credentialsSettings.Digits, + credentialsSettings.HashIterations, + credentialsSettings.HashSaltedValue, + credentialsSettings.Period, + credentialsSettings.Salt, + credentialsSettings.Temporary, + credentialsSettings.Type, + credentialsSettings.Value); + + private static FederatedIdentityModel ToModel(FederatedIdentitySettings federatedIdentitySettings) => + new(federatedIdentitySettings.IdentityProvider, + federatedIdentitySettings.UserId, + federatedIdentitySettings.UserName); + + private static UserAccessModel ToModel(this UserAccessSettings userAccessSettings) => + new(userAccessSettings.ManageGroupMembership, + userAccessSettings.View, + userAccessSettings.MapRoles, + userAccessSettings.Impersonate, + userAccessSettings.Manage); + + private static UserConsentModel ToModel(ClientConsentSettings clientConsentSettings) => + new(clientConsentSettings.ClientId, + clientConsentSettings.GrantedClientScopes, + clientConsentSettings.CreatedDate, + clientConsentSettings.LastUpdatedDate); + + private static UserModel ToModel(UserSettings userSettings) => + new(userSettings.Id, + userSettings.CreatedTimestamp, + userSettings.Username, + userSettings.Enabled, + userSettings.Totp, + userSettings.EmailVerified, + userSettings.FirstName, + userSettings.LastName, + userSettings.Email, + userSettings.Attributes?.Select(ToModel).ToImmutableDictionary(), + userSettings.Credentials?.Select(ToModel), + userSettings.DisableableCredentialTypes, + userSettings.RequiredActions, + userSettings.FederatedIdentities?.Select(ToModel), + userSettings.RealmRoles, + userSettings.ClientRoles?.Select(ToModel).ToImmutableDictionary(), + userSettings.NotBefore, + userSettings.Groups, + userSettings.ServiceAccountClientId, + userSettings.Access?.ToModel(), + userSettings.ClientConsents?.Select(ToModel), + userSettings.FederationLink, + userSettings.Origin, + userSettings.Self); + + private static ScopeMappingModel ToModel(ScopeMappingSettings scopeMappingSettings) => + new(scopeMappingSettings.ClientScope, + scopeMappingSettings.Roles); + + private static ClientScopeMappingModel ToModel(ClientScopeMappingSettings clientScopeMappingSettings) => + new(clientScopeMappingSettings.Client, + clientScopeMappingSettings.Roles); + private static KeyValuePair?> ToModel(ClientScopeMappingSettingsEntry clientScopeMappingSettingsEntry) => + KeyValuePair.Create( + clientScopeMappingSettingsEntry.ClientId ?? throw new ConfigurationException("clientScopeMappingsEntry ClientId name must not be null"), + clientScopeMappingSettingsEntry.ClientScopeMappings?.Select(ToModel)); + + private static KeyValuePair ToModel(ClientAttributeSettings clientAttributeSettings) => + KeyValuePair.Create( + clientAttributeSettings.Name ?? throw new ConfigurationException("clientAttributes Name must not be null"), + clientAttributeSettings.Value); + + private static KeyValuePair ToModel(AuthenticationFlowBindingOverrideSettings authenticationFlowBindingOverrideSettings) => + KeyValuePair.Create( + authenticationFlowBindingOverrideSettings.Name ?? throw new ConfigurationException("authenticationFlowBindingOverrides Name must not be null"), + authenticationFlowBindingOverrideSettings.Value); + + private static KeyValuePair ToModel(ProtocolMapperConfigSettings protocolMapperConfigSettings) => + KeyValuePair.Create( + protocolMapperConfigSettings.Name ?? throw new ConfigurationException("protocolMapperConfigs Name must not be null"), + protocolMapperConfigSettings.Value); + + private static ProtocolMapperModel ToModel(ProtocolMapperSettings protocolMapperSettings) => + new(protocolMapperSettings.Id, + protocolMapperSettings.Name, + protocolMapperSettings.Protocol, + protocolMapperSettings.ProtocolMapper, + protocolMapperSettings.ConsentRequired, + protocolMapperSettings.Config?.Select(ToModel).ToImmutableDictionary()); + + private static ClientAccessModel ToModel(this ClientAccessSettings clientAccessSettings) => + new(clientAccessSettings.Configure, + clientAccessSettings.Manage, + clientAccessSettings.View); + + private static ClientModel ToModel(ClientSettings clientSettings) => + new(clientSettings.Id, + clientSettings.ClientId, + clientSettings.Name, + clientSettings.RootUrl, + clientSettings.BaseUrl, + clientSettings.SurrogateAuthRequired, + clientSettings.Enabled, + clientSettings.AlwaysDisplayInConsole, + clientSettings.ClientAuthenticatorType, + clientSettings.RedirectUris, + clientSettings.WebOrigins, + clientSettings.NotBefore, + clientSettings.BearerOnly, + clientSettings.ConsentRequired, + clientSettings.StandardFlowEnabled, + clientSettings.ImplicitFlowEnabled, + clientSettings.DirectAccessGrantsEnabled, + clientSettings.ServiceAccountsEnabled, + clientSettings.PublicClient, + clientSettings.FrontchannelLogout, + clientSettings.Protocol, + clientSettings.Attributes?.Select(ToModel).ToImmutableDictionary(), + clientSettings.AuthenticationFlowBindingOverrides?.Select(ToModel).ToImmutableDictionary(), + clientSettings.FullScopeAllowed, + clientSettings.NodeReRegistrationTimeout, + clientSettings.DefaultClientScopes, + clientSettings.OptionalClientScopes, + clientSettings.ProtocolMappers?.Select(ToModel), + clientSettings.Access?.ToModel(), + clientSettings.Secret, + clientSettings.AdminUrl, + clientSettings.Description, + clientSettings.AuthorizationServicesEnabled); + + private static ClientScopeModel ToModel(this ClientScopeSettings clientScopeSettings) => + new(clientScopeSettings.Id, + clientScopeSettings.Name, + clientScopeSettings.Protocol, + clientScopeSettings.Attributes?.Select(ToModel).ToImmutableDictionary(), + clientScopeSettings.ProtocolMappers?.Select(ToModel), + clientScopeSettings.Description); + + private static BrowserSecurityHeadersModel ToModel(this BrowserSecurityHeadersSettings browserSecurityHeadersSettings) => + new(browserSecurityHeadersSettings.ContentSecurityPolicyReportOnly, + browserSecurityHeadersSettings.XContentTypeOptions, + browserSecurityHeadersSettings.XRobotsTag, + browserSecurityHeadersSettings.XFrameOptions, + browserSecurityHeadersSettings.ContentSecurityPolicy, + browserSecurityHeadersSettings.XXSSProtection, + browserSecurityHeadersSettings.StrictTransportSecurity); + + private static SmtpServerModel ToModel(this SmtpServerSettings smtpServerSettings) => + new(smtpServerSettings.Password, + smtpServerSettings.Starttls, + smtpServerSettings.Auth, + smtpServerSettings.Port, + smtpServerSettings.Host, + smtpServerSettings.ReplyToDisplayName, + smtpServerSettings.ReplyTo, + smtpServerSettings.FromDisplayName, + smtpServerSettings.From, + smtpServerSettings.EnvelopeFrom, + smtpServerSettings.Ssl, + smtpServerSettings.User); + + private static IdentityProviderConfigModel ToModel(this IdentityProviderConfigSettings identityProviderConfigSettings) => + new(identityProviderConfigSettings.HideOnLoginPage, + identityProviderConfigSettings.ClientSecret, + identityProviderConfigSettings.DisableUserInfo, + identityProviderConfigSettings.ValidateSignature, + identityProviderConfigSettings.ClientId, + identityProviderConfigSettings.TokenUrl, + identityProviderConfigSettings.AuthorizationUrl, + identityProviderConfigSettings.ClientAuthMethod, + identityProviderConfigSettings.JwksUrl, + identityProviderConfigSettings.LogoutUrl, + identityProviderConfigSettings.ClientAssertionSigningAlg, + identityProviderConfigSettings.SyncMode, + identityProviderConfigSettings.UseJwksUrl, + identityProviderConfigSettings.UserInfoUrl, + identityProviderConfigSettings.Issuer, + identityProviderConfigSettings.NameIDPolicyFormat, + identityProviderConfigSettings.PrincipalType, + identityProviderConfigSettings.SignatureAlgorithm, + identityProviderConfigSettings.XmlSigKeyInfoKeyNameTransformer, + identityProviderConfigSettings.AllowCreate, + identityProviderConfigSettings.EntityId, + identityProviderConfigSettings.AuthnContextComparisonType, + identityProviderConfigSettings.BackchannelSupported, + identityProviderConfigSettings.PostBindingResponse, + identityProviderConfigSettings.PostBindingAuthnRequest, + identityProviderConfigSettings.PostBindingLogout, + identityProviderConfigSettings.WantAuthnRequestsSigned, + identityProviderConfigSettings.WantAssertionsSigned, + identityProviderConfigSettings.WantAssertionsEncrypted, + identityProviderConfigSettings.ForceAuthn, + identityProviderConfigSettings.SignSpMetadata, + identityProviderConfigSettings.LoginHint, + identityProviderConfigSettings.SingleSignOnServiceUrl, + identityProviderConfigSettings.AllowedClockSkew, + identityProviderConfigSettings.AttributeConsumingServiceIndex); + + private static IdentityProviderModel ToModel(IdentityProviderSettings identityProviderSettings) => + new(identityProviderSettings.Alias, + identityProviderSettings.DisplayName, + identityProviderSettings.InternalId, + identityProviderSettings.ProviderId, + identityProviderSettings.Enabled, + identityProviderSettings.UpdateProfileFirstLoginMode, + identityProviderSettings.TrustEmail, + identityProviderSettings.StoreToken, + identityProviderSettings.AddReadTokenRoleOnCreate, + identityProviderSettings.AuthenticateByDefault, + identityProviderSettings.LinkOnly, + identityProviderSettings.FirstBrokerLoginFlowAlias, + identityProviderSettings.PostBrokerLoginFlowAlias, + identityProviderSettings.Config?.ToModel()); + + private static KeyValuePair ToModel(IdentityProviderMapperConfigSettings identityProviderMapperConfigSettings) => + KeyValuePair.Create( + identityProviderMapperConfigSettings.Name ?? throw new ConfigurationException("identityProviderConfigs Name must not be null"), + identityProviderMapperConfigSettings.Value); + + private static IdentityProviderMapperModel ToModel(IdentityProviderMapperSettings identityProviderMapperSettings) => + new(identityProviderMapperSettings.Id, + identityProviderMapperSettings.Name, + identityProviderMapperSettings.IdentityProviderAlias, + identityProviderMapperSettings.IdentityProviderMapper, + identityProviderMapperSettings.Config?.Select(ToModel).ToImmutableDictionary()); + + private static KeyValuePair?> ToModel(ComponentConfigSettings componentConfigSettings) => + KeyValuePair.Create( + componentConfigSettings.Name ?? throw new ConfigurationException(), + componentConfigSettings.Values); + + private static ComponentModel ToModel(ComponentSettings componentSettings) => + new(componentSettings.Id, + componentSettings.Name, + componentSettings.ProviderId, + componentSettings.SubType, + componentSettings.SubComponents, + componentSettings.Config?.Select(ToModel).ToImmutableDictionary()); + + private static KeyValuePair?> ToModel(ComponentSettingsEntry componentSettingsEntry) => + KeyValuePair.Create( + componentSettingsEntry.Name ?? throw new ConfigurationException(), + componentSettingsEntry.ComponentSettings?.Select(ToModel)); + + private static AuthenticationExecutionModel ToModel(AuthenticationExecutionSettings authenticationExecutionSettings) => + new(authenticationExecutionSettings.Authenticator, + authenticationExecutionSettings.AuthenticatorFlow, + authenticationExecutionSettings.Requirement, + authenticationExecutionSettings.Priority, + authenticationExecutionSettings.UserSetupAllowed, + authenticationExecutionSettings.AutheticatorFlow, + authenticationExecutionSettings.FlowAlias, + authenticationExecutionSettings.AuthenticatorConfig); + + private static AuthenticationFlowModel ToModel(AuthenticationFlowSettings authenticationFlowSettings) => + new(authenticationFlowSettings.Id, + authenticationFlowSettings.Alias, + authenticationFlowSettings.Description, + authenticationFlowSettings.ProviderId, + authenticationFlowSettings.TopLevel, + authenticationFlowSettings.BuiltIn, + authenticationFlowSettings.AuthenticationExecutions?.Select(ToModel)); + + private static KeyValuePair ToModel(AuthenticatorConfigConfigSettings authenticatorConfigConfigSettings) => + KeyValuePair.Create( + authenticatorConfigConfigSettings.Name ?? throw new ConfigurationException(), + authenticatorConfigConfigSettings.Value); + + private static AuthenticatorConfigModel ToModel(AuthenticatorConfigSettings authenticatorConfigSettings) => + new(authenticatorConfigSettings.Id, + authenticatorConfigSettings.Alias, + authenticatorConfigSettings.Config?.Select(ToModel).ToImmutableDictionary()); + + private static RequiredActionModel ToModel(this RequiredActionSettings requiredActionSettings) => + new(requiredActionSettings.Alias, + requiredActionSettings.Name, + requiredActionSettings.ProviderId, + requiredActionSettings.Enabled, + requiredActionSettings.DefaultAction, + requiredActionSettings.Priority, + requiredActionSettings.Config); // TODO config is declared as object + + private static ClientProfilesModel ToModel(this ClientProfilesSettings clientProfilesSettings) => + new(clientProfilesSettings.Profiles); // TODO profiles is declared as IEnumerable + + private static ClientPoliciesModel ToModel(this ClientPoliciesSettings clientPoliciesSettings) => + new(clientPoliciesSettings.Policies); // TODO policies is declared as IEnumerable +} diff --git a/src/keycloak/Keycloak.Seeding/Program.cs b/src/keycloak/Keycloak.Seeding/Program.cs index b444d7c327..2e1f96ff1c 100644 --- a/src/keycloak/Keycloak.Seeding/Program.cs +++ b/src/keycloak/Keycloak.Seeding/Program.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2023 BMW Group AG * Copyright (c) 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -29,7 +28,6 @@ LoggingExtensions.EnsureInitialized(); Log.Information("Building keycloak-seeder"); -var isDevelopment = false; try { var host = Host @@ -62,13 +60,12 @@ { FlurlUntrustedCertExceptionHandler.ConfigureExceptions(urlsToTrust); } - isDevelopment = true; } }) .UseSerilog() .Build(); - FlurlErrorHandler.ConfigureErrorHandler(host.Services.GetRequiredService>(), isDevelopment); + FlurlErrorHandler.ConfigureErrorHandler(host.Services.GetRequiredService>()); Log.Information("Building keycloak-seeder completed"); diff --git a/src/keycloak/Keycloak.Seeding/appsettings.json b/src/keycloak/Keycloak.Seeding/appsettings.json index f76ad81cd0..eba5322645 100644 --- a/src/keycloak/Keycloak.Seeding/appsettings.json +++ b/src/keycloak/Keycloak.Seeding/appsettings.json @@ -1,13 +1,13 @@ { "Serilog": { "Using": [ "Serilog.Sinks.Console" ], -"MinimumLevel": { - "Default": "Information", - "Override": { - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } -}, + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, "WriteTo": [ { "Name": "Console" } ], @@ -32,8 +32,12 @@ } }, "KeycloakSeeding": { - "DataPathes": [], - "InstanceName": "", - "ExcludedUserAttributes": [] + "Realms": [ + { + "Realm": "", + "InstanceName": "", + "DataPaths": [] + } + ] } } diff --git a/src/processes/Processes.Worker/Program.cs b/src/processes/Processes.Worker/Program.cs index d42c44b028..f8d585a0b2 100644 --- a/src/processes/Processes.Worker/Program.cs +++ b/src/processes/Processes.Worker/Program.cs @@ -45,7 +45,6 @@ LoggingExtensions.EnsureInitialized(); Log.Information("Building worker"); -var isDevelopment = false; try { var host = Host @@ -84,14 +83,12 @@ { FlurlUntrustedCertExceptionHandler.ConfigureExceptions(urlsToTrust); } - - isDevelopment = true; } }) .AddLogging() .Build(); Log.Information("Building worker completed"); - FlurlErrorHandler.ConfigureErrorHandler(host.Services.GetRequiredService>(), isDevelopment); + FlurlErrorHandler.ConfigureErrorHandler(host.Services.GetRequiredService>()); using var tokenSource = new CancellationTokenSource(); Console.CancelKeyPress += (s, e) => diff --git a/src/web/Web.Initialization/WebAppHelper.cs b/src/web/Web.Initialization/WebAppHelper.cs index e336d0faf6..2948b184ef 100644 --- a/src/web/Web.Initialization/WebAppHelper.cs +++ b/src/web/Web.Initialization/WebAppHelper.cs @@ -66,6 +66,6 @@ public static Task BuildAndRunWebApplicationAsync(string[] args, strin } } - FlurlErrorHandler.ConfigureErrorHandler(app.Services.GetRequiredService>(), environment.IsDevelopment()); + FlurlErrorHandler.ConfigureErrorHandler(app.Services.GetRequiredService>()); }); } diff --git a/tests/framework/Framework.Linq.Tests/NullOrSequenceEqualExtensionsTests.cs b/tests/framework/Framework.Linq.Tests/NullOrSequenceEqualExtensionsTests.cs index ece7efc0c3..e3ff3c790b 100644 --- a/tests/framework/Framework.Linq.Tests/NullOrSequenceEqualExtensionsTests.cs +++ b/tests/framework/Framework.Linq.Tests/NullOrSequenceEqualExtensionsTests.cs @@ -26,12 +26,13 @@ public class NullOrSequenceEqualExtensionsTests { [Theory] [InlineData(new[] { "a", "b", "c" }, new[] { "c", "b", "a" }, true)] + [InlineData(new[] { "a", "b", null }, new[] { null, "b", "a" }, true)] [InlineData(null, new[] { "c", "b", "a" }, false)] [InlineData(new[] { "a", "b", "c" }, null, false)] [InlineData(null, null, true)] [InlineData(new[] { "a", "b", "c" }, new[] { "a", "b", "c", "x" }, false)] [InlineData(new[] { "a", "b", "c", "x" }, new[] { "a", "b", "c" }, false)] - public void NullOrContentEqual_ReturnsExpected(IEnumerable? first, IEnumerable? second, bool expected) + public void NullOrContentEqual_ReturnsExpected(IEnumerable? first, IEnumerable? second, bool expected) { // Act var result = first.NullOrContentEqual(second); @@ -49,6 +50,8 @@ private class TestComparer : IEqualityComparer [Theory] [InlineData(new[] { "a", "b", "c" }, new[] { "a", "b", "c" }, true)] [InlineData(new[] { "a", "b", "c" }, new[] { "c", "b", "a" }, true)] + [InlineData(new[] { "a", "b", null }, new[] { "a", "b", null }, true)] + [InlineData(new[] { "a", "b", null }, new[] { null, "b", "a" }, true)] [InlineData(null, new[] { "c", "b", "a" }, false)] [InlineData(new[] { "a", "b", "c" }, null, false)] [InlineData(null, null, true)] @@ -66,6 +69,8 @@ public void NullOrContentEqual_WithComparer_ReturnsExpected(IEnumerable? [Theory] [InlineData(new[] { "a", "b", "c" }, new[] { "av", "bv", "cv" }, new[] { "a", "b", "c" }, new[] { "av", "bv", "cv" }, true)] [InlineData(new[] { "a", "b", "c" }, new[] { "av", "bv", "cv" }, new[] { "c", "b", "a" }, new[] { "cv", "bv", "av" }, true)] + [InlineData(new[] { "a", "b", "c" }, new[] { "av", "bv", null }, new[] { "a", "b", "c" }, new[] { "av", "bv", null }, true)] + [InlineData(new[] { "a", "b", "c" }, new[] { "av", "bv", null }, new[] { "c", "b", "a" }, new[] { null, "bv", "av" }, true)] [InlineData(null, null, new[] { "c", "b", "a" }, new[] { "cv", "bv", "av" }, false)] [InlineData(new[] { "a", "b", "c" }, new[] { "av", "bv", "cv" }, null, null, false)] [InlineData(null, null, null, null, true)] @@ -73,11 +78,34 @@ public void NullOrContentEqual_WithComparer_ReturnsExpected(IEnumerable? [InlineData(new[] { "a", "b", "c" }, new[] { "av", "bv", "cv" }, new[] { "a", "b", "c", "x" }, new[] { "av", "bv", "cv", "xv" }, false)] [InlineData(new[] { "a", "b", "x" }, new[] { "av", "bv", "cv" }, new[] { "a", "b", "c" }, new[] { "av", "bv", "cv" }, false)] [InlineData(new[] { "a", "b", "c" }, new[] { "av", "bv", "xv" }, new[] { "a", "b", "c" }, new[] { "av", "bv", "cv" }, false)] - public void NullOrContentEqual_WithKeyValuePairs_ReturnsExpected(IEnumerable? first, IEnumerable? firstValues, IEnumerable? second, IEnumerable? secondValues, bool expected) + public void NullOrContentEqual_WithKeyValuePairs_ReturnsExpected(IEnumerable? first, IEnumerable? firstValues, IEnumerable? second, IEnumerable? secondValues, bool expected) { // Arrange - var firstItems = first?.Zip(firstValues ?? throw new UnexpectedConditionException("firstValues should never be null here"), (x, y) => new KeyValuePair(x, y)); - var secondItems = second?.Zip(secondValues ?? throw new UnexpectedConditionException("secondValues should never be null here"), (x, y) => new KeyValuePair(x, y)); + var firstItems = first?.Zip(firstValues ?? throw new UnexpectedConditionException("firstValues should never be null here"), (x, y) => new KeyValuePair(x, y)); + var secondItems = second?.Zip(secondValues ?? throw new UnexpectedConditionException("secondValues should never be null here"), (x, y) => new KeyValuePair(x, y)); + + // Act + var result = firstItems.NullOrContentEqual(secondItems); + + // Assert + result.Should().Be(expected); + } + + [Theory] + [InlineData(new[] { "a", "b", "c" }, new[] { 1, 2, 3 }, new[] { "a", "b", "c" }, new[] { 1, 2, 3 }, true)] + [InlineData(new[] { "a", "b", "c" }, new[] { 1, 2, 3 }, new[] { "c", "b", "a" }, new[] { 3, 2, 1 }, true)] + [InlineData(null, null, new[] { "c", "b", "a" }, new[] { 3, 2, 1 }, false)] + [InlineData(new[] { "a", "b", "c" }, new[] { 1, 2, 3 }, null, null, false)] + [InlineData(null, null, null, null, true)] + [InlineData(new[] { "a", "b", "c", "x" }, new[] { 1, 2, 3, 4 }, new[] { "a", "b", "c" }, new[] { 1, 2, 3 }, false)] + [InlineData(new[] { "a", "b", "c" }, new[] { 1, 2, 3 }, new[] { "a", "b", "c", "x" }, new[] { 1, 2, 3, 4 }, false)] + [InlineData(new[] { "a", "b", "x" }, new[] { 1, 2, 3 }, new[] { "a", "b", "c" }, new[] { 1, 2, 3 }, false)] + [InlineData(new[] { "a", "b", "c" }, new[] { 1, 2, 4 }, new[] { "a", "b", "c" }, new[] { 1, 2, 3 }, false)] + public void NullOrContentEqual_WithValueTypedKeyValuePairs_ReturnsExpected(IEnumerable? first, IEnumerable? firstValues, IEnumerable? second, IEnumerable? secondValues, bool expected) + { + // Arrange + var firstItems = first?.Zip(firstValues ?? throw new UnexpectedConditionException("firstValues should never be null here"), (x, y) => new KeyValuePair(x, y)); + var secondItems = second?.Zip(secondValues ?? throw new UnexpectedConditionException("secondValues should never be null here"), (x, y) => new KeyValuePair(x, y)); // Act var result = firstItems.NullOrContentEqual(secondItems); @@ -107,4 +135,30 @@ public void NullOrContentEqual_WithKeyValuePairEnumerables_ReturnsExpected(IEnum // Assert result.Should().Be(expected); } + + [Theory] + [InlineData(new[] { "a", "b", "c" }, new[] { "a1", "a2", "a3" }, new[] { "b1", "b2", "b3" }, new[] { "c1", "c2", "c3" }, new[] { "a", "b", "c" }, new[] { "a1", "a2", "a3" }, new[] { "b1", "b2", "b3" }, new[] { "c1", "c2", "c3" }, true)] + [InlineData(new[] { "c", "b", "a" }, new[] { "c1", "c2", "c3" }, new[] { "b1", "b2", "b3" }, new[] { "a1", "a2", "a3" }, new[] { "a", "b", "c" }, new[] { "a1", "a2", "a3" }, new[] { "b1", "b2", "b3" }, new[] { "c1", "c2", "c3" }, true)] + [InlineData(new[] { "a", "b", "c" }, new[] { "a3", "a2", "a1" }, new[] { "b1", "b2", "b3" }, new[] { "c1", "c2", "c3" }, new[] { "a", "b", "c" }, new[] { "a1", "a2", "a3" }, new[] { "b1", "b2", "b3" }, new[] { "c1", "c2", "c3" }, true)] + [InlineData(new[] { "a", "b", "c" }, new[] { null, "a2", "a1" }, new[] { "b1", "b2", "b3" }, new[] { "c1", "c2", "c3" }, new[] { "a", "b", "c" }, new[] { "a1", "a2", null }, new[] { "b1", "b2", "b3" }, new[] { "c1", "c2", "c3" }, true)] + [InlineData(new[] { "a", "b", "x" }, new[] { "a1", "a2", "a3" }, new[] { "b1", "b2", "b3" }, new[] { "c1", "c2", "c3" }, new[] { "a", "b", "c" }, new[] { "a1", "a2", "a3" }, new[] { "b1", "b2", "b3" }, new[] { "c1", "c2", "c3" }, false)] + [InlineData(new[] { "a", "b", "c" }, new[] { "a1", "a2", "x3" }, new[] { "b1", "b2", "b3" }, new[] { "c1", "c2", "c3" }, new[] { "a", "b", "c" }, new[] { "a1", "a2", "a3" }, new[] { "b1", "b2", "b3" }, new[] { "c1", "c2", "c3" }, false)] + [InlineData(null, new string[] { }, new string[] { }, new string[] { }, new[] { "a", "b", "c" }, new[] { "a1", "a2", "a3" }, new[] { "b1", "b2", "b3" }, new[] { "c1", "c2", "c3" }, false)] + [InlineData(new[] { "a", "b", "c" }, new[] { "a1", "a2", "a3" }, new[] { "b1", "b2", "b3" }, new[] { "c1", "c2", "c3" }, null, new string[] { }, new string[] { }, new string[] { }, false)] + [InlineData(null, new string[] { }, new string[] { }, new string[] { }, null, new string[] { }, new string[] { }, new string[] { }, true)] + [InlineData(new[] { "a", "b", "c" }, null, new[] { "b1", "b2", "b3" }, new[] { "c1", "c2", "c3" }, new[] { "a", "b", "c" }, null, new[] { "b1", "b2", "b3" }, new[] { "c1", "c2", "c3" }, true)] + [InlineData(new[] { "a", "b", "c" }, null, new[] { "b1", "b2", "b3" }, new[] { "c1", "c2", "c3" }, new[] { "a", "b", "c" }, new[] { "a1", "a2", "a3" }, new[] { "b1", "b2", "b3" }, new[] { "c1", "c2", "c3" }, false)] + [InlineData(new[] { "a", "b", "c" }, new[] { "a1", "a2", "a3" }, new[] { "b1", "b2", "b3" }, new[] { "c1", "c2", "c3" }, new[] { "a", "b", "c" }, null, new[] { "b1", "b2", "b3" }, new[] { "c1", "c2", "c3" }, false)] + public void NullOrContentEqual_WithNullableKeyValuePairEnumerables_ReturnsExpected(IEnumerable? first, IEnumerable? firstFirstValues, IEnumerable? firstSecondValues, IEnumerable? firstThirdValues, IEnumerable? second, IEnumerable? secondFirstValues, IEnumerable? secondSecondValues, IEnumerable? secondThirdValues, bool expected) + { + // Arrange + var firstItems = first?.Zip(new[] { firstFirstValues, firstSecondValues, firstThirdValues }, (x, y) => new KeyValuePair?>(x, y)); + var secondItems = second?.Zip(new[] { secondFirstValues, secondSecondValues, secondThirdValues }, (x, y) => new KeyValuePair?>(x, y)); + + // Act + var result = firstItems.NullOrNullableContentEqual(secondItems); + + // Assert + result.Should().Be(expected); + } } diff --git a/tests/keycloak/Keycloak.Seeding.Tests/KeycloakRealmModelTests.cs b/tests/keycloak/Keycloak.Seeding.Tests/KeycloakRealmModelTests.cs index 8996fbf5cb..b88d09090d 100644 --- a/tests/keycloak/Keycloak.Seeding.Tests/KeycloakRealmModelTests.cs +++ b/tests/keycloak/Keycloak.Seeding.Tests/KeycloakRealmModelTests.cs @@ -28,10 +28,49 @@ public class KeycloakRealmModelTests public async Task SeedDataHandlerImportsExpected() { // Arrange + var settings = new KeycloakSeederSettings() + { + Realms = [ + new() + { + Realm = "TestRealm", + InstanceName = "foo", + DataPaths = ["TestSeeds/test-realm.json"], + Clients = [ + new() + { + ClientId = "TestClientId", + Secret = "testsecret", + RedirectUris = [ + "https://redirect.url" + ], + Attributes = [ + new() + { + Name = "login_theme", + Value = "test" + } + ] + } + ], + IdentityProviders = [ + new() + { + Alias = "Test Identity Provider", + Config = new() + { + TokenUrl = "https://token.test", + ClientSecret = "foobarsecret" + } + } + ] + } + ] + }; var sut = new SeedDataHandler(); // Act - await sut.Import("TestSeeds/test-realm.json", CancellationToken.None); + await sut.Import(settings.Realms.First(), CancellationToken.None); var keycloakRealm = sut.KeycloakRealm; var clients = sut.Clients; @@ -106,11 +145,10 @@ public async Task SeedDataHandlerImportsExpected() x.FailureFactor == 30 && x.Roles != null && x.Roles.Client != null && - x.Roles.Client.SequenceEqual(clientRoles) && + x.Roles.Client.SequenceEqual(clientRoles.Select(x => KeyValuePair.Create?>(x.ClientId, x.RoleModels))) && x.Roles.Realm != null && x.Roles.Realm.SequenceEqual(realmRoles) && - x.Groups != null && - // roles and groups are being asserted separately + x.Groups != null && // roles and groups are being asserted separately x.DefaultRole != null && x.DefaultRole.Id == "fd20bacb-f39f-499c-8fc3-c3d14e0770d9" && x.DefaultRole.Name == "default-roles-testrealm" && @@ -160,11 +198,9 @@ public async Task SeedDataHandlerImportsExpected() x.Users != null && x.ScopeMappings != null && x.ClientScopeMappings != null && - x.Clients != null && - x.Clients.SequenceEqual(clients) && + x.Clients != null && // clients are being asserted separately x.ClientScopes != null && - x.ClientScopes.SequenceEqual(clientScopes) && - // users, scopeMappings, clientScopeMappings, clients, clientScopes are being asserted separately + x.ClientScopes.SequenceEqual(clientScopes) && // users, scopeMappings, clientScopeMappings, clients, clientScopes are being asserted separately x.DefaultDefaultClientScopes != null && x.DefaultDefaultClientScopes.SequenceEqual(new[] { "role_list", "profile", "email", "roles", "web-origins" }) && x.DefaultOptionalClientScopes != null && @@ -204,54 +240,66 @@ public async Task SeedDataHandlerImportsExpected() !x.AdminEventsEnabled.Value && x.AdminEventsDetailsEnabled.HasValue && !x.AdminEventsDetailsEnabled.Value && - x.IdentityProviders != null && - x.IdentityProviders.SequenceEqual(identityProviders) && + x.IdentityProviders != null && // identityProviders, identityProviderMappers, components are being asserted separately x.IdentityProviderMappers != null && x.IdentityProviderMappers.SequenceEqual(identityProviderMappers) && - // identityProviders, identityProviderMappers, components are being asserted separately x.InternationalizationEnabled.HasValue && x.InternationalizationEnabled.Value && x.SupportedLocales != null && x.SupportedLocales.SequenceEqual(new[] { "de", "no", "ru", "sv", "pt-BR", "lt", "en", "it", "fr", "hu", "zh-CN", "es", "cs", "ja", "sk", "pl", "da", "ca", "nl", "tr" }) && x.DefaultLocale == "en" && - // authenticationFlows, authenticatorConfigs, requiredActions are being asserted separately - x.BrowserFlow == "browser" && + x.BrowserFlow == "browser" && // authenticationFlows, authenticatorConfigs, requiredActions are being asserted separately x.RegistrationFlow == "registration" && x.DirectGrantFlow == "direct grant" && x.ResetCredentialsFlow == "reset credentials" && x.ClientAuthenticationFlow == "clients" && x.DockerAuthenticationFlow == "docker auth" && x.Attributes != null && - x.Attributes.SequenceEqual(new Dictionary { - { "cibaBackchannelTokenDeliveryMode", "poll" }, - { "cibaAuthRequestedUserHint", "login_hint" }, - { "oauth2DevicePollingInterval", "5" }, - { "clientOfflineSessionMaxLifespan", "0" }, - { "clientSessionIdleTimeout", "0" }, - { "userProfileEnabled", "false" }, - { "clientOfflineSessionIdleTimeout", "0" }, - { "cibaInterval", "5" }, - { "cibaExpiresIn", "120" }, - { "oauth2DeviceCodeLifespan", "600" }, - { "parRequestUriLifespan", "60" }, - { "clientSessionMaxLifespan", "0" }, - { "frontendUrl", "http://frontend.url" } + x.Attributes.SequenceEqual(new[] { + KeyValuePair.Create("cibaBackchannelTokenDeliveryMode", "poll"), + KeyValuePair.Create("cibaAuthRequestedUserHint", "login_hint"), + KeyValuePair.Create("oauth2DevicePollingInterval", "5"), + KeyValuePair.Create("clientOfflineSessionMaxLifespan", "0"), + KeyValuePair.Create("clientSessionIdleTimeout", "0"), + KeyValuePair.Create("userProfileEnabled", "false"), + KeyValuePair.Create("clientOfflineSessionIdleTimeout", "0"), + KeyValuePair.Create("cibaInterval", "5"), + KeyValuePair.Create("cibaExpiresIn", "120"), + KeyValuePair.Create("oauth2DeviceCodeLifespan", "600"), + KeyValuePair.Create("parRequestUriLifespan", "60"), + KeyValuePair.Create("clientSessionMaxLifespan", "0"), + KeyValuePair.Create("frontendUrl", "http://frontend.url") }) && x.KeycloakVersion == "16.1.1" && x.UserManagedAccessAllowed.HasValue && !x.UserManagedAccessAllowed.Value ); + keycloakRealm.Clients.Should().Contain(x => x.ClientId == "TestClientId") + .Which.Should().Match(x => + x.Name == "TestClient Name" && + x.Secret == "testsecret" && + x.RedirectUris != null && + x.RedirectUris.SequenceEqual(new[] { "https://redirect.url" }) && + x.Attributes != null && + x.Attributes["login_theme"] == "test" + ); + + keycloakRealm.IdentityProviders.Should().Contain(x => x.Alias == "Test Identity Provider") + .Which.Should().Match(x => + x.DisplayName == "Test Identity Provider Display Name" && + x.Config != null && + x.Config.TokenUrl == "https://token.test" && + x.Config.ClientSecret == "foobarsecret" + ); + keycloakRealm.Groups.Should().ContainSingle() .Which.Should().Match(x => x.Id == "145bc75c-7755-4cd2-a746-45097fb2883a" && x.Name == "Test Group 1" && x.Path == "/Test Group 1" && - x.Attributes.NullOrContentEqual( - new Dictionary> - { - { "Test Group 1 Attribute", new [] { "Test Group 1 Attribute Value" } } - }, + x.Attributes.NullOrNullableContentEqual( + new[] { KeyValuePair.Create?>("Test Group 1 Attribute", new[] { "Test Group 1 Attribute Value" }) }, null) && x.RealmRoles != null && x.RealmRoles.SequenceEqual( @@ -259,11 +307,8 @@ public async Task SeedDataHandlerImportsExpected() { "offline_access" }) && - x.ClientRoles.NullOrContentEqual( - new Dictionary> - { - { "realm-management", new [] { "create-client" } } - }, + x.ClientRoles.NullOrNullableContentEqual( + new[] { KeyValuePair.Create?>("realm-management", new[] { "create-client" }) }, null) ); @@ -278,11 +323,8 @@ public async Task SeedDataHandlerImportsExpected() x.Composites.Realm != null && x.Composites.Realm.SequenceEqual(new[] { "offline_access", "uma_authorization" }) && x.Composites.Client != null && - x.Composites.Client.NullOrContentEqual( - new Dictionary> - { - { "account", new [] { "view-profile", "manage-account" } } - }, + x.Composites.Client.NullOrNullableContentEqual( + new[] { KeyValuePair.Create?>("account", new[] { "view-profile", "manage-account" }) }, null ) && x.ClientRole.HasValue && @@ -308,10 +350,18 @@ public async Task SeedDataHandlerImportsExpected() x.Id == "e9b8d11f-8e45-4910-8dbb-aa206764f1bc"); clientRoles.Should().HaveCount(8) - .And.ContainKeys(new[] { "realm-management", "security-admin-console", "admin-cli", "account-console", "broker", "TestClientId", "account", "TestServiceAccount1" }); + .And.Satisfy( + x => x.ClientId == "realm-management", + x => x.ClientId == "security-admin-console", + x => x.ClientId == "admin-cli", + x => x.ClientId == "account-console", + x => x.ClientId == "broker", + x => x.ClientId == "TestClientId", + x => x.ClientId == "account", + x => x.ClientId == "TestServiceAccount1"); - clientRoles.Should().ContainKey("TestClientId") - .WhoseValue.Should().HaveCount(2) + clientRoles.Should().ContainSingle(x => x.ClientId == "TestClientId") + .Which.RoleModels.Should().HaveCount(2) .And.Satisfy( x => x.Id == "889fd981-c56f-4b46-bc43-f62e1004185e" && @@ -320,20 +370,14 @@ public async Task SeedDataHandlerImportsExpected() x.Composite.HasValue && x.Composite.Value && x.Composites != null && - x.Composites.Client.NullOrContentEqual( - new Dictionary> - { - { "TestClientId", new [] { "test_role_1" } } - }, + x.Composites.Client.NullOrNullableContentEqual( + new[] { KeyValuePair.Create?>("TestClientId", new[] { "test_role_1" }) }, null) && x.ClientRole.HasValue && x.ClientRole.Value && x.ContainerId == "654052fa-59c4-484e-90f7-0c389c0e9d37" && - x.Attributes.NullOrContentEqual( - new Dictionary> - { - { "Test Composite Role Attribute", new [] { "Test Composite Role Attribute Value" } } - }, + x.Attributes.NullOrNullableContentEqual( + new[] { KeyValuePair.Create?>("Test Composite Role Attribute", new[] { "Test Composite Role Attribute Value" }) }, null ), x => @@ -346,11 +390,8 @@ public async Task SeedDataHandlerImportsExpected() x.ClientRole.HasValue && x.ClientRole.Value && x.ContainerId == "654052fa-59c4-484e-90f7-0c389c0e9d37" && - x.Attributes.NullOrContentEqual( - new Dictionary> - { - { "test_role_1_attribute", new [] { "test_role_1_attribute_value" } } - }, + x.Attributes.NullOrNullableContentEqual( + new[] { KeyValuePair.Create?>("test_role_1_attribute", new[] { "test_role_1_attribute_value" }) }, null )); @@ -388,7 +429,7 @@ public async Task SeedDataHandlerImportsExpected() x.FirstName == "Test" && x.LastName == "User" && x.Email == "test.user@mail.org" && - x.Attributes.NullOrContentEqual(new Dictionary> { { "foo", new[] { "DEADBEEF", "deadbeef" } } }, null) && + x.Attributes.NullOrNullableContentEqual(new[] { KeyValuePair.Create?>("foo", new[] { "DEADBEEF", "deadbeef" }) }, null) && x.Credentials != null && !x.Credentials.Any() && x.DisableableCredentialTypes != null && @@ -397,7 +438,7 @@ public async Task SeedDataHandlerImportsExpected() x.FederatedIdentities.Select(i => new ValueTuple(i.IdentityProvider, i.UserId, i.UserName)).NullOrContentEqual(new[] { new ValueTuple("Test Identity Provider", "testIdentityProviderUserId1", "testIdentityProviderUserName1") }, null) && x.RealmRoles != null && x.RealmRoles.SequenceEqual(new[] { "default-roles-testrealm", "test_realm_role_1" }) && - x.ClientRoles.NullOrContentEqual(new Dictionary> { { "TestClientId", new[] { "test_role_1" } } }, null) && + x.ClientRoles.NullOrNullableContentEqual(new[] { KeyValuePair.Create?>("TestClientId", new[] { "test_role_1" }) }, null) && x.NotBefore == 0 && x.Groups != null && !x.Groups.Any());