From 329439f562085214a1567320720764bdfa25773d Mon Sep 17 00:00:00 2001 From: Norbert Truchsess Date: Wed, 26 Jun 2024 19:01:59 +0200 Subject: [PATCH] adjust handling of null dictionary-values in seeder-settings --- .../Framework.Linq/IfAnyExtension.cs | 13 +++ .../NullOrSequenceEqualExtension.cs | 22 ++++- .../Framework.Linq/NullableExtensions.cs | 20 +++++ .../AuthenticationFlowsUpdater.cs | 8 +- .../BusinessLogic/ClientScopeMapperUpdater.cs | 17 ++-- .../BusinessLogic/ClientScopesUpdater.cs | 6 +- .../BusinessLogic/ClientsUpdater.cs | 10 +-- .../BusinessLogic/ISeedDataHandler.cs | 4 +- .../BusinessLogic/IdentityProvidersUpdater.cs | 5 +- .../BusinessLogic/ProtocolMappersUpdater.cs | 5 +- .../BusinessLogic/RealmUpdater.cs | 9 +- .../BusinessLogic/RolesUpdater.cs | 5 +- .../BusinessLogic/SeedDataHandler.cs | 11 ++- .../BusinessLogic/UsersUpdater.cs | 40 +++------ .../Keycloak.Seeding/Models/KeycloakRealm.cs | 35 ++++---- .../Models/KeycloakRealmSettingsExtentions.cs | 52 +++++------ .../NullOrSequenceEqualExtensionsTests.cs | 62 ++++++++++++- .../KeycloakRealmModelTests.cs | 90 +++++++++---------- 18 files changed, 240 insertions(+), 174 deletions(-) diff --git a/src/framework/Framework.Linq/IfAnyExtension.cs b/src/framework/Framework.Linq/IfAnyExtension.cs index d5cb182fa5..5dcdc6a4e2 100644 --- a/src/framework/Framework.Linq/IfAnyExtension.cs +++ b/src/framework/Framework.Linq/IfAnyExtension.cs @@ -107,4 +107,17 @@ public static bool IfAny(this IEnumerable source, Func, returnValue = null; 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..cd30965a9f 100644 --- a/src/framework/Framework.Linq/NullableExtensions.cs +++ b/src/framework/Framework.Linq/NullableExtensions.cs @@ -23,4 +23,24 @@ public static class NullableExtensions { public static bool IsNullOrEmpty(this IEnumerable? collection) => collection == null || !collection.Any(); + + public static IEnumerable> FilterNotNullValues(this IEnumerable> source) + { + foreach (var (key, value) in source) + { + if (value == null) + continue; + yield return KeyValuePair.Create(key, value); + } + } + + public static IEnumerable FilterNotNull(this IEnumerable source) + { + foreach (var item in source) + { + if (item == null) + continue; + yield return item; + } + } } diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/AuthenticationFlowsUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/AuthenticationFlowsUpdater.cs index b41330b041..c76aff95fe 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 30710aa4ff..53a715df2c 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientsUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientsUpdater.cs @@ -161,8 +161,8 @@ 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, @@ -202,8 +202,8 @@ private static bool CompareClient(Client client, ClientModel update) => 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) && @@ -228,7 +228,7 @@ private static bool CompareClientProtocolMapper(ClientProtocolMapper mapper, Pro mapper.Config != null && update.Config != null && CompareClientProtocolMapperConfig(mapper.Config, update.Config)); - private static bool CompareClientProtocolMapperConfig(ClientConfig config, IReadOnlyDictionary update) => + 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") && diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/ISeedDataHandler.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/ISeedDataHandler.cs index e9685b35b7..f0fab90c2a 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/ISeedDataHandler.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/ISeedDataHandler.cs @@ -32,7 +32,7 @@ public interface ISeedDataHandler IEnumerable Clients { get; } - IReadOnlyDictionary> ClientRoles { get; } + IEnumerable<(string ClientId, IEnumerable RoleModels)> ClientRoles { get; } IEnumerable RealmRoles { get; } @@ -48,7 +48,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/IdentityProvidersUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/IdentityProvidersUpdater.cs index 5e8204b0b6..fab321bc9d 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 @@ -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/ProtocolMappersUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/ProtocolMappersUpdater.cs index e0d338faad..2b8a0dbb20 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"), @@ -67,7 +66,7 @@ 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") && diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/RealmUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/RealmUpdater.cs index 7723e63bdc..cb11307b41 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..c167bbe437 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/RolesUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/RolesUpdater.cs @@ -136,6 +136,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 +209,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 +218,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 f6c0c2665c..e050704a69 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/SeedDataHandler.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/SeedDataHandler.cs @@ -19,6 +19,7 @@ using Microsoft.Extensions.Options; 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; @@ -75,10 +76,9 @@ public IEnumerable Clients get => _jsonRealm?.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 => _jsonRealm?.Roles?.Client?.FilterNotNullValues().Select(x => (x.Key, x.Value)) ?? Enumerable.Empty<(string, IEnumerable)>(); } public IEnumerable RealmRoles @@ -117,10 +117,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 => _jsonRealm?.ClientScopeMappings?.FilterNotNullValues().Select(x => (x.Key, x.Value)) ?? Enumerable.Empty<(string, IEnumerable)>(); } public async Task SetClientInternalIds(IAsyncEnumerable<(string ClientId, string Id)> clientInternalIds) diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/UsersUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/UsersUpdater.cs index 33834b971a..50ac972acb 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 @@ -127,29 +126,17 @@ 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() @@ -167,7 +154,7 @@ private static async Task UpdateUserRoles(Func>> getUserR DisableableCredentialTypes = update.DisableableCredentialTypes, RequiredActions = update.RequiredActions, NotBefore = update.NotBefore, - Attributes = UpdateAttributes(user?.Attributes, update.Attributes?.ToDictionary(x => x.Key, x => x.Value), excludedUserAttributes), + Attributes = UpdateAttributes(user?.Attributes, update.Attributes?.FilterNotNullValues(), excludedUserAttributes)?.ToDictionary(), Groups = update.Groups, ServiceAccountClientId = update.ServiceAccountClientId }; @@ -185,7 +172,7 @@ 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; @@ -262,17 +249,16 @@ await keycloak.AddUserSocialLoginProviderAsync( } } - private static IDictionary>? UpdateAttributes(IDictionary>? existingAttributes, IDictionary>? updatedDictionary, IEnumerable excludedUserAttributes) + private static IEnumerable>>? UpdateAttributes(IEnumerable>>? existingAttributes, IEnumerable>>? updatedDictionary, IEnumerable excludedUserAttributes) { if (existingAttributes is null) return updatedDictionary; - var attributesToKeep = existingAttributes.Where(x => excludedUserAttributes.Contains(x.Key)); + var attributesToKeep = existingAttributes.IntersectBy(excludedUserAttributes, x => x.Key); return updatedDictionary is null - ? attributesToKeep.ToDictionary(x => x.Key, x => x.Value) + ? attributesToKeep : updatedDictionary - .Where(x => !excludedUserAttributes.Contains(x.Key)) - .Concat(attributesToKeep) - .ToDictionary(x => x.Key, x => x.Value); + .ExceptBy(excludedUserAttributes, x => x.Key) + .Concat(attributesToKeep); } } diff --git a/src/keycloak/Keycloak.Seeding/Models/KeycloakRealm.cs b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealm.cs index d221272989..299710eeaa 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,13 +173,13 @@ public record UserModel( string? FirstName, string? LastName, string? Email, - IReadOnlyDictionary>? Attributes, + IReadOnlyDictionary?>? Attributes, IEnumerable? Credentials, IEnumerable? DisableableCredentialTypes, IEnumerable? RequiredActions, IEnumerable? FederatedIdentities, IEnumerable? RealmRoles, - IReadOnlyDictionary>? ClientRoles, + IReadOnlyDictionary?>? ClientRoles, int? NotBefore, IEnumerable? Groups, string? ServiceAccountClientId @@ -196,9 +195,9 @@ public record GroupModel( string? Id, string? Name, string? Path, - IReadOnlyDictionary>? Attributes, + IReadOnlyDictionary?>? Attributes, IEnumerable? RealmRoles, - IReadOnlyDictionary>? ClientRoles, + IReadOnlyDictionary?>? ClientRoles, IEnumerable? SubGroups ); @@ -234,8 +233,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 +259,14 @@ public record ProtocolMapperModel( string? Protocol, string? ProtocolMapper, bool? ConsentRequired, - IReadOnlyDictionary? Config + IReadOnlyDictionary? Config ); public record ClientScopeModel( string? Id, string? Name, string? Protocol, - IReadOnlyDictionary? Attributes, + IReadOnlyDictionary? Attributes, IEnumerable? ProtocolMappers, string? Description ); @@ -359,7 +358,7 @@ public record IdentityProviderMapperModel( string? Name, string? IdentityProviderAlias, string? IdentityProviderMapper, - IReadOnlyDictionary? Config + IReadOnlyDictionary? Config ); public record ComponentModel( @@ -368,7 +367,7 @@ public record ComponentModel( string? ProviderId, string? SubType, object? SubComponents, - IReadOnlyDictionary>? Config + IReadOnlyDictionary?>? Config ); public record AuthenticationFlowModel( @@ -395,7 +394,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/KeycloakRealmSettingsExtentions.cs b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettingsExtentions.cs index 67d0ad325e..191d54d1be 100644 --- a/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettingsExtentions.cs +++ b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettingsExtentions.cs @@ -146,25 +146,25 @@ public static KeycloakRealm ToModel(this KeycloakRealmSettings keycloakRealmSett ClientPolicies = keycloakRealmSettings.ClientPolicies?.ToModel() }; - private static KeyValuePair ToModel(AttributeSettings attributeSettings) => + private static KeyValuePair ToModel(AttributeSettings attributeSettings) => KeyValuePair.Create( attributeSettings.Name ?? throw new ConfigurationException(), - attributeSettings.Value ?? throw new ConfigurationException()); + attributeSettings.Value); - private static KeyValuePair> ToModel(MultiValueAttributeSettings multiValueAttributeSettings) => + private static KeyValuePair?> ToModel(MultiValueAttributeSettings multiValueAttributeSettings) => KeyValuePair.Create( multiValueAttributeSettings.Name ?? throw new ConfigurationException("Attribute name must not be null"), - multiValueAttributeSettings.Values ?? throw new ConfigurationException("Attribute values must not be null")); + multiValueAttributeSettings.Values); - private static KeyValuePair> ToModel(CompositeClientRolesSettings compositeClientRolesSettings) => + private static KeyValuePair?> ToModel(CompositeClientRolesSettings compositeClientRolesSettings) => KeyValuePair.Create( compositeClientRolesSettings.ClientId ?? throw new ConfigurationException("CompositeClientRoles ClientId name must not be null"), - compositeClientRolesSettings.Roles ?? throw new ConfigurationException("CompositeClientRoles roles must not be null")); + compositeClientRolesSettings.Roles); - private static KeyValuePair> ToModel(ClientRoleSettings clientRoleSettings) => + private static KeyValuePair?> ToModel(ClientRoleSettings clientRoleSettings) => KeyValuePair.Create( clientRoleSettings.ClientId ?? throw new ConfigurationException("clientRoles ClientId name must not be null"), - (clientRoleSettings.Roles ?? throw new ConfigurationException("clientRoles roles must not be null")).Select(x => x.ToModel())); + clientRoleSettings.Roles?.Select(x => x.ToModel())); private static CompositeRolesModel ToModel(this CompositeRolesSettings compositeRolesSettings) => new(compositeRolesSettings.Realm, @@ -184,10 +184,10 @@ 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) => + private static KeyValuePair?> ToModel(UserClientRolesSettings userClientRolesSettings) => KeyValuePair.Create( userClientRolesSettings.ClientId ?? throw new ConfigurationException("userClientRoles ClientId name must not be null"), - userClientRolesSettings.Roles ?? throw new ConfigurationException("userClientRoles roles must not be null")); + userClientRolesSettings.Roles); private static GroupModel ToModel(GroupSettings groupSettings) => new(groupSettings.Id, @@ -231,25 +231,25 @@ private static ScopeMappingModel ToModel(ScopeMappingSettings scopeMappingSettin private static ClientScopeMappingModel ToModel(ClientScopeMappingSettings clientScopeMappingSettings) => new(clientScopeMappingSettings.Client, clientScopeMappingSettings.Roles); - private static KeyValuePair> ToModel(ClientScopeMappingSettingsEntry clientScopeMappingSettingsEntry) => + private static KeyValuePair?> ToModel(ClientScopeMappingSettingsEntry clientScopeMappingSettingsEntry) => KeyValuePair.Create( clientScopeMappingSettingsEntry.ClientId ?? throw new ConfigurationException("clientScopeMappingsEntry ClientId name must not be null"), - clientScopeMappingSettingsEntry.ClientScopeMappings?.Select(ToModel) ?? throw new ConfigurationException("clientScopeMappingsEntry ClientScopeMappings name must not be null")); + clientScopeMappingSettingsEntry.ClientScopeMappings?.Select(ToModel)); - private static KeyValuePair ToModel(ClientAttributeSettings clientAttributeSettings) => + private static KeyValuePair ToModel(ClientAttributeSettings clientAttributeSettings) => KeyValuePair.Create( clientAttributeSettings.Name ?? throw new ConfigurationException("clientAttributes Name must not be null"), - clientAttributeSettings.Value ?? throw new ConfigurationException("clientAttributes Value must not be null")); + clientAttributeSettings.Value); - private static KeyValuePair ToModel(AuthenticationFlowBindingOverrideSettings authenticationFlowBindingOverrideSettings) => + private static KeyValuePair ToModel(AuthenticationFlowBindingOverrideSettings authenticationFlowBindingOverrideSettings) => KeyValuePair.Create( authenticationFlowBindingOverrideSettings.Name ?? throw new ConfigurationException("authenticationFlowBindingOverrides Name must not be null"), - authenticationFlowBindingOverrideSettings.Value ?? throw new ConfigurationException("authenticationFlowBindingOverrides Value must not be null")); + authenticationFlowBindingOverrideSettings.Value); - private static KeyValuePair ToModel(ProtocolMapperConfigSettings protocolMapperConfigSettings) => + private static KeyValuePair ToModel(ProtocolMapperConfigSettings protocolMapperConfigSettings) => KeyValuePair.Create( protocolMapperConfigSettings.Name ?? throw new ConfigurationException("protocolMapperConfigs Name must not be null"), - protocolMapperConfigSettings.Value ?? throw new ConfigurationException("protocolMapperConfigs Value must not be null")); + protocolMapperConfigSettings.Value); private static ProtocolMapperModel ToModel(ProtocolMapperSettings protocolMapperSettings) => new(protocolMapperSettings.Id, @@ -383,10 +383,10 @@ private static IdentityProviderModel ToModel(IdentityProviderSettings identityPr identityProviderSettings.PostBrokerLoginFlowAlias, identityProviderSettings.Config?.ToModel()); - private static KeyValuePair ToModel(IdentityProviderMapperConfigSettings identityProviderMapperConfigSettings) => + private static KeyValuePair ToModel(IdentityProviderMapperConfigSettings identityProviderMapperConfigSettings) => KeyValuePair.Create( identityProviderMapperConfigSettings.Name ?? throw new ConfigurationException("identityProviderConfigs Name must not be null"), - identityProviderMapperConfigSettings.Value ?? throw new ConfigurationException("identityProviderConfigs Value must not be null")); + identityProviderMapperConfigSettings.Value); private static IdentityProviderMapperModel ToModel(IdentityProviderMapperSettings identityProviderMapperSettings) => new(identityProviderMapperSettings.Id, @@ -395,10 +395,10 @@ private static IdentityProviderMapperModel ToModel(IdentityProviderMapperSetting identityProviderMapperSettings.IdentityProviderMapper, identityProviderMapperSettings.Config?.Select(ToModel)?.ToImmutableDictionary()); - private static KeyValuePair> ToModel(ComponentConfigSettings componentConfigSettings) => + private static KeyValuePair?> ToModel(ComponentConfigSettings componentConfigSettings) => KeyValuePair.Create( componentConfigSettings.Name ?? throw new ConfigurationException(), - componentConfigSettings.Values ?? throw new ConfigurationException()); + componentConfigSettings.Values); private static ComponentModel ToModel(ComponentSettings componentSettings) => new(componentSettings.Id, @@ -408,10 +408,10 @@ private static ComponentModel ToModel(ComponentSettings componentSettings) => componentSettings.SubComponents, componentSettings.Config?.Select(ToModel)?.ToImmutableDictionary()); - private static KeyValuePair> ToModel(ComponentSettingsEntry componentSettingsEntry) => + private static KeyValuePair?> ToModel(ComponentSettingsEntry componentSettingsEntry) => KeyValuePair.Create( componentSettingsEntry.Name ?? throw new ConfigurationException(), - componentSettingsEntry.ComponentSettings?.Select(ToModel) ?? throw new ConfigurationException()); + componentSettingsEntry.ComponentSettings?.Select(ToModel)); private static AuthenticationExecutionModel ToModel(AuthenticationExecutionSettings authenticationExecutionSettings) => new(authenticationExecutionSettings.Authenticator, @@ -432,10 +432,10 @@ private static AuthenticationFlowModel ToModel(AuthenticationFlowSettings authen authenticationFlowSettings.BuiltIn, authenticationFlowSettings.AuthenticationExecutions?.Select(ToModel)); - private static KeyValuePair ToModel(AuthenticatorConfigConfigSettings authenticatorConfigConfigSettings) => + private static KeyValuePair ToModel(AuthenticatorConfigConfigSettings authenticatorConfigConfigSettings) => KeyValuePair.Create( authenticatorConfigConfigSettings.Name ?? throw new ConfigurationException(), - authenticatorConfigConfigSettings.Value ?? throw new ConfigurationException()); + authenticatorConfigConfigSettings.Value); private static AuthenticatorConfigModel ToModel(AuthenticatorConfigSettings authenticatorConfigSettings) => new(authenticatorConfigSettings.Id, 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 f2646e7e3a..93be43c70f 100644 --- a/tests/keycloak/Keycloak.Seeding.Tests/KeycloakRealmModelTests.cs +++ b/tests/keycloak/Keycloak.Seeding.Tests/KeycloakRealmModelTests.cs @@ -144,7 +144,7 @@ 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 && @@ -259,20 +259,20 @@ public async Task SeedDataHandlerImportsExpected() 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 && @@ -302,11 +302,8 @@ public async Task SeedDataHandlerImportsExpected() 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( @@ -314,11 +311,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) ); @@ -333,11 +327,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 && @@ -363,10 +354,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" && @@ -375,20 +374,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 => @@ -401,11 +394,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 )); @@ -443,7 +433,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 && @@ -452,7 +442,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());