From 69f4e6be979ffef074270cbd9c62617989ac53ca Mon Sep 17 00:00:00 2001 From: Phil Schneider Date: Wed, 20 Nov 2024 13:50:02 +0100 Subject: [PATCH] feat(seeding): add userProfile and localizationTexts to seeding (#1154) * feat(seeding): add userProfile and localizationTexts to seeding Refs: #1150 --- .../Components/KeycloakClient.cs | 52 ++---- .../Keycloak.Library/Keycloak.Library.csproj | 1 + .../Localization/KeycloakClient.cs | 87 ++++++++++ .../Models/Components/Component.cs | 20 ++- .../Models/Components/Config.cs | 47 ------ .../Models/RealmsAdmin/Realm.cs | 4 + .../Keycloak.Library/Models/Root/Locale.cs | 5 +- .../Models/Users/UserProfileConfig.cs | 155 ++++++++++++++++++ .../Keycloak.Library/Users/KeycloakClient.cs | 16 ++ .../BusinessLogic/ILocalizationsUpdater.cs | 25 +++ .../BusinessLogic/ISeedDataHandler.cs | 3 + .../BusinessLogic/IUserProfileUpdater.cs | 25 +++ .../BusinessLogic/IdentityProvidersUpdater.cs | 20 +-- .../BusinessLogic/KeecloakSeeder.cs | 64 ++++---- .../BusinessLogic/LocalizationsUpdater.cs | 107 ++++++++++++ .../BusinessLogic/RealmUpdater.cs | 20 +-- .../BusinessLogic/SeedDataHandler.cs | 10 ++ .../BusinessLogic/UserProfileUpdater.cs | 64 ++++++++ .../Keycloak.Seeding/Models/KeycloakRealm.cs | 3 +- .../Models/KeycloakRealmExtensions.cs | 1 + .../Models/KeycloakRealmSettings.cs | 1 + .../Models/KeycloakRealmSettingsExtentions.cs | 2 + src/keycloak/Keycloak.Seeding/Program.cs | 2 + 23 files changed, 575 insertions(+), 159 deletions(-) create mode 100644 src/keycloak/Keycloak.Library/Localization/KeycloakClient.cs delete mode 100644 src/keycloak/Keycloak.Library/Models/Components/Config.cs create mode 100644 src/keycloak/Keycloak.Library/Models/Users/UserProfileConfig.cs create mode 100644 src/keycloak/Keycloak.Seeding/BusinessLogic/ILocalizationsUpdater.cs create mode 100644 src/keycloak/Keycloak.Seeding/BusinessLogic/IUserProfileUpdater.cs create mode 100644 src/keycloak/Keycloak.Seeding/BusinessLogic/LocalizationsUpdater.cs create mode 100644 src/keycloak/Keycloak.Seeding/BusinessLogic/UserProfileUpdater.cs diff --git a/src/keycloak/Keycloak.Library/Components/KeycloakClient.cs b/src/keycloak/Keycloak.Library/Components/KeycloakClient.cs index d279fa2e98..a92a4c90f4 100644 --- a/src/keycloak/Keycloak.Library/Components/KeycloakClient.cs +++ b/src/keycloak/Keycloak.Library/Components/KeycloakClient.cs @@ -30,15 +30,15 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library; public partial class KeycloakClient { - public async Task CreateComponentAsync(string realm, Component componentRepresentation) => - await (await GetBaseUrlAsync(realm).ConfigureAwait(ConfigureAwaitOptions.None)) + public async Task CreateComponentAsync(string realm, Component componentRepresentation, CancellationToken cancellationToken) => + await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)) .AppendPathSegment("/admin/realms/") .AppendPathSegment(realm, true) .AppendPathSegment("/components") - .PostJsonAsync(componentRepresentation) + .PostJsonAsync(componentRepresentation, cancellationToken: cancellationToken) .ConfigureAwait(ConfigureAwaitOptions.None); - public async Task> GetComponentsAsync(string realm, string? name = null, string? parent = null, string? type = null) + public async Task> GetComponentsAsync(string realm, string? name = null, string? parent = null, string? type = null, CancellationToken cancellationToken = default) { var queryParams = new Dictionary { @@ -47,58 +47,30 @@ public async Task> GetComponentsAsync(string realm, strin [nameof(type)] = type }; - return await (await GetBaseUrlAsync(realm).ConfigureAwait(ConfigureAwaitOptions.None)) + return await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)) .AppendPathSegment("/admin/realms/") .AppendPathSegment(realm, true) .AppendPathSegment("/components") .SetQueryParams(queryParams) - .GetJsonAsync>() + .GetJsonAsync>(cancellationToken: cancellationToken) .ConfigureAwait(ConfigureAwaitOptions.None); } - public async Task GetComponentAsync(string realm, string componentId) => - await (await GetBaseUrlAsync(realm).ConfigureAwait(ConfigureAwaitOptions.None)) + public async Task UpdateComponentAsync(string realm, string componentId, Component componentRepresentation, CancellationToken cancellationToken) => + await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)) .AppendPathSegment("/admin/realms/") .AppendPathSegment(realm, true) .AppendPathSegment("/components/") .AppendPathSegment(componentId, true) - .GetJsonAsync() + .PutJsonAsync(componentRepresentation, cancellationToken: cancellationToken) .ConfigureAwait(ConfigureAwaitOptions.None); - public async Task UpdateComponentAsync(string realm, string componentId, Component componentRepresentation) => - await (await GetBaseUrlAsync(realm).ConfigureAwait(ConfigureAwaitOptions.None)) + public async Task DeleteComponentAsync(string realm, string componentId, CancellationToken cancellationToken) => + await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)) .AppendPathSegment("/admin/realms/") .AppendPathSegment(realm, true) .AppendPathSegment("/components/") .AppendPathSegment(componentId, true) - .PutJsonAsync(componentRepresentation) + .DeleteAsync(cancellationToken: cancellationToken) .ConfigureAwait(ConfigureAwaitOptions.None); - - public async Task DeleteComponentAsync(string realm, string componentId) => - await (await GetBaseUrlAsync(realm).ConfigureAwait(ConfigureAwaitOptions.None)) - .AppendPathSegment("/admin/realms/") - .AppendPathSegment(realm, true) - .AppendPathSegment("/components/") - .AppendPathSegment(componentId, true) - .DeleteAsync() - .ConfigureAwait(ConfigureAwaitOptions.None); - - public async Task> GetSubcomponentTypesAsync(string realm, string componentId, string? type = null) - { - var queryParams = new Dictionary - { - [nameof(type)] = type - }; - - var result = await (await GetBaseUrlAsync(realm).ConfigureAwait(ConfigureAwaitOptions.None)) - .AppendPathSegment("/admin/realms/") - .AppendPathSegment(realm, true) - .AppendPathSegment("/components/") - .AppendPathSegment(componentId, true) - .AppendPathSegment("/sub-component-types") - .SetQueryParams(queryParams) - .GetJsonAsync>() - .ConfigureAwait(ConfigureAwaitOptions.None); - return result; - } } diff --git a/src/keycloak/Keycloak.Library/Keycloak.Library.csproj b/src/keycloak/Keycloak.Library/Keycloak.Library.csproj index b0e7f5a261..de9d357dce 100644 --- a/src/keycloak/Keycloak.Library/Keycloak.Library.csproj +++ b/src/keycloak/Keycloak.Library/Keycloak.Library.csproj @@ -31,6 +31,7 @@ net8.0 enable enable + ef300f2e-b1c3-4ce1-b028-92533b71aa73 diff --git a/src/keycloak/Keycloak.Library/Localization/KeycloakClient.cs b/src/keycloak/Keycloak.Library/Localization/KeycloakClient.cs new file mode 100644 index 0000000000..414ac2ed42 --- /dev/null +++ b/src/keycloak/Keycloak.Library/Localization/KeycloakClient.cs @@ -0,0 +1,87 @@ +/******************************************************************************** + * 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 Flurl.Http; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Linq; +using System.Net.Http.Headers; + +namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library; + +public partial class KeycloakClient +{ + private const string AdminUrlSegment = "/admin/realms/"; + private const string LocalizationUrlSegment = "/localization/"; + + public async Task> GetLocaleAsync(string realm, CancellationToken cancellationToken = default) + { + return await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)) + .AppendPathSegment(AdminUrlSegment) + .AppendPathSegment(realm, true) + .AppendPathSegment("/localization") + .GetJsonAsync>(cancellationToken: cancellationToken) + .ConfigureAwait(ConfigureAwaitOptions.None); + } + + public async Task>> GetLocaleAsync(string realm, string locale, CancellationToken cancellationToken = default) + { + var response = await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)) + .AppendPathSegment(AdminUrlSegment) + .AppendPathSegment(realm, true) + .AppendPathSegment(LocalizationUrlSegment) + .AppendPathSegment(locale, true) + .GetJsonAsync?>(cancellationToken: cancellationToken) + .ConfigureAwait(ConfigureAwaitOptions.None); + + return response == null + ? Enumerable.Empty>() + : response.FilterNotNull(); + } + + public async Task UpdateLocaleAsync(string realm, string locale, string key, string translation, CancellationToken cancellationToken) + { + using var content = new StringContent(translation, MediaTypeHeaderValue.Parse("text/plain")); + await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)) + .AppendPathSegment(AdminUrlSegment) + .AppendPathSegment(realm, true) + .AppendPathSegment(LocalizationUrlSegment) + .AppendPathSegment(locale, true) + .AppendPathSegment(key, true) + .PutAsync(content, cancellationToken: cancellationToken) + .ConfigureAwait(ConfigureAwaitOptions.None); + } + + public async Task DeleteLocaleAsync(string realm, string locale, string key, CancellationToken cancellationToken) => + await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)) + .AppendPathSegment(AdminUrlSegment) + .AppendPathSegment(realm, true) + .AppendPathSegment(LocalizationUrlSegment) + .AppendPathSegment(locale, true) + .AppendPathSegment(key, true) + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(ConfigureAwaitOptions.None); + + public async Task DeleteLocaleAsync(string realm, string locale, CancellationToken cancellationToken) => + await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)) + .AppendPathSegment(AdminUrlSegment) + .AppendPathSegment(realm, true) + .AppendPathSegment(LocalizationUrlSegment) + .AppendPathSegment(locale, true) + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(ConfigureAwaitOptions.None); +} diff --git a/src/keycloak/Keycloak.Library/Models/Components/Component.cs b/src/keycloak/Keycloak.Library/Models/Components/Component.cs index 301767263a..be06b6c2cf 100644 --- a/src/keycloak/Keycloak.Library/Models/Components/Component.cs +++ b/src/keycloak/Keycloak.Library/Models/Components/Component.cs @@ -31,17 +31,23 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Components public class Component { [JsonPropertyName("id")] - public string Id { get; set; } + public string? Id { get; set; } + [JsonPropertyName("name")] - public string Name { get; set; } + public string? Name { get; set; } + [JsonPropertyName("providerId")] - public string ProviderId { get; set; } + public string? ProviderId { get; set; } + [JsonPropertyName("providerType")] - public string ProviderType { get; set; } + public string? ProviderType { get; set; } + [JsonPropertyName("parentId")] - public string ParentId { get; set; } + public string? ParentId { get; set; } + [JsonPropertyName("config")] - public Config Config { get; set; } + public IReadOnlyDictionary?>? Config { get; set; } + [JsonPropertyName("subType")] - public string SubType { get; set; } + public string? SubType { get; set; } } diff --git a/src/keycloak/Keycloak.Library/Models/Components/Config.cs b/src/keycloak/Keycloak.Library/Models/Components/Config.cs deleted file mode 100644 index 3497e32b34..0000000000 --- a/src/keycloak/Keycloak.Library/Models/Components/Config.cs +++ /dev/null @@ -1,47 +0,0 @@ -/******************************************************************************** - * MIT License - * - * Copyright (c) 2019 Luk Vermeulen - * Copyright (c) 2022 BMW Group AG - * Copyright (c) 2022 Contributors to the Eclipse Foundation - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - ********************************************************************************/ - -using System.Text.Json.Serialization; - -namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Components; - -public class Config -{ - [JsonPropertyName("priority")] - public IEnumerable Priority { get; set; } - [JsonPropertyName("allowdefaultscopes")] - public IEnumerable AllowDefaultScopes { get; set; } - [JsonPropertyName("maxclients")] - public IEnumerable MaxClients { get; set; } - [JsonPropertyName("allowedprotocolmappertypes")] - public IEnumerable AllowedProtocolMapperTypes { get; set; } - [JsonPropertyName("algorithm")] - public IEnumerable Algorithm { get; set; } - [JsonPropertyName("hostsendingregistrationrequestmustmatch")] - public IEnumerable HostSendingRegistrationRequestMustMatch { get; set; } - [JsonPropertyName("clienturismustmatch")] - public IEnumerable ClientUrisMustMatch { get; set; } -} diff --git a/src/keycloak/Keycloak.Library/Models/RealmsAdmin/Realm.cs b/src/keycloak/Keycloak.Library/Models/RealmsAdmin/Realm.cs index cd311b5ed0..1fb29f7cb8 100644 --- a/src/keycloak/Keycloak.Library/Models/RealmsAdmin/Realm.cs +++ b/src/keycloak/Keycloak.Library/Models/RealmsAdmin/Realm.cs @@ -176,4 +176,8 @@ public class Realm public bool? UserManagedAccessAllowed { get; set; } [JsonPropertyName("passwordPolicy")] public string? PasswordPolicy { get; set; } + [JsonPropertyName("defaultLocale")] + public string? DefaultLocale { get; set; } + [JsonPropertyName("localizationTexts")] + public IDictionary?>? LocalizationTexts { get; set; } } diff --git a/src/keycloak/Keycloak.Library/Models/Root/Locale.cs b/src/keycloak/Keycloak.Library/Models/Root/Locale.cs index f332520d7c..d474a7fba6 100644 --- a/src/keycloak/Keycloak.Library/Models/Root/Locale.cs +++ b/src/keycloak/Keycloak.Library/Models/Root/Locale.cs @@ -34,5 +34,8 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Root; public enum Locale { [EnumMember(Value = "en")] - En + En, + + [EnumMember(Value = "de")] + De } diff --git a/src/keycloak/Keycloak.Library/Models/Users/UserProfileConfig.cs b/src/keycloak/Keycloak.Library/Models/Users/UserProfileConfig.cs new file mode 100644 index 0000000000..b6e14dee85 --- /dev/null +++ b/src/keycloak/Keycloak.Library/Models/Users/UserProfileConfig.cs @@ -0,0 +1,155 @@ +/******************************************************************************** + * 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.Users; + +public sealed class UserProfileConfig : IEquatable +{ + public IEnumerable Attributes { get; set; } = null!; + + public IEnumerable Groups { get; set; } = null!; + + public bool Equals(UserProfileConfig? other) => + other is not null && + Attributes.OrderBy(x => x.Name).SequenceEqual(other.Attributes.OrderBy(x => x.Name)) && + Groups.OrderBy(x => x.Name).SequenceEqual(other.Groups.OrderBy(x => x.Name)); + + public override bool Equals(object? obj) => + obj is not null && obj.GetType() == this.GetType() && Equals((UserProfileConfig)obj); + + public override int GetHashCode() => + HashCode.Combine(Attributes, Groups); +} + +public sealed class ProfileAttribute : IEquatable +{ + public string Name { get; set; } = null!; + public string DisplayName { get; set; } = null!; + public object? Validations { get; set; } + public object? Annotations { get; set; } + public ProfileAttributeRequired? Required { get; set; } + public ProfileAttributePermission Permissions { get; set; } = null!; + public ProfileAttributeSelector? Selector { get; set; } + public string Group { get; set; } = null!; + public bool Multivalued { get; set; } + + public bool Equals(ProfileAttribute? other) => + other is not null && + Name == other.Name && + DisplayName == other.DisplayName && + Equals(Validations, other.Validations) && + Equals(Annotations, other.Annotations) && + Equals(Required, other.Required) && + Permissions.Equals(other.Permissions) && + Equals(Selector, other.Selector) && + Group == other.Group && + Multivalued == other.Multivalued; + + public override bool Equals(object? obj) => + obj is not null && obj.GetType() == this.GetType() && Equals((ProfileAttribute)obj); + + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(Name); + hashCode.Add(DisplayName); + hashCode.Add(Validations); + hashCode.Add(Annotations); + hashCode.Add(Required); + hashCode.Add(Permissions); + hashCode.Add(Selector); + hashCode.Add(Group); + hashCode.Add(Multivalued); + return hashCode.ToHashCode(); + } +} + +public sealed class ProfileAttributeRequired : IEquatable +{ + public IEnumerable? Roles { get; init; } = null!; + public IEnumerable? Scopes { get; init; } = null!; + + public bool Equals(ProfileAttributeRequired? other) => + other is not null && + ((Roles == null && other.Roles == null) || + Roles != null && other.Roles != null && Roles.Order().SequenceEqual(other.Roles.Order())) && + ((Scopes == null && other.Scopes == null) || Scopes != null && other.Scopes != null && Scopes.Order().SequenceEqual(other.Scopes.Order())); + + public override bool Equals(object? obj) => + obj is not null && obj.GetType() == this.GetType() && Equals((ProfileAttributeRequired)obj); + + public override int GetHashCode() => HashCode.Combine(Roles, Scopes); +} + +public sealed class ProfileAttributePermission : IEquatable +{ + public IEnumerable View { get; init; } = null!; + public IEnumerable Edit { get; init; } = null!; + + public bool Equals(ProfileAttributePermission? other) => + other is not null && + View.Order().SequenceEqual(other.View.Order()) && + Edit.Order().SequenceEqual(other.Edit.Order()); + + public override bool Equals(object? obj) => + obj is not null && obj.GetType() == this.GetType() && Equals((ProfileAttributePermission)obj); + + public override int GetHashCode() => + HashCode.Combine(View, Edit); +} + +public sealed class ProfileAttributeSelector : IEquatable +{ + public IEnumerable Attributes { get; init; } = null!; + public IEnumerable Groups { get; init; } = null!; + public object? UnmanagedAttributePolicy { get; init; } + + public bool Equals(ProfileAttributeSelector? other) => + other is not null && + Attributes.OrderBy(x => x.Name).SequenceEqual(other.Attributes.OrderBy(x => x.Name)) && + Groups.OrderBy(x => x.Name).SequenceEqual(other.Groups.OrderBy(x => x.Name)) && + Equals(UnmanagedAttributePolicy, other.UnmanagedAttributePolicy); + + public override bool Equals(object? obj) => + obj is not null && obj.GetType() == this.GetType() && Equals((ProfileAttributeSelector)obj); + + public override int GetHashCode() => + HashCode.Combine(Attributes, Groups, UnmanagedAttributePolicy); +} + +public sealed class ProfileGroup : IEquatable +{ + public string Name { get; init; } = null!; + public string DisplayHeader { get; init; } = null!; + public string DisplayDescription { get; init; } = null!; + public IEnumerable Annotations { get; init; } = null!; + + public bool Equals(ProfileGroup? other) => + other is not null && + Name == other.Name && + DisplayHeader == other.DisplayHeader && + DisplayDescription == other.DisplayDescription && + Annotations.Order().SequenceEqual(other.Annotations.Order()); + + public override bool Equals(object? obj) => + obj is not null && obj.GetType() == this.GetType() && Equals((ProfileGroup)obj); + + public override int GetHashCode() => + HashCode.Combine(Name, DisplayHeader, DisplayDescription, Annotations); +} diff --git a/src/keycloak/Keycloak.Library/Users/KeycloakClient.cs b/src/keycloak/Keycloak.Library/Users/KeycloakClient.cs index ec44e27326..a4529bb992 100644 --- a/src/keycloak/Keycloak.Library/Users/KeycloakClient.cs +++ b/src/keycloak/Keycloak.Library/Users/KeycloakClient.cs @@ -355,4 +355,20 @@ public async Task> GetUserSessionsAsync(string realm, s .AppendPathSegment("/sessions") .GetJsonAsync>() .ConfigureAwait(ConfigureAwaitOptions.None); + + public async Task GetUsersProfile(string realm, CancellationToken cancellationToken) => + await (await GetBaseUrlAsync(realm, cancellationToken: cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)) + .AppendPathSegment("/admin/realms/") + .AppendPathSegment(realm, true) + .AppendPathSegment("/users/profile") + .GetJsonAsync(cancellationToken: cancellationToken) + .ConfigureAwait(ConfigureAwaitOptions.None); + + public async Task UpdateUsersProfile(string realm, UserProfileConfig config, CancellationToken cancellationToken) => + await (await GetBaseUrlAsync(realm, cancellationToken: cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)) + .AppendPathSegment("/admin/realms/") + .AppendPathSegment(realm, true) + .AppendPathSegment("/users/profile") + .PutJsonAsync(config, cancellationToken: cancellationToken) + .ConfigureAwait(ConfigureAwaitOptions.None); } diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/ILocalizationsUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/ILocalizationsUpdater.cs new file mode 100644 index 0000000000..8e69ef4fa0 --- /dev/null +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/ILocalizationsUpdater.cs @@ -0,0 +1,25 @@ +/******************************************************************************** + * 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.Seeding.BusinessLogic; + +public interface ILocalizationsUpdater +{ + Task UpdateLocalizations(string keycloakInstanceName, CancellationToken cancellationToken); +} diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/ISeedDataHandler.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/ISeedDataHandler.cs index 3500896c30..9a2beecac6 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/ISeedDataHandler.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/ISeedDataHandler.cs @@ -49,6 +49,9 @@ public interface ISeedDataHandler IEnumerable<(string ClientId, IEnumerable ClientScopeMappingModels)> ClientScopeMappings { get; } + IEnumerable<(string ProviderType, ComponentModel ComponentModel)> RealmComponents { get; } + IEnumerable<(string Locale, IEnumerable> Translations)> RealmLocalizations { get; } + Task SetClientInternalIds(IAsyncEnumerable<(string ClientId, string Id)> clientInternalIds); string GetIdOfClient(string clientId); diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/IUserProfileUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/IUserProfileUpdater.cs new file mode 100644 index 0000000000..ed5e91a334 --- /dev/null +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/IUserProfileUpdater.cs @@ -0,0 +1,25 @@ +/******************************************************************************** + * 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.Seeding.BusinessLogic; + +public interface IUserProfileUpdater +{ + Task UpdateUserProfile(string keycloakInstanceName, CancellationToken cancellationToken); +} diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/IdentityProvidersUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/IdentityProvidersUpdater.cs index a043c8c81e..d78892a450 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/IdentityProvidersUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/IdentityProvidersUpdater.cs @@ -27,23 +27,15 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic; -public class IdentityProvidersUpdater : IIdentityProvidersUpdater +public class IdentityProvidersUpdater(IKeycloakFactory keycloakFactory, ISeedDataHandler seedDataHandler) + : IIdentityProvidersUpdater { - private readonly IKeycloakFactory _keycloakFactory; - private readonly ISeedDataHandler _seedData; - - public IdentityProvidersUpdater(IKeycloakFactory keycloakFactory, ISeedDataHandler seedDataHandler) - { - _keycloakFactory = keycloakFactory; - _seedData = seedDataHandler; - } - public async Task UpdateIdentityProviders(string keycloakInstanceName, CancellationToken cancellationToken) { - var keycloak = _keycloakFactory.CreateKeycloakClient(keycloakInstanceName); - var realm = _seedData.Realm; + var keycloak = keycloakFactory.CreateKeycloakClient(keycloakInstanceName); + var realm = seedDataHandler.Realm; - foreach (var updateIdentityProvider in _seedData.IdentityProviders) + foreach (var updateIdentityProvider in seedDataHandler.IdentityProviders) { if (updateIdentityProvider.Alias == null) throw new ConflictException($"identityProvider alias must not be null: {updateIdentityProvider.InternalId} {updateIdentityProvider.DisplayName}"); @@ -64,7 +56,7 @@ public async Task UpdateIdentityProviders(string keycloakInstanceName, Cancellat await keycloak.CreateIdentityProviderAsync(realm, identityProvider, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } - var updateMappers = _seedData.IdentityProviderMappers.Where(x => x.IdentityProviderAlias == updateIdentityProvider.Alias); + var updateMappers = seedDataHandler.IdentityProviderMappers.Where(x => x.IdentityProviderAlias == updateIdentityProvider.Alias); var mappers = await keycloak.GetIdentityProviderMappersAsync(realm, updateIdentityProvider.Alias, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); await DeleteObsoleteIdentityProviderMappers(keycloak, realm, updateIdentityProvider.Alias, mappers, updateMappers, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/KeecloakSeeder.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/KeecloakSeeder.cs index aa86dbf5c7..b3e749aa00 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/KeecloakSeeder.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/KeecloakSeeder.cs @@ -21,48 +21,40 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic; -public class KeycloakSeeder : IKeycloakSeeder +public class KeycloakSeeder( + ISeedDataHandler seedDataHandler, + IRealmUpdater realmUpdater, + IRolesUpdater rolesUpdater, + IClientsUpdater clientsUpdater, + IIdentityProvidersUpdater identityProvidersUpdater, + IUsersUpdater usersUpdater, + IClientScopesUpdater clientScopesUpdater, + IAuthenticationFlowsUpdater authenticationFlowsUpdater, + IClientScopeMapperUpdater clientScopeMapperUpdater, + ILocalizationsUpdater localizationsUpdater, + IUserProfileUpdater userProfileUpdater, + IOptions options) + : IKeycloakSeeder { - private readonly KeycloakSeederSettings _settings; - private readonly ISeedDataHandler _seedData; - private readonly IRealmUpdater _realmUpdater; - private readonly IRolesUpdater _rolesUpdater; - private readonly IClientsUpdater _clientsUpdater; - private readonly IIdentityProvidersUpdater _identityProvidersUpdater; - private readonly IUsersUpdater _usersUpdater; - private readonly IClientScopesUpdater _clientScopesUpdater; - private readonly IAuthenticationFlowsUpdater _authenticationFlowsUpdater; - private readonly IClientScopeMapperUpdater _clientScopeMapperUpdater; - - public KeycloakSeeder(ISeedDataHandler seedDataHandler, IRealmUpdater realmUpdater, IRolesUpdater rolesUpdater, IClientsUpdater clientsUpdater, IIdentityProvidersUpdater identityProvidersUpdater, IUsersUpdater usersUpdater, IClientScopesUpdater clientScopesUpdater, IAuthenticationFlowsUpdater authenticationFlowsUpdater, IClientScopeMapperUpdater clientScopeMapperUpdater, IOptions options) - { - _seedData = seedDataHandler; - _realmUpdater = realmUpdater; - _rolesUpdater = rolesUpdater; - _clientsUpdater = clientsUpdater; - _identityProvidersUpdater = identityProvidersUpdater; - _usersUpdater = usersUpdater; - _clientScopesUpdater = clientScopesUpdater; - _authenticationFlowsUpdater = authenticationFlowsUpdater; - _clientScopeMapperUpdater = clientScopeMapperUpdater; - _settings = options.Value; - } + private readonly KeycloakSeederSettings _settings = options.Value; public async Task Seed(CancellationToken cancellationToken) { foreach (var realm in _settings.Realms) { - 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); + await seedDataHandler.Import(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await realmUpdater.UpdateRealm(realm.InstanceName, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await localizationsUpdater.UpdateLocalizations(realm.InstanceName, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await userProfileUpdater.UpdateUserProfile(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/LocalizationsUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/LocalizationsUpdater.cs new file mode 100644 index 0000000000..c6b8840ecc --- /dev/null +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/LocalizationsUpdater.cs @@ -0,0 +1,107 @@ +/******************************************************************************** + * 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.Linq; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Factory; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library; + +namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic; + +public class LocalizationsUpdater(IKeycloakFactory keycloakFactory, ISeedDataHandler seedDataHandler) + : ILocalizationsUpdater +{ + public async Task UpdateLocalizations(string keycloakInstanceName, CancellationToken cancellationToken) + { + var keycloak = keycloakFactory.CreateKeycloakClient(keycloakInstanceName); + var realm = seedDataHandler.Realm; + var localizations = await keycloak.GetLocaleAsync(realm, cancellationToken: cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + var updateRealmLocalizations = seedDataHandler.RealmLocalizations; + + await UpdateLocaleTranslations(keycloak, realm, localizations, updateRealmLocalizations, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + foreach (var deleteTranslation in + localizations.ExceptBy(updateRealmLocalizations.Select(t => t.Locale), locale => locale)) + { + await keycloak.DeleteLocaleAsync(realm, deleteTranslation, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + } + } + + private static async Task UpdateLocaleTranslations(KeycloakClient keycloak, string realm, IEnumerable locales, + IEnumerable<(string Locale, IEnumerable> Translations)> translations, CancellationToken cancellationToken) + { + if (!await locales + .Join( + translations, + l => l, + trans => trans.Locale, + (l, trans) => (Locale: l, Update: trans)) + .IfAnyAwait(async localesToUpdate => + { + foreach (var (locale, update) in + localesToUpdate) + { + var localizations = await keycloak.GetLocaleAsync(realm, locale, cancellationToken: cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await UpdateLocales(keycloak, realm, cancellationToken, update, localizations, locale).ConfigureAwait(ConfigureAwaitOptions.None); + await DeleteLocales(keycloak, realm, cancellationToken, localizations, update, locale).ConfigureAwait(ConfigureAwaitOptions.None); + } + }).ConfigureAwait(false)) + { + await AddLocales(keycloak, realm, translations, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + } + } + + private static async Task DeleteLocales(KeycloakClient keycloak, string realm, CancellationToken cancellationToken, + IEnumerable> localizations, (string Locale, IEnumerable> Translations) update, string locale) + { + foreach (var deleteTranslation in + localizations.ExceptBy(update.Translations.Select(t => t.Key), + l => l.Key)) + { + await keycloak.DeleteLocaleAsync(realm, locale, deleteTranslation.Key, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + } + } + + private static async Task UpdateLocales(KeycloakClient keycloak, string realm, CancellationToken cancellationToken, + (string Locale, IEnumerable> Translations) update, IEnumerable> localizations, string locale) + { + foreach (var missingTranslation in update.Translations.ExceptBy(localizations.Select(loc => loc.Key), + locModel => locModel.Key)) + { + await keycloak.UpdateLocaleAsync(realm, locale, missingTranslation.Key, missingTranslation.Value, cancellationToken).ConfigureAwait(false); + } + + foreach (var updateTranslation in + localizations.Join( + update.Translations, + l => l.Key, + trans => trans.Key, + (l, trans) => (Key: l.Key, Update: trans))) + { + await keycloak.UpdateLocaleAsync(realm, locale, updateTranslation.Key, updateTranslation.Update.Value, cancellationToken).ConfigureAwait(false); + } + } + + private static async Task AddLocales(KeycloakClient keycloak, string realm, IEnumerable<(string Locale, IEnumerable> Translations)> translations, + CancellationToken cancellationToken) + { + foreach (var translation in translations.SelectMany(x => x.Translations.Select(t => (x.Locale, t.Key, t.Value)))) + { + await keycloak.UpdateLocaleAsync(realm, translation.Locale, translation.Key, translation.Value, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/RealmUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/RealmUpdater.cs index 1d11cc06a6..14f6db8ae8 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/RealmUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/RealmUpdater.cs @@ -25,22 +25,14 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic; -public class RealmUpdater : IRealmUpdater +public class RealmUpdater(IKeycloakFactory keycloakFactory, ISeedDataHandler seedDataHandler) + : IRealmUpdater { - private readonly IKeycloakFactory _keycloakFactory; - private readonly ISeedDataHandler _seedData; - - public RealmUpdater(IKeycloakFactory keycloakFactory, ISeedDataHandler seedDataHandler) - { - _keycloakFactory = keycloakFactory; - _seedData = seedDataHandler; - } - public async Task UpdateRealm(string keycloakInstanceName, CancellationToken cancellationToken) { - var keycloak = _keycloakFactory.CreateKeycloakClient(keycloakInstanceName); - var realm = _seedData.Realm; - var seedRealm = _seedData.KeycloakRealm; + var keycloak = keycloakFactory.CreateKeycloakClient(keycloakInstanceName); + var realm = seedDataHandler.Realm; + var seedRealm = seedDataHandler.KeycloakRealm; Realm keycloakRealm; try @@ -118,6 +110,7 @@ public async Task UpdateRealm(string keycloakInstanceName, CancellationToken can keycloakRealm.AdminEventsEnabled = seedRealm.AdminEventsEnabled; keycloakRealm.AdminEventsDetailsEnabled = seedRealm.AdminEventsDetailsEnabled; keycloakRealm.InternationalizationEnabled = seedRealm.InternationalizationEnabled; + keycloakRealm.DefaultLocale = seedRealm.DefaultLocale; keycloakRealm.SupportedLocales = seedRealm.SupportedLocales; keycloakRealm.BrowserFlow = seedRealm.BrowserFlow; keycloakRealm.RegistrationFlow = seedRealm.RegistrationFlow; @@ -136,6 +129,7 @@ public async Task UpdateRealm(string keycloakInstanceName, CancellationToken can private static bool CompareRealm(Realm keycloakRealm, KeycloakRealm seedRealm) => keycloakRealm._Realm == seedRealm.Realm && keycloakRealm.DisplayName == seedRealm.DisplayName && + keycloakRealm.DefaultLocale == seedRealm.DefaultLocale && keycloakRealm.NotBefore == seedRealm.NotBefore && keycloakRealm.DefaultSignatureAlgorithm == seedRealm.DefaultSignatureAlgorithm && keycloakRealm.RevokeRefreshToken == seedRealm.RevokeRefreshToken && diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/SeedDataHandler.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/SeedDataHandler.cs index 0c45684cf0..49977033d8 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/SeedDataHandler.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/SeedDataHandler.cs @@ -125,6 +125,16 @@ public IReadOnlyDictionary ClientsDictionary get => _keycloakRealm?.ClientScopeMappings?.FilterNotNullValues().Select(x => (x.Key, x.Value)) ?? Enumerable.Empty<(string, IEnumerable)>(); } + public IEnumerable<(string ProviderType, ComponentModel ComponentModel)> RealmComponents + { + get => _keycloakRealm?.Components?.FilterNotNullValues().SelectMany(x => x.Value.Select(v => (x.Key, v))) ?? Enumerable.Empty<(string, ComponentModel)>(); + } + + public IEnumerable<(string Locale, IEnumerable> Translations)> RealmLocalizations + { + get => _keycloakRealm?.LocalizationTexts?.FilterNotNullValues().Select(x => (x.Key, x.Value as IEnumerable>)) ?? Enumerable.Empty<(string, IEnumerable>)>(); + } + public async Task SetClientInternalIds(IAsyncEnumerable<(string ClientId, string Id)> clientInternalIds) { var clientIds = new Dictionary(); diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/UserProfileUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/UserProfileUpdater.cs new file mode 100644 index 0000000000..812abf9692 --- /dev/null +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/UserProfileUpdater.cs @@ -0,0 +1,64 @@ +/******************************************************************************** + * 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 Org.Eclipse.TractusX.Portal.Backend.Keycloak.Factory; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Users; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic; + +public class UserProfileUpdater(IKeycloakFactory keycloakFactory, ISeedDataHandler seedDataHandler) + : IUserProfileUpdater +{ + private const string UserProfileType = "org.keycloak.userprofile.UserProfileProvider"; + private const string UserProfileConfig = "kc.user.profile.config"; + + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + + public async Task UpdateUserProfile(string keycloakInstanceName, CancellationToken cancellationToken) + { + var keycloak = keycloakFactory.CreateKeycloakClient(keycloakInstanceName); + var realm = seedDataHandler.Realm; + var userProfiles = seedDataHandler.RealmComponents.Where(x => x.ProviderType == UserProfileType); + + var userProfile = await keycloak.GetUsersProfile(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + + if (userProfiles.Count() != 1 || !(userProfiles.Single().ComponentModel.Config?.TryGetValue(UserProfileConfig, out var configs) ?? false) || configs?.Count() != 1) + { + throw new ConflictException("There must be exactly one user profile"); + } + + var update = JsonSerializer.Deserialize(configs.Single(), JsonOptions); + if (update is null) + { + return; + } + + if (!userProfile.Equals(update)) + { + await keycloak.UpdateUsersProfile(realm, update, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + } + } +} diff --git a/src/keycloak/Keycloak.Seeding/Models/KeycloakRealm.cs b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealm.cs index 1d2fbc0309..3546001a98 100644 --- a/src/keycloak/Keycloak.Seeding/Models/KeycloakRealm.cs +++ b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealm.cs @@ -82,6 +82,7 @@ public class KeycloakRealm public int? OtpPolicyPeriod { get; set; } public IEnumerable? OtpSupportedApplications { get; set; } public string? PasswordPolicy { get; set; } + public IDictionary?>? LocalizationTexts { get; set; } public string? WebAuthnPolicyRpEntityName { get; set; } public IEnumerable? WebAuthnPolicySignatureAlgorithms { get; set; } public string? WebAuthnPolicyRpId { get; set; } @@ -122,7 +123,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 IDictionary?>? Components { get; set; } public bool? InternationalizationEnabled { get; set; } public IEnumerable? SupportedLocales { get; set; } public string? DefaultLocale { get; set; } diff --git a/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmExtensions.cs b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmExtensions.cs index 32227e4d91..3729e48118 100644 --- a/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmExtensions.cs +++ b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmExtensions.cs @@ -85,6 +85,7 @@ public static KeycloakRealm Merge(this KeycloakRealm left, KeycloakRealm right) OtpPolicyPeriod = right.OtpPolicyPeriod ?? left.OtpPolicyPeriod, OtpSupportedApplications = right.OtpSupportedApplications ?? left.OtpSupportedApplications, PasswordPolicy = right.PasswordPolicy ?? left.PasswordPolicy, + LocalizationTexts = right.LocalizationTexts ?? left.LocalizationTexts, WebAuthnPolicyRpEntityName = right.WebAuthnPolicyRpEntityName ?? left.WebAuthnPolicyRpEntityName, WebAuthnPolicySignatureAlgorithms = right.WebAuthnPolicySignatureAlgorithms ?? left.WebAuthnPolicySignatureAlgorithms, WebAuthnPolicyRpId = right.WebAuthnPolicyRpId ?? left.WebAuthnPolicyRpId, diff --git a/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettings.cs b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettings.cs index 8c1a8592ee..5ac852d022 100644 --- a/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettings.cs +++ b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettings.cs @@ -94,6 +94,7 @@ public class KeycloakRealmSettings [DistinctValues] public IEnumerable? OtpSupportedApplications { get; set; } public string? PasswordPolicy { get; set; } + public IDictionary?>? LocalizationTexts { get; set; } public string? WebAuthnPolicyRpEntityName { get; set; } [DistinctValues] public IEnumerable? WebAuthnPolicySignatureAlgorithms { get; set; } diff --git a/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettingsExtentions.cs b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettingsExtentions.cs index a29dc5f4bc..932badf53e 100644 --- a/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettingsExtentions.cs +++ b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettingsExtentions.cs @@ -86,6 +86,7 @@ public static KeycloakRealm ToModel(this KeycloakRealmSettings keycloakRealmSett OtpPolicyPeriod = keycloakRealmSettings.OtpPolicyPeriod, OtpSupportedApplications = keycloakRealmSettings.OtpSupportedApplications, PasswordPolicy = keycloakRealmSettings.PasswordPolicy, + LocalizationTexts = keycloakRealmSettings.LocalizationTexts, WebAuthnPolicyRpEntityName = keycloakRealmSettings.WebAuthnPolicyRpEntityName, WebAuthnPolicySignatureAlgorithms = keycloakRealmSettings.WebAuthnPolicySignatureAlgorithms, WebAuthnPolicyRpId = keycloakRealmSettings.WebAuthnPolicyRpId, @@ -269,6 +270,7 @@ private static ScopeMappingModel ToModel(ScopeMappingSettings scopeMappingSettin 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"), diff --git a/src/keycloak/Keycloak.Seeding/Program.cs b/src/keycloak/Keycloak.Seeding/Program.cs index 2e1f96ff1c..d50f5cf883 100644 --- a/src/keycloak/Keycloak.Seeding/Program.cs +++ b/src/keycloak/Keycloak.Seeding/Program.cs @@ -45,6 +45,8 @@ .AddTransient() .AddTransient() .AddTransient() + .AddTransient() + .AddTransient() .AddTransient() .ConfigureKeycloakSettingsMap(hostContext.Configuration.GetSection("Keycloak")) .AddTransient()