diff --git a/.tractusx b/.tractusx index 4717e14410..3472b596cb 100644 --- a/.tractusx +++ b/.tractusx @@ -19,8 +19,8 @@ leadingRepository: "https://github.com/eclipse-tractusx/portal" openApiSpecs: -- "https://raw.githubusercontent.com/eclipse-tractusx/portal-backend/refs/tags/v2.3.0-RC3/docs/api/administration-service.yaml" -- "https://raw.githubusercontent.com/eclipse-tractusx/portal-backend/refs/tags/v2.3.0-RC3/docs/api/apps-service.yaml" -- "https://raw.githubusercontent.com/eclipse-tractusx/portal-backend/refs/tags/v2.3.0-RC3/docs/api/notifications-service.yaml" -- "https://raw.githubusercontent.com/eclipse-tractusx/portal-backend/refs/tags/v2.3.0-RC3/docs/api/registration-service.yaml" -- "https://raw.githubusercontent.com/eclipse-tractusx/portal-backend/refs/tags/v2.3.0-RC3/docs/api/services-service.yaml" +- "https://raw.githubusercontent.com/eclipse-tractusx/portal-backend/refs/tags/v2.3.0-RC4/docs/api/administration-service.yaml" +- "https://raw.githubusercontent.com/eclipse-tractusx/portal-backend/refs/tags/v2.3.0-RC4/docs/api/apps-service.yaml" +- "https://raw.githubusercontent.com/eclipse-tractusx/portal-backend/refs/tags/v2.3.0-RC4/docs/api/notifications-service.yaml" +- "https://raw.githubusercontent.com/eclipse-tractusx/portal-backend/refs/tags/v2.3.0-RC4/docs/api/registration-service.yaml" +- "https://raw.githubusercontent.com/eclipse-tractusx/portal-backend/refs/tags/v2.3.0-RC4/docs/api/services-service.yaml" diff --git a/CHANGELOG.md b/CHANGELOG.md index 8238223992..bfd02bba64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ New features, fixed bugs, known defects and other noteworthy changes to each rel ## Unreleased +## 2.3.0-RC4 + +### Change + +* **Keycloak realm seeding job**: made seeder configurable [#1174](https://github.com/eclipse-tractusx/portal-backend/pull/1174) + ## 2.3.0-RC3 ### Technical Support diff --git a/docs/api/administration-service.yaml b/docs/api/administration-service.yaml index 7307356085..438a8cd4ac 100644 --- a/docs/api/administration-service.yaml +++ b/docs/api/administration-service.yaml @@ -1,7 +1,7 @@ openapi: 3.0.1 info: title: Org.Eclipse.TractusX.Portal.Backend.Administration.Service - version: v2.3.0-RC3 + version: v2.3.0-RC4 paths: /api/administration/companydata/ownCompanyDetails: get: diff --git a/docs/api/apps-service.yaml b/docs/api/apps-service.yaml index f3f08e4eab..3877dccc8f 100644 --- a/docs/api/apps-service.yaml +++ b/docs/api/apps-service.yaml @@ -1,7 +1,7 @@ openapi: 3.0.1 info: title: Org.Eclipse.TractusX.Portal.Backend.Apps.Service - version: v2.3.0-RC3 + version: v2.3.0-RC4 paths: '/api/apps/AppChange/{appId}/role/activeapp': post: diff --git a/docs/api/notifications-service.yaml b/docs/api/notifications-service.yaml index 2bc85ce85d..269763bff8 100644 --- a/docs/api/notifications-service.yaml +++ b/docs/api/notifications-service.yaml @@ -1,7 +1,7 @@ openapi: 3.0.1 info: title: Org.Eclipse.TractusX.Portal.Backend.Notifications.Service - version: v2.3.0-RC3 + version: v2.3.0-RC4 paths: /api/notification/errormessage: get: diff --git a/docs/api/registration-service.yaml b/docs/api/registration-service.yaml index ed6898922f..64ad9c4d99 100644 --- a/docs/api/registration-service.yaml +++ b/docs/api/registration-service.yaml @@ -1,7 +1,7 @@ openapi: 3.0.1 info: title: Org.Eclipse.TractusX.Portal.Backend.Registration.Service - version: v2.3.0-RC3 + version: v2.3.0-RC4 paths: /api/registration/errormessage: get: diff --git a/docs/api/services-service.yaml b/docs/api/services-service.yaml index 23fb44d3df..56597210ea 100644 --- a/docs/api/services-service.yaml +++ b/docs/api/services-service.yaml @@ -1,7 +1,7 @@ openapi: 3.0.1 info: title: Org.Eclipse.TractusX.Portal.Backend.Services.Service - version: v2.3.0-RC3 + version: v2.3.0-RC4 paths: /api/services/errormessage: get: diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 7df3464fe3..b1a975343f 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -20,6 +20,6 @@ 2.3.0 - RC3 + RC4 diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/AuthenticationFlowsUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/AuthenticationFlowsUpdater.cs index af6f2d65f4..0c9dda7aa7 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/AuthenticationFlowsUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/AuthenticationFlowsUpdater.cs @@ -23,81 +23,76 @@ 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.AuthenticationManagement; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Extensions; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; using System.Collections.Immutable; namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic; -public class AuthenticationFlowsUpdater : IAuthenticationFlowsUpdater +public class AuthenticationFlowsUpdater(IKeycloakFactory keycloakFactory, ISeedDataHandler seedDataHandler) + : IAuthenticationFlowsUpdater { - private readonly IKeycloakFactory _keycloakFactory; - private readonly ISeedDataHandler _seedData; - - public AuthenticationFlowsUpdater(IKeycloakFactory keycloakFactory, ISeedDataHandler seedDataHandler) - { - _keycloakFactory = keycloakFactory; - _seedData = seedDataHandler; - } - public Task UpdateAuthenticationFlows(string keycloakInstanceName, CancellationToken cancellationToken) { - var keycloak = _keycloakFactory.CreateKeycloakClient(keycloakInstanceName); + var keycloak = keycloakFactory.CreateKeycloakClient(keycloakInstanceName); - var handler = new AuthenticationFlowHandler(keycloak, _seedData); + var handler = new AuthenticationFlowHandler(keycloak, seedDataHandler); return handler.UpdateAuthenticationFlows(cancellationToken); } - private sealed class AuthenticationFlowHandler + private sealed class AuthenticationFlowHandler(KeycloakClient keycloak, ISeedDataHandler seedDataHandler) { - private readonly string _realm; - private readonly KeycloakClient _keycloak; - private readonly ISeedDataHandler _seedData; - - public AuthenticationFlowHandler(KeycloakClient keycloak, ISeedDataHandler seedData) - { - _keycloak = keycloak; - _seedData = seedData; - _realm = seedData.Realm; - } + private readonly string _realm = seedDataHandler.Realm; public async Task UpdateAuthenticationFlows(CancellationToken cancellationToken) { - var flows = await _keycloak.GetAuthenticationFlowsAsync(_realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - var seedFlows = _seedData.TopLevelCustomAuthenticationFlows; + var flows = await keycloak.GetAuthenticationFlowsAsync(_realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + var seedFlows = seedDataHandler.TopLevelCustomAuthenticationFlows; var topLevelCustomFlows = flows.Where(flow => !(flow.BuiltIn ?? false) && (flow.TopLevel ?? false)); + var seederConfiguration = seedDataHandler.GetSpecificConfiguration(ConfigurationKey.AuthenticationFlows); + var authFlowExecutionConfig = seedDataHandler.GetSpecificConfiguration(ConfigurationKey.AuthenticationFlowExecution); + var authenticatorConfig = seedDataHandler.GetSpecificConfiguration(ConfigurationKey.AuthenticatorConfig); - await DeleteRedundantAuthenticationFlows(topLevelCustomFlows, seedFlows, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await AddMissingAuthenticationFlows(topLevelCustomFlows, seedFlows, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await UpdateExistingAuthenticationFlows(topLevelCustomFlows, seedFlows, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await DeleteRedundantAuthenticationFlows(topLevelCustomFlows, seedFlows, seederConfiguration, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await AddMissingAuthenticationFlows(topLevelCustomFlows, seedFlows, seederConfiguration, authenticatorConfig, authFlowExecutionConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await UpdateExistingAuthenticationFlows(topLevelCustomFlows, seedFlows, seederConfiguration, authenticatorConfig, authFlowExecutionConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } - private async Task DeleteRedundantAuthenticationFlows(IEnumerable topLevelCustomFlows, IEnumerable seedFlows, CancellationToken cancellationToken) + private async Task DeleteRedundantAuthenticationFlows(IEnumerable topLevelCustomFlows, IEnumerable seedFlows, KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { - foreach (var delete in topLevelCustomFlows.ExceptBy(seedFlows.Select(x => x.Alias), x => x.Alias)) + foreach (var delete in topLevelCustomFlows + .Where(x => seederConfig.ModificationAllowed(ModificationType.Delete, x.Alias)) + .ExceptBy(seedFlows.Select(x => x.Alias), x => x.Alias)) { if (delete.Id == null) throw new ConflictException($"authenticationFlow.id is null {delete.Alias} {delete.Description}"); - await _keycloak.DeleteAuthenticationFlowAsync(_realm, delete.Id, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await keycloak.DeleteAuthenticationFlowAsync(_realm, delete.Id, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } } - private async Task AddMissingAuthenticationFlows(IEnumerable topLevelCustomFlows, IEnumerable seedFlows, CancellationToken cancellationToken) + private async Task AddMissingAuthenticationFlows(IEnumerable topLevelCustomFlows, IEnumerable seedFlows, KeycloakSeederConfigModel seederConfig, KeycloakSeederConfigModel authenticatorConfig, KeycloakSeederConfigModel authFlowExecutionConfig, CancellationToken cancellationToken) { - foreach (var addFlow in seedFlows.ExceptBy(topLevelCustomFlows.Select(x => x.Alias), x => x.Alias)) + foreach (var addFlow in seedFlows + .ExceptBy(topLevelCustomFlows.Select(x => x.Alias), x => x.Alias)) { if (addFlow.Alias == null) throw new ConflictException($"authenticationFlow.Alias is null {addFlow.Id} {addFlow.Description}"); if (addFlow.BuiltIn ?? false) throw new ConflictException($"authenticationFlow.buildIn is true. flow cannot be added: {addFlow.Alias}"); - await _keycloak.CreateAuthenticationFlowAsync(_realm, CreateUpdateAuthenticationFlow(null, addFlow), cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await UpdateAuthenticationFlowExecutions(addFlow.Alias, cancellationToken); + if (seederConfig.ModificationAllowed(ModificationType.Create, addFlow.Alias)) + { + await keycloak.CreateAuthenticationFlowAsync(_realm, CreateUpdateAuthenticationFlow(null, addFlow), cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + } + + await UpdateAuthenticationFlowExecutions(addFlow.Alias, authenticatorConfig, authFlowExecutionConfig, cancellationToken); } } - private async Task UpdateExistingAuthenticationFlows(IEnumerable topLevelCustomFlows, IEnumerable seedFlows, CancellationToken cancellationToken) + private async Task UpdateExistingAuthenticationFlows(IEnumerable topLevelCustomFlows, IEnumerable seedFlows, KeycloakSeederConfigModel seederConfig, KeycloakSeederConfigModel authenticatorConfig, KeycloakSeederConfigModel authFlowExecutionConfig, CancellationToken cancellationToken) { foreach (var (flow, seed) in topLevelCustomFlows + .Where(x => seederConfig.ModificationAllowed(ModificationType.Update, x.Alias)) .Join( seedFlows, x => x.Alias, @@ -110,13 +105,14 @@ private async Task UpdateExistingAuthenticationFlows(IEnumerable new AuthenticationFlow + private static AuthenticationFlow CreateUpdateAuthenticationFlow(string? id, AuthenticationFlowModel update) => new() { Id = id, Alias = update.Alias, @@ -132,18 +128,19 @@ private static bool CompareAuthenticationFlow(AuthenticationFlow flow, Authentic flow.ProviderId == update.ProviderId && flow.TopLevel == update.TopLevel; - private async Task UpdateAuthenticationFlowExecutions(string alias, CancellationToken cancellationToken) + private async Task UpdateAuthenticationFlowExecutions(string alias, KeycloakSeederConfigModel authenticatorConfig, KeycloakSeederConfigModel authFlowExecutionConfig, CancellationToken cancellationToken) { - var updateExecutions = _seedData.GetAuthenticationExecutions(alias); + var updateExecutions = seedDataHandler.GetAuthenticationExecutions(alias); var executionNodes = ExecutionNode.Parse(await GetExecutions(alias, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)); if (!CompareStructureRecursive(executionNodes, updateExecutions)) { - await DeleteExecutionsRecursive(executionNodes, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await AddExecutionsRecursive(alias, updateExecutions, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await DeleteExecutionsRecursive(executionNodes, authFlowExecutionConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await AddExecutionsRecursive(alias, updateExecutions, authFlowExecutionConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); executionNodes = ExecutionNode.Parse(await GetExecutions(alias, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)); } - await UpdateExecutionsRecursive(alias, executionNodes, updateExecutions, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + + await UpdateExecutionsRecursive(alias, executionNodes, updateExecutions, authenticatorConfig, authFlowExecutionConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } private bool CompareStructureRecursive(IReadOnlyList executions, IEnumerable updateExecutions) => @@ -153,44 +150,45 @@ private bool CompareStructureRecursive(IReadOnlyList executions, (execution, update) => (Node: execution, Update: update)).All( x => (x.Node.Execution.AuthenticationFlow ?? false) == (x.Update.AuthenticatorFlow ?? false) && - (!(x.Node.Execution.AuthenticationFlow ?? false) || CompareStructureRecursive(x.Node.Children, _seedData.GetAuthenticationExecutions(x.Update.FlowAlias)))); + (!(x.Node.Execution.AuthenticationFlow ?? false) || CompareStructureRecursive(x.Node.Children, seedDataHandler.GetAuthenticationExecutions(x.Update.FlowAlias)))); - private async Task DeleteExecutionsRecursive(IEnumerable executionNodes, CancellationToken cancellationToken) + private async Task DeleteExecutionsRecursive(IEnumerable executionNodes, KeycloakSeederConfigModel authFlowExecutionConfig, CancellationToken cancellationToken) { - foreach (var executionNode in executionNodes) + foreach (var executionNode in executionNodes.Where(x => authFlowExecutionConfig.ModificationAllowed(ModificationType.Delete, x.Execution.Id))) { if (executionNode.Execution.AuthenticationFlow ?? false) { - await DeleteExecutionsRecursive(executionNode.Children, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await DeleteExecutionsRecursive(executionNode.Children, authFlowExecutionConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } - await _keycloak.DeleteAuthenticationExecutionAsync(_realm, executionNode.Execution.Id ?? throw new ConflictException("authenticationFlow.Id is null"), cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + + await keycloak.DeleteAuthenticationExecutionAsync(_realm, executionNode.Execution.Id ?? throw new ConflictException("authenticationFlow.Id is null"), cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } } - private async Task AddExecutionsRecursive(string? alias, IEnumerable seedExecutions, CancellationToken cancellationToken) + private async Task AddExecutionsRecursive(string? alias, IEnumerable seedExecutions, KeycloakSeederConfigModel authFlowExecutionConfig, CancellationToken cancellationToken) { - foreach (var execution in seedExecutions) + foreach (var execution in seedExecutions.Where(x => authFlowExecutionConfig.ModificationAllowed(ModificationType.Create, x.FlowAlias))) { await (execution.AuthenticatorFlow switch { true => AddAuthenticationFlowExecutionRecursive(alias!, execution, cancellationToken), - _ => _keycloak.AddAuthenticationFlowExecutionAsync(_realm, alias!, CreateDataWithProvider(execution), cancellationToken) + _ => keycloak.AddAuthenticationFlowExecutionAsync(_realm, alias!, CreateDataWithProvider(execution), cancellationToken) }).ConfigureAwait(ConfigureAwaitOptions.None); } - async Task AddAuthenticationFlowExecutionRecursive(string alias, AuthenticationExecutionModel execution, CancellationToken cancellationToken) + async Task AddAuthenticationFlowExecutionRecursive(string updateAlias, AuthenticationExecutionModel execution, CancellationToken ct) { - await _keycloak.AddAuthenticationFlowAndExecutionToAuthenticationFlowAsync(_realm, alias, CreateDataWithAliasTypeProviderDescription(execution), cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await AddExecutionsRecursive(execution.FlowAlias, _seedData.GetAuthenticationExecutions(execution.FlowAlias), cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await keycloak.AddAuthenticationFlowAndExecutionToAuthenticationFlowAsync(_realm, updateAlias, CreateDataWithAliasTypeProviderDescription(execution), ct).ConfigureAwait(ConfigureAwaitOptions.None); + await AddExecutionsRecursive(execution.FlowAlias, seedDataHandler.GetAuthenticationExecutions(execution.FlowAlias), authFlowExecutionConfig, ct).ConfigureAwait(ConfigureAwaitOptions.None); } } - private async Task UpdateExecutionsRecursive(string alias, IReadOnlyList executionNodes, IEnumerable seedExecutions, CancellationToken cancellationToken) + private async Task UpdateExecutionsRecursive(string alias, IReadOnlyCollection executionNodes, IEnumerable seedExecutions, KeycloakSeederConfigModel authenticatorConfig, KeycloakSeederConfigModel authFlowExecutionConfig, CancellationToken cancellationToken) { if (executionNodes.Count != seedExecutions.Count()) throw new ArgumentException("number of elements in executionNodes doesn't match seedData"); - foreach (var (executionNode, update) in executionNodes.Zip(seedExecutions)) + foreach (var (executionNode, update) in executionNodes.Zip(seedExecutions).Where(x => authFlowExecutionConfig.ModificationAllowed(ModificationType.Update, x.First.Execution.Id))) { if ((executionNode.Execution.AuthenticationFlow ?? false) != (update.AuthenticatorFlow ?? false)) throw new ArgumentException("execution.AuthenticatorFlow doesn't match seedData"); @@ -202,19 +200,19 @@ private async Task UpdateExecutionsRecursive(string alias, IReadOnlyList - execution.Description == _seedData.GetAuthenticationFlow(update.FlowAlias).Description && + execution.Description == seedDataHandler.GetAuthenticationFlow(update.FlowAlias).Description && execution.DisplayName == update.FlowAlias && execution.Requirement == update.Requirement; @@ -317,8 +317,8 @@ private bool CompareFlowExecutions(AuthenticationFlowExecution execution, Authen private async Task<(bool, AuthenticatorConfig?)> CompareAuthenticationConfig(string authenticatorConfigId, string authenticatorConfigAlias, CancellationToken cancellationToken) { - var config = await _keycloak.GetAuthenticatorConfigurationAsync(_realm, authenticatorConfigId, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - var update = _seedData.GetAuthenticatorConfig(authenticatorConfigAlias); + var config = await keycloak.GetAuthenticatorConfigurationAsync(_realm, authenticatorConfigId, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + var update = seedDataHandler.GetAuthenticatorConfig(authenticatorConfigAlias); return (CompareAuthenticatorConfig(config, update), config); } @@ -327,11 +327,11 @@ private static bool CompareAuthenticatorConfig(AuthenticatorConfig config, Authe config.Config.NullOrContentEqual(update.Config?.FilterNotNullValues()); private Task> GetExecutions(string alias, CancellationToken cancellationToken) => - _keycloak.GetAuthenticationFlowExecutionsAsync(_realm, alias, cancellationToken); + keycloak.GetAuthenticationFlowExecutionsAsync(_realm, alias, cancellationToken); private IDictionary CreateDataWithAliasTypeProviderDescription(AuthenticationExecutionModel execution) { - var seedFlow = _seedData.GetAuthenticationFlow(execution.FlowAlias); + var seedFlow = seedDataHandler.GetAuthenticationFlow(execution.FlowAlias); return new Dictionary { { "alias", execution.FlowAlias ?? throw new ConflictException($"authenticationExecution.FlowAlias is null: {seedFlow.Alias}")}, { "description", seedFlow.Description ?? throw new ConflictException($"authenticationFlow.ProviderId is null: {seedFlow.Alias}")}, diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientScopeMapperUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientScopeMapperUpdater.cs index bb74869231..1593d9fb1e 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientScopeMapperUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientScopeMapperUpdater.cs @@ -22,27 +22,22 @@ 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; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Extensions; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic; -public class ClientScopeMapperUpdater : IClientScopeMapperUpdater +public class ClientScopeMapperUpdater(IKeycloakFactory keycloakFactory, ISeedDataHandler seedDataHandler) + : IClientScopeMapperUpdater { - private readonly IKeycloakFactory _keycloakFactory; - private readonly ISeedDataHandler _seedData; - - public ClientScopeMapperUpdater(IKeycloakFactory keycloakFactory, ISeedDataHandler seedDataHandler) - { - _keycloakFactory = keycloakFactory; - _seedData = seedDataHandler; - } - public async Task UpdateClientScopeMapper(string instanceName, CancellationToken cancellationToken) { - var keycloak = _keycloakFactory.CreateKeycloakClient(instanceName); - var realm = _seedData.Realm; + var keycloak = keycloakFactory.CreateKeycloakClient(instanceName); + var realm = seedDataHandler.Realm; + var seederConfig = seedDataHandler.GetSpecificConfiguration(ConfigurationKey.ClientScopes); var clients = await keycloak.GetClientsAsync(realm, null, true, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - foreach (var (clientName, mappingModels) in _seedData.ClientScopeMappings) + foreach (var (clientName, mappingModels) in seedDataHandler.ClientScopeMappings) { var client = clients.SingleOrDefault(x => x.ClientId == clientName); if (client?.Id is null) @@ -60,17 +55,23 @@ public async Task UpdateClientScopeMapper(string instanceName, CancellationToken } 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")) ?? Enumerable.Empty(); - await AddAndDeleteRoles(keycloak, realm, clientScope.Id, client.Id, clientRoles, mappingModelRoles, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await AddAndDeleteRoles(keycloak, realm, clientScope.Id, client.Id, clientRoles, mappingModelRoles, seederConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } } } - private static async Task AddAndDeleteRoles(KeycloakClient keycloak, string realm, string clientScopeId, string clientId, IEnumerable roles, IEnumerable updateRoles, CancellationToken cancellationToken) + private static async Task AddAndDeleteRoles(KeycloakClient keycloak, string realm, string clientScopeId, string clientId, IEnumerable roles, IEnumerable updateRoles, KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { - await updateRoles.ExceptBy(roles.Select(role => role.Name), roleModel => roleModel.Name).IfAnyAwait(rolesToAdd => - keycloak.AddClientRolesScopeMappingToClientAsync(realm, clientScopeId, clientId, rolesToAdd, cancellationToken)).ConfigureAwait(false); + await updateRoles + .Where(x => seederConfig.ModificationAllowed(ModificationType.Create, x.Name)) + .ExceptBy(roles.Select(role => role.Name), roleModel => roleModel.Name) + .IfAnyAwait(rolesToAdd => + keycloak.AddClientRolesScopeMappingToClientAsync(realm, clientScopeId, clientId, rolesToAdd, cancellationToken)).ConfigureAwait(false); - await roles.ExceptBy(updateRoles.Select(roleModel => roleModel.Name), role => role.Name).IfAnyAwait(rolesToDelete => - keycloak.RemoveClientRolesFromClientScopeForClientAsync(realm, clientScopeId, clientId, rolesToDelete, cancellationToken)).ConfigureAwait(false); + await roles + .Where(x => seederConfig.ModificationAllowed(ModificationType.Delete, x.Name)) + .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 6419a4f844..a9ecfef54d 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientScopesUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientScopesUpdater.cs @@ -22,37 +22,38 @@ using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.ClientScopes; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.ProtocolMappers; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Extensions; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic; -public class ClientScopesUpdater : IClientScopesUpdater +public class ClientScopesUpdater(IKeycloakFactory keycloakFactory, ISeedDataHandler seedDataHandler) + : IClientScopesUpdater { - private readonly IKeycloakFactory _keycloakFactory; - private readonly ISeedDataHandler _seedData; - - public ClientScopesUpdater(IKeycloakFactory keycloakFactory, ISeedDataHandler seedDataHandler) - { - _keycloakFactory = keycloakFactory; - _seedData = seedDataHandler; - } - public async Task UpdateClientScopes(string instanceName, CancellationToken cancellationToken) { - var keycloak = _keycloakFactory.CreateKeycloakClient(instanceName); - var realm = _seedData.Realm; + var keycloak = keycloakFactory.CreateKeycloakClient(instanceName); + var realm = seedDataHandler.Realm; + var seederConfig = seedDataHandler.GetSpecificConfiguration(ConfigurationKey.ClientScopes); var clientScopes = await keycloak.GetClientScopesAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - var seedClientScopes = _seedData.ClientScopes; + var seedClientScopes = seedDataHandler.ClientScopes; - await RemoveObsoleteClientScopes(keycloak, realm, clientScopes, seedClientScopes, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await CreateMissingClientScopes(keycloak, realm, clientScopes, seedClientScopes, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await UpdateExistingClientScopes(keycloak, realm, clientScopes, seedClientScopes, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await CheckAndExecute(ModificationType.Delete, keycloak, realm, clientScopes, seedClientScopes, seederConfig, cancellationToken, RemoveObsoleteClientScopes).ConfigureAwait(ConfigureAwaitOptions.None); + await CheckAndExecute(ModificationType.Create, keycloak, realm, clientScopes, seedClientScopes, seederConfig, cancellationToken, CreateMissingClientScopes).ConfigureAwait(ConfigureAwaitOptions.None); + await CheckAndExecute(ModificationType.Update, keycloak, realm, clientScopes, seedClientScopes, seederConfig, cancellationToken, UpdateExistingClientScopes).ConfigureAwait(ConfigureAwaitOptions.None); } - private static async Task RemoveObsoleteClientScopes(KeycloakClient keycloak, string realm, IEnumerable clientScopes, IEnumerable seedClientScopes, CancellationToken cancellationToken) + private static Task CheckAndExecute(ModificationType modificationType, KeycloakClient keycloak, string realm, IEnumerable clientScopes, IEnumerable seedClientScopes, KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken, Func, IEnumerable, KeycloakSeederConfigModel, CancellationToken, Task> executeLogic) => + seederConfig.ModificationAllowed(modificationType) + ? executeLogic(keycloak, realm, clientScopes, seedClientScopes, seederConfig, cancellationToken) + : Task.CompletedTask; + + private static async Task RemoveObsoleteClientScopes(KeycloakClient keycloak, string realm, IEnumerable clientScopes, IEnumerable seedClientScopes, KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { - foreach (var deleteScope in clientScopes.ExceptBy(seedClientScopes.Select(x => x.Name), x => x.Name)) + foreach (var deleteScope in clientScopes + .Where(x => seederConfig.ModificationAllowed(ModificationType.Delete, x.Name)) + .ExceptBy(seedClientScopes.Select(x => x.Name), x => x.Name)) { await keycloak.DeleteClientScopeAsync( realm, @@ -61,32 +62,38 @@ await keycloak.DeleteClientScopeAsync( } } - private static async Task CreateMissingClientScopes(KeycloakClient keycloak, string realm, IEnumerable clientScopes, IEnumerable seedClientScopes, CancellationToken cancellationToken) + private static async Task CreateMissingClientScopes(KeycloakClient keycloak, string realm, IEnumerable clientScopes, IEnumerable seedClientScopes, KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { - foreach (var addScope in seedClientScopes.ExceptBy(clientScopes.Select(x => x.Name), x => x.Name)) + foreach (var addScope in seedClientScopes + .Where(x => seederConfig.ModificationAllowed(ModificationType.Create, x.Name)) + .ExceptBy(clientScopes.Select(x => x.Name), x => x.Name)) { await keycloak.CreateClientScopeAsync(realm, CreateClientScope(null, addScope, true), cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } } - private static async Task UpdateExistingClientScopes(KeycloakClient keycloak, string realm, IEnumerable clientScopes, IEnumerable seedClientScopes, CancellationToken cancellationToken) + private static async Task UpdateExistingClientScopes(KeycloakClient keycloak, string realm, IEnumerable clientScopes, IEnumerable seedClientScopes, KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { foreach (var (clientScope, update) in clientScopes + .Where(x => seederConfig.ModificationAllowed(ModificationType.Update, x.Name)) .Join( seedClientScopes, x => x.Name, x => x.Name, (clientScope, update) => (ClientScope: clientScope, Update: update))) { - await UpdateClientScopeWithProtocolMappers(keycloak, realm, clientScope, update, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await UpdateClientScopeWithProtocolMappers(keycloak, realm, clientScope, update, seederConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } } - private static async Task UpdateClientScopeWithProtocolMappers(KeycloakClient keycloak, string realm, ClientScope clientScope, ClientScopeModel update, CancellationToken cancellationToken) + private static async Task UpdateClientScopeWithProtocolMappers(KeycloakClient keycloak, string realm, ClientScope clientScope, ClientScopeModel update, KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { if (clientScope.Id == null) throw new ConflictException($"clientScope.Id is null: {clientScope.Name}"); + if (clientScope.Name == null) + throw new ConflictException($"clientScope.Name is null: {clientScope.Name}"); + if (!CompareClientScope(clientScope, update)) { await keycloak.UpdateClientScopeAsync( @@ -99,14 +106,16 @@ await keycloak.UpdateClientScopeAsync( var mappers = clientScope.ProtocolMappers ?? Enumerable.Empty(); var updateMappers = update.ProtocolMappers ?? Enumerable.Empty(); - await DeleteObsoleteProtocolMappers(keycloak, realm, clientScope.Id, mappers, updateMappers, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await CreateMissingProtocolMappers(keycloak, realm, clientScope.Id, mappers, updateMappers, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await UpdateExistingProtocolMappers(keycloak, realm, clientScope.Id, mappers, updateMappers, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await DeleteObsoleteProtocolMappers(keycloak, realm, clientScope.Name, clientScope.Id, mappers, updateMappers, seederConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await CreateMissingProtocolMappers(keycloak, realm, clientScope.Name, clientScope.Id, mappers, updateMappers, seederConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await UpdateExistingProtocolMappers(keycloak, realm, clientScope.Name, clientScope.Id, mappers, updateMappers, seederConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } - private static async Task DeleteObsoleteProtocolMappers(KeycloakClient keycloak, string realm, string clientScopeId, IEnumerable mappers, IEnumerable updateMappers, CancellationToken cancellationToken) + private static async Task DeleteObsoleteProtocolMappers(KeycloakClient keycloak, string realm, string clientScopeName, string clientScopeId, IEnumerable mappers, IEnumerable updateMappers, KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { - foreach (var mapper in mappers.ExceptBy(updateMappers.Select(x => x.Name), x => x.Name)) + foreach (var mapper in mappers + .Where(x => seederConfig.ModificationAllowed(clientScopeName, ConfigurationKey.ProtocolMappers, ModificationType.Delete, x.Name)) + .ExceptBy(updateMappers.Select(x => x.Name), x => x.Name)) { await keycloak.DeleteProtocolMapperAsync( realm, @@ -116,9 +125,11 @@ await keycloak.DeleteProtocolMapperAsync( } } - private static async Task CreateMissingProtocolMappers(KeycloakClient keycloak, string realm, string clientScopeId, IEnumerable mappers, IEnumerable updateMappers, CancellationToken cancellationToken) + private static async Task CreateMissingProtocolMappers(KeycloakClient keycloak, string realm, string clientScopeName, string clientScopeId, IEnumerable mappers, IEnumerable updateMappers, KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { - foreach (var update in updateMappers.ExceptBy(mappers.Select(x => x.Name), x => x.Name)) + foreach (var update in updateMappers + .Where(x => seederConfig.ModificationAllowed(clientScopeName, ConfigurationKey.ProtocolMappers, ModificationType.Create, x.Name)) + .ExceptBy(mappers.Select(x => x.Name), x => x.Name)) { await keycloak.CreateProtocolMapperAsync( realm, @@ -128,13 +139,15 @@ await keycloak.CreateProtocolMapperAsync( } } - private static async Task UpdateExistingProtocolMappers(KeycloakClient keycloak, string realm, string clientScopeId, IEnumerable mappers, IEnumerable updateMappers, CancellationToken cancellationToken) + private static async Task UpdateExistingProtocolMappers(KeycloakClient keycloak, string realm, string clientScopeName, string clientScopeId, IEnumerable mappers, IEnumerable updateMappers, KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { - foreach (var (mapper, update) in mappers.Join( - updateMappers, - x => x.Name, - x => x.Name, - (mapper, update) => (Mapper: mapper, Update: update)) + foreach (var (mapper, update) in mappers + .Where(x => seederConfig.ModificationAllowed(clientScopeName, ConfigurationKey.ProtocolMappers, ModificationType.Update, x.Name)) + .Join( + updateMappers, + x => x.Name, + x => x.Name, + (mapper, update) => (Mapper: mapper, Update: update)) .Where(x => !ProtocolMappersUpdater.CompareProtocolMapper(x.Mapper, x.Update))) { await keycloak.UpdateProtocolMapperAsync( diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientsUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientsUpdater.cs index 92582fd248..9e7b9a11c6 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientsUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/ClientsUpdater.cs @@ -24,35 +24,31 @@ using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Clients; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.ProtocolMappers; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.RealmsAdmin; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Extensions; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; using System.Runtime.CompilerServices; namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic; -public class ClientsUpdater : IClientsUpdater +public class ClientsUpdater(IKeycloakFactory keycloakFactory, ISeedDataHandler seedDataHandler) + : IClientsUpdater { - private readonly IKeycloakFactory _keycloakFactory; - private readonly ISeedDataHandler _seedData; - - public ClientsUpdater(IKeycloakFactory keycloakFactory, ISeedDataHandler seedDataHandler) - { - _keycloakFactory = keycloakFactory; - _seedData = seedDataHandler; - } - public Task UpdateClients(string keycloakInstanceName, CancellationToken cancellationToken) { - var realm = _seedData.Realm; - var keycloak = _keycloakFactory.CreateKeycloakClient(keycloakInstanceName); - return _seedData.SetClientInternalIds(UpdateClientsInternal(keycloak, realm, cancellationToken)); + var realm = seedDataHandler.Realm; + var keycloak = keycloakFactory.CreateKeycloakClient(keycloakInstanceName); + var seederConfig = seedDataHandler.GetSpecificConfiguration(ConfigurationKey.Clients); + var clientScopesSeederConfig = seedDataHandler.GetSpecificConfiguration(ConfigurationKey.ClientScopes); + + return seedDataHandler.SetClientInternalIds(UpdateClientsInternal(keycloak, realm, seederConfig, clientScopesSeederConfig, cancellationToken)); } - private async IAsyncEnumerable<(string ClientId, string Id)> UpdateClientsInternal(KeycloakClient keycloak, string realm, [EnumeratorCancellation] CancellationToken cancellationToken) + private async IAsyncEnumerable<(string ClientId, string Id)> UpdateClientsInternal(KeycloakClient keycloak, string realm, KeycloakSeederConfigModel seederConfig, KeycloakSeederConfigModel clientScopesSeederConfig, [EnumeratorCancellation] CancellationToken cancellationToken) { var clientScopes = await keycloak.GetClientScopesAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); string GetClientScopeId(string scope) => clientScopes.SingleOrDefault(x => x.Name == scope)?.Id ?? throw new ConflictException($"id of clientScope {scope} is undefined"); - foreach (var update in _seedData.Clients) + foreach (var update in seedDataHandler.Clients) { if (update.ClientId == null) throw new ConflictException($"clientId must not be null {update.Id}"); @@ -60,24 +56,33 @@ public Task UpdateClients(string keycloakInstanceName, CancellationToken cancell var client = (await keycloak.GetClientsAsync(realm, clientId: update.ClientId, cancellationToken: cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)).SingleOrDefault(x => x.ClientId == update.ClientId); if (client == null) { + if (!seederConfig.ModificationAllowed(ModificationType.Create, update.ClientId)) + { + continue; + } + client = await CreateClient(keycloak, realm, update, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } else { - await UpdateClient( + if (seederConfig.ModificationAllowed(ModificationType.Update, update.ClientId)) + { + await UpdateClient( keycloak, realm, client.Id ?? throw new ConflictException($"client.Id must not be null: clientId {update.ClientId}"), client, update, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + } await UpdateClientProtocolMappers( keycloak, realm, - client.Id, + client.Id ?? throw new ConflictException($"client.Id must not be null: clientId {update.ClientId}"), client, update, + seederConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } @@ -88,6 +93,7 @@ await UpdateDefaultClientScopes( client, update, GetClientScopeId, + clientScopesSeederConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); await UpdateOptionalClientScopes( @@ -97,6 +103,7 @@ await UpdateOptionalClientScopes( client, update, GetClientScopeId, + clientScopesSeederConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); yield return (update.ClientId, client.Id); @@ -110,6 +117,7 @@ private static async Task CreateClient(KeycloakClient keycloak, string r { throw new ConflictException($"PartialImport failed to add client id: {update.Id}, clientId: {update.ClientId}"); } + var client = (await keycloak.GetClientsAsync(realm, clientId: update.ClientId, cancellationToken: cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)).SingleOrDefault(x => x.ClientId == update.ClientId); return client ?? throw new ConflictException($"failed to read newly created client {update.ClientId}"); } @@ -127,22 +135,31 @@ await keycloak.UpdateClientAsync( } } - private static async Task UpdateClientProtocolMappers(KeycloakClient keycloak, string realm, string clientId, Client client, ClientModel update, CancellationToken cancellationToken) + private static async Task UpdateClientProtocolMappers(KeycloakClient keycloak, string realm, string clientId, Client client, ClientModel update, KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { var clientProtocolMappers = client.ProtocolMappers ?? Enumerable.Empty(); var updateProtocolMappers = update.ProtocolMappers ?? Enumerable.Empty(); + if (client.ClientId == null) + throw new ConflictException("client.ClientId must never be null"); - foreach (var mapperId in clientProtocolMappers.ExceptBy(updateProtocolMappers.Select(x => x.Name), x => x.Name).Select(x => x.Id ?? throw new ConflictException($"protocolMapper.Id is null {x.Name}"))) + foreach (var mapperId in clientProtocolMappers + .Where(x => seederConfig.ModificationAllowed(client.ClientId, ConfigurationKey.ClientProtocolMapper, ModificationType.Delete, x.Name)) + .ExceptBy(updateProtocolMappers.Select(x => x.Name), x => x.Name) + .Select(x => x.Id ?? throw new ConflictException($"protocolMapper.Id is null {x.Name}"))) { await keycloak.DeleteClientProtocolMapperAsync(realm, clientId, mapperId, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } - foreach (var mapper in updateProtocolMappers.ExceptBy(clientProtocolMappers.Select(x => x.Name), x => x.Name).Select(x => ProtocolMappersUpdater.CreateProtocolMapper(null, x))) + foreach (var mapper in updateProtocolMappers + .Where(x => seederConfig.ModificationAllowed(client.ClientId, ConfigurationKey.ClientProtocolMapper, ModificationType.Create, x.Name)) + .ExceptBy(clientProtocolMappers.Select(x => x.Name), x => x.Name) + .Select(x => ProtocolMappersUpdater.CreateProtocolMapper(null, x))) { await keycloak.CreateClientProtocolMapperAsync(realm, clientId, mapper, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } foreach (var (mapperId, mapper) in clientProtocolMappers + .Where(x => seederConfig.ModificationAllowed(client.ClientId, ConfigurationKey.ClientProtocolMapper, ModificationType.Update, x.Name)) .Join( updateProtocolMappers, x => x.Name, @@ -157,33 +174,45 @@ private static async Task UpdateClientProtocolMappers(KeycloakClient keycloak, s } } - private static async Task UpdateOptionalClientScopes(KeycloakClient keycloak, string realm, string idOfClient, Client client, ClientModel update, Func getClientScopeId, CancellationToken cancellationToken) + private static async Task UpdateOptionalClientScopes(KeycloakClient keycloak, string realm, string idOfClient, Client client, ClientModel update, Func getClientScopeId, KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { var optionalScopes = client.OptionalClientScopes ?? Enumerable.Empty(); var updateScopes = update.OptionalClientScopes ?? Enumerable.Empty(); - foreach (var scopeId in optionalScopes.Except(updateScopes).Select(getClientScopeId)) + foreach (var scopeId in optionalScopes + .Where(x => seederConfig.ModificationAllowed(ModificationType.Delete, x)) + .Except(updateScopes) + .Select(getClientScopeId)) { await keycloak.DeleteOptionalClientScopeAsync(realm, idOfClient, scopeId, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } - foreach (var scopeId in updateScopes.Except(optionalScopes).Select(getClientScopeId)) + foreach (var scopeId in updateScopes + .Where(x => seederConfig.ModificationAllowed(ModificationType.Update, x)) + .Except(optionalScopes) + .Select(getClientScopeId)) { await keycloak.UpdateOptionalClientScopeAsync(realm, idOfClient, scopeId, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } } - private static async Task UpdateDefaultClientScopes(KeycloakClient keycloak, string realm, string idOfClient, Client client, ClientModel update, Func getClientScopeId, CancellationToken cancellationToken) + private static async Task UpdateDefaultClientScopes(KeycloakClient keycloak, string realm, string idOfClient, Client client, ClientModel update, Func getClientScopeId, KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { var defaultScopes = client.DefaultClientScopes ?? Enumerable.Empty(); var updateScopes = update.DefaultClientScopes ?? Enumerable.Empty(); - foreach (var scopeId in defaultScopes.Except(updateScopes).Select(getClientScopeId)) + foreach (var scopeId in defaultScopes + .Where(x => seederConfig.ModificationAllowed(ModificationType.Delete, x)) + .Except(updateScopes) + .Select(getClientScopeId)) { await keycloak.DeleteDefaultClientScopeAsync(realm, idOfClient, scopeId, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } - foreach (var scopeId in updateScopes.Except(defaultScopes).Select(getClientScopeId)) + foreach (var scopeId in updateScopes + .Where(x => seederConfig.ModificationAllowed(ModificationType.Update, x)) + .Except(defaultScopes) + .Select(getClientScopeId)) { await keycloak.UpdateDefaultClientScopeAsync(realm, idOfClient, scopeId, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/ISeedDataHandler.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/ISeedDataHandler.cs index 9a2beecac6..10dd81fe55 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/ISeedDataHandler.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/ISeedDataHandler.cs @@ -61,4 +61,6 @@ public interface ISeedDataHandler IEnumerable GetAuthenticationExecutions(string? alias); AuthenticatorConfigModel GetAuthenticatorConfig(string? alias); + KeycloakSeederConfigModel GetSpecificConfiguration(ConfigurationKey configKey); + bool IsModificationAllowed(ConfigurationKey configKey); } diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/IdentityProvidersUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/IdentityProvidersUpdater.cs index d78892a450..63cbabc6e7 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/IdentityProvidersUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/IdentityProvidersUpdater.cs @@ -23,6 +23,7 @@ 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.IdentityProviders; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Extensions; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic; @@ -34,6 +35,7 @@ public async Task UpdateIdentityProviders(string keycloakInstanceName, Cancellat { var keycloak = keycloakFactory.CreateKeycloakClient(keycloakInstanceName); var realm = seedDataHandler.Realm; + var seederConfig = seedDataHandler.GetSpecificConfiguration(ConfigurationKey.IdentityProviders); foreach (var updateIdentityProvider in seedDataHandler.IdentityProviders) { @@ -43,7 +45,7 @@ public async Task UpdateIdentityProviders(string keycloakInstanceName, Cancellat try { var identityProvider = await keycloak.GetIdentityProviderAsync(realm, updateIdentityProvider.Alias, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - if (!CompareIdentityProvider(identityProvider, updateIdentityProvider)) + if (seederConfig.ModificationAllowed(ModificationType.Update, updateIdentityProvider.Alias) && !CompareIdentityProvider(identityProvider, updateIdentityProvider)) { UpdateIdentityProvider(identityProvider, updateIdentityProvider); await keycloak.UpdateIdentityProviderAsync(realm, updateIdentityProvider.Alias, identityProvider, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); @@ -51,23 +53,28 @@ public async Task UpdateIdentityProviders(string keycloakInstanceName, Cancellat } catch (KeycloakEntityNotFoundException) { - var identityProvider = new IdentityProvider(); - UpdateIdentityProvider(identityProvider, updateIdentityProvider); - await keycloak.CreateIdentityProviderAsync(realm, identityProvider, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + if (seederConfig.ModificationAllowed(ModificationType.Create, updateIdentityProvider.Alias)) + { + var identityProvider = new IdentityProvider(); + UpdateIdentityProvider(identityProvider, updateIdentityProvider); + await keycloak.CreateIdentityProviderAsync(realm, identityProvider, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + } } 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); - await CreateMissingIdentityProviderMappers(keycloak, realm, updateIdentityProvider.Alias, mappers, updateMappers, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await UpdateExistingIdentityProviderMappers(keycloak, realm, updateIdentityProvider.Alias, mappers, updateMappers, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await DeleteObsoleteIdentityProviderMappers(keycloak, realm, updateIdentityProvider.Alias, mappers, updateMappers, seederConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await CreateMissingIdentityProviderMappers(keycloak, realm, updateIdentityProvider.Alias, mappers, updateMappers, seederConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await UpdateExistingIdentityProviderMappers(keycloak, realm, updateIdentityProvider.Alias, mappers, updateMappers, seederConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } } - private static async Task CreateMissingIdentityProviderMappers(KeycloakClient keycloak, string realm, string alias, IEnumerable mappers, IEnumerable updateMappers, CancellationToken cancellationToken) + private static async Task CreateMissingIdentityProviderMappers(KeycloakClient keycloak, string realm, string alias, IEnumerable mappers, IEnumerable updateMappers, KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { - foreach (var mapper in updateMappers.ExceptBy(mappers.Select(x => x.Name), x => x.Name)) + foreach (var mapper in updateMappers + .Where(x => seederConfig.ModificationAllowed(alias, ConfigurationKey.IdentityProviderMappers, ModificationType.Create, x.Name)) + .ExceptBy(mappers.Select(x => x.Name), x => x.Name)) { await keycloak.AddIdentityProviderMapperAsync( realm, @@ -83,16 +90,16 @@ await keycloak.AddIdentityProviderMapperAsync( } } - private static async Task UpdateExistingIdentityProviderMappers(KeycloakClient keycloak, string realm, string alias, IEnumerable mappers, IEnumerable updateMappers, CancellationToken cancellationToken) + private static async Task UpdateExistingIdentityProviderMappers(KeycloakClient keycloak, string realm, string alias, IEnumerable mappers, IEnumerable updateMappers, KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { foreach (var (mapper, update) in mappers + .Where(x => seederConfig.ModificationAllowed(alias, ConfigurationKey.IdentityProviderMappers, ModificationType.Update, x.Name)) .Join( updateMappers, x => x.Name, x => x.Name, (mapper, update) => (Mapper: mapper, Update: update)) - .Where( - x => !CompareIdentityProviderMapper(x.Mapper, x.Update))) + .Where(x => !CompareIdentityProviderMapper(x.Mapper, x.Update))) { await keycloak.UpdateIdentityProviderMapperAsync( realm, @@ -103,21 +110,23 @@ await keycloak.UpdateIdentityProviderMapperAsync( } } - private static async Task DeleteObsoleteIdentityProviderMappers(KeycloakClient keycloak, string realm, string alias, IEnumerable mappers, IEnumerable updateMappers, CancellationToken cancellationToken) + private static async Task DeleteObsoleteIdentityProviderMappers(KeycloakClient keycloak, string realm, string alias, IEnumerable mappers, IEnumerable updateMappers, KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { - if (mappers.ExceptBy(updateMappers.Select(x => x.Name), x => x.Name).IfAny( - async deleteMappers => - { - foreach (var mapper in deleteMappers) + if (mappers + .Where(x => seederConfig.ModificationAllowed(alias, ConfigurationKey.IdentityProviderMappers, ModificationType.Delete, x.Name)) + .ExceptBy(updateMappers.Select(x => x.Name), x => x.Name) + .IfAny(async deleteMappers => { - await keycloak.DeleteIdentityProviderMapperAsync( - realm, - alias, - mapper.Id ?? throw new ConflictException($"identityProviderMapper.id must never be null {mapper.Name} {mapper.IdentityProviderAlias}"), - cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - } - }, - out var deleteMappersTask)) + foreach (var mapper in deleteMappers) + { + await keycloak.DeleteIdentityProviderMapperAsync( + realm, + alias, + mapper.Id ?? throw new ConflictException($"identityProviderMapper.id must never be null {mapper.Name} {mapper.IdentityProviderAlias}"), + cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + } + }, + out var deleteMappersTask)) { await deleteMappersTask.ConfigureAwait(ConfigureAwaitOptions.None); } diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/KeecloakSeeder.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/KeecloakSeeder.cs index b3e749aa00..cd3ad2983e 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/KeecloakSeeder.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/KeecloakSeeder.cs @@ -18,6 +18,8 @@ ********************************************************************************/ using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Extensions; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic; @@ -44,17 +46,23 @@ public async Task Seed(CancellationToken cancellationToken) { 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 CheckAndExecuteUpdater(ConfigurationKey.Localizations, realm.InstanceName, localizationsUpdater.UpdateLocalizations, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await CheckAndExecuteUpdater(ConfigurationKey.UserProfile, realm.InstanceName, userProfileUpdater.UpdateUserProfile, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await CheckAndExecuteUpdater(ConfigurationKey.Roles, realm.InstanceName, rolesUpdater.UpdateRealmRoles, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await CheckAndExecuteUpdater(ConfigurationKey.ClientScopes, realm.InstanceName, clientScopesUpdater.UpdateClientScopes, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + // The clients updater must run to set the clientIds 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 CheckAndExecuteUpdater(ConfigurationKey.ClientRoles, realm.InstanceName, rolesUpdater.UpdateClientRoles, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await CheckAndExecuteUpdater(ConfigurationKey.Roles, realm.InstanceName, rolesUpdater.UpdateCompositeRoles, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await CheckAndExecuteUpdater(ConfigurationKey.IdentityProviders, realm.InstanceName, identityProvidersUpdater.UpdateIdentityProviders, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await CheckAndExecuteUpdater(ConfigurationKey.Users, realm.InstanceName, usersUpdater.UpdateUsers, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await CheckAndExecuteUpdater(ConfigurationKey.ClientScopeMappers, realm.InstanceName, clientScopeMapperUpdater.UpdateClientScopeMapper, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await CheckAndExecuteUpdater(ConfigurationKey.AuthenticationFlows, realm.InstanceName, authenticationFlowsUpdater.UpdateAuthenticationFlows, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } } + + private Task CheckAndExecuteUpdater(ConfigurationKey configKey, string instanceName, Func updaterExecution, CancellationToken cancellationToken) => + seedDataHandler.IsModificationAllowed(configKey) + ? updaterExecution(instanceName, cancellationToken) + : Task.CompletedTask; } diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/LocalizationsUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/LocalizationsUpdater.cs index c6b8840ecc..b867723bed 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/LocalizationsUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/LocalizationsUpdater.cs @@ -20,6 +20,8 @@ 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.Seeding.Extensions; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic; @@ -30,19 +32,22 @@ public async Task UpdateLocalizations(string keycloakInstanceName, CancellationT { var keycloak = keycloakFactory.CreateKeycloakClient(keycloakInstanceName); var realm = seedDataHandler.Realm; + var seederConfig = seedDataHandler.GetSpecificConfiguration(ConfigurationKey.Localizations); 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 UpdateLocaleTranslations(keycloak, realm, localizations, updateRealmLocalizations, seederConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + foreach (var deleteTranslation in localizations + .Where(x => seederConfig.ModificationAllowed(ModificationType.Delete, x)) + .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) + IEnumerable<(string Locale, IEnumerable> Translations)> translations, + KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { if (!await locales .Join( @@ -56,50 +61,55 @@ private static async Task UpdateLocaleTranslations(KeycloakClient keycloak, stri 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); + await UpdateLocales(keycloak, realm, update, localizations, locale, seederConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await DeleteLocales(keycloak, realm, localizations, update, locale, seederConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } }).ConfigureAwait(false)) { - await AddLocales(keycloak, realm, translations, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await AddLocales(keycloak, realm, translations, seederConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } } - private static async Task DeleteLocales(KeycloakClient keycloak, string realm, CancellationToken cancellationToken, - IEnumerable> localizations, (string Locale, IEnumerable> Translations) update, string locale) + private static async Task DeleteLocales(KeycloakClient keycloak, string realm, + IEnumerable> localizations, (string Locale, IEnumerable> Translations) update, string locale, + KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { - foreach (var deleteTranslation in - localizations.ExceptBy(update.Translations.Select(t => t.Key), - l => l.Key)) + foreach (var deleteTranslation in localizations + .Where(x => seederConfig.ModificationAllowed(ModificationType.Delete, x.Key)) + .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) + private static async Task UpdateLocales(KeycloakClient keycloak, string realm, + (string Locale, IEnumerable> Translations) update, IEnumerable> localizations, string locale, + KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { - foreach (var missingTranslation in update.Translations.ExceptBy(localizations.Select(loc => loc.Key), - locModel => locModel.Key)) + foreach (var missingTranslation in update.Translations + .Where(x => seederConfig.ModificationAllowed(ModificationType.Update, x.Key)) + .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))) + localizations + .Where(x => seederConfig.ModificationAllowed(ModificationType.Update, x.Key)) + .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) + KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { - foreach (var translation in translations.SelectMany(x => x.Translations.Select(t => (x.Locale, t.Key, t.Value)))) + foreach (var translation in translations.SelectMany(x => x.Translations.Select(t => (x.Locale, t.Key, t.Value))).Where(x => seederConfig.ModificationAllowed(ModificationType.Create, x.Key))) { await keycloak.UpdateLocaleAsync(realm, translation.Locale, translation.Key, translation.Value, cancellationToken).ConfigureAwait(false); } diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/RolesUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/RolesUpdater.cs index 928aff5d80..304c58b83d 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/RolesUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/RolesUpdater.cs @@ -22,62 +22,64 @@ 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; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Extensions; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic; -public class RolesUpdater : IRolesUpdater +public class RolesUpdater(IKeycloakFactory keycloakFactory, ISeedDataHandler seedDataHandler) + : IRolesUpdater { - private readonly IKeycloakFactory _keycloakFactory; - private readonly ISeedDataHandler _seedData; - - public RolesUpdater(IKeycloakFactory keycloakFactory, ISeedDataHandler seedDataHandler) - { - _keycloakFactory = keycloakFactory; - _seedData = seedDataHandler; - } - public async Task UpdateClientRoles(string keycloakInstanceName, CancellationToken cancellationToken) { - var keycloak = _keycloakFactory.CreateKeycloakClient(keycloakInstanceName); - var realm = _seedData.Realm; + var keycloak = keycloakFactory.CreateKeycloakClient(keycloakInstanceName); + var realm = seedDataHandler.Realm; + var seederConfig = seedDataHandler.GetSpecificConfiguration(ConfigurationKey.ClientRoles); - foreach (var (clientId, updateRoles) in _seedData.ClientRoles) + foreach (var (clientId, updateRoles) in seedDataHandler.ClientRoles) { - var id = _seedData.GetIdOfClient(clientId); + var id = seedDataHandler.GetIdOfClient(clientId); var roles = await keycloak.GetRolesAsync(realm, id, cancellationToken: cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - foreach (var newRole in updateRoles.ExceptBy(roles.Select(role => role.Name), roleModel => roleModel.Name)) + foreach (var newRole in updateRoles + .Where(x => seederConfig.ModificationAllowed(ModificationType.Create, x.Name)) + .ExceptBy(roles.Select(role => role.Name), roleModel => roleModel.Name)) { await keycloak.CreateRoleAsync(realm, id, CreateRole(newRole), cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } - await UpdateAndDeleteRoles(keycloak, realm, roles, updateRoles, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await UpdateAndDeleteRoles(keycloak, realm, roles, updateRoles, seederConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } } public async Task UpdateRealmRoles(string keycloakInstanceName, CancellationToken cancellationToken) { - var keycloak = _keycloakFactory.CreateKeycloakClient(keycloakInstanceName); - var realm = _seedData.Realm; + var keycloak = keycloakFactory.CreateKeycloakClient(keycloakInstanceName); + var realm = seedDataHandler.Realm; + var seederConfig = seedDataHandler.GetSpecificConfiguration(ConfigurationKey.Roles); var roles = await keycloak.GetRolesAsync(realm, cancellationToken: cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - var updateRealmRoles = _seedData.RealmRoles; + var updateRealmRoles = seedDataHandler.RealmRoles; - foreach (var newRole in updateRealmRoles.ExceptBy(roles.Select(role => role.Name), roleModel => roleModel.Name)) + foreach (var newRole in updateRealmRoles + .Where(x => seederConfig.ModificationAllowed(ModificationType.Create, x.Name)) + .ExceptBy(roles.Select(role => role.Name), roleModel => roleModel.Name)) { await keycloak.CreateRoleAsync(realm, CreateRole(newRole), cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } - await UpdateAndDeleteRoles(keycloak, realm, roles, updateRealmRoles, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + + await UpdateAndDeleteRoles(keycloak, realm, roles, updateRealmRoles, seederConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } - private static async Task UpdateAndDeleteRoles(KeycloakClient keycloak, string realm, IEnumerable roles, IEnumerable updateRoles, CancellationToken cancellationToken) + private static async Task UpdateAndDeleteRoles(KeycloakClient keycloak, string realm, IEnumerable roles, IEnumerable updateRoles, KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { foreach (var (role, update) in - roles.Join( - updateRoles, - role => role.Name, - roleModel => roleModel.Name, - (role, roleModel) => (Role: role, Update: roleModel))) + roles + .Where(x => seederConfig.ModificationAllowed(ModificationType.Update, x.Name)) + .Join( + updateRoles, + role => role.Name, + roleModel => roleModel.Name, + (role, roleModel) => (Role: role, Update: roleModel))) { if (!CompareRole(role, update)) { @@ -90,8 +92,9 @@ private static async Task UpdateAndDeleteRoles(KeycloakClient keycloak, string r } } - foreach (var deleteRole in - roles.ExceptBy(updateRoles.Select(roleModel => roleModel.Name), role => role.Name)) + foreach (var deleteRole in roles + .Where(x => seederConfig.ModificationAllowed(ModificationType.Delete, x.Name)) + .ExceptBy(updateRoles.Select(roleModel => roleModel.Name), role => role.Name)) { if (deleteRole.Id == null) throw new ConflictException($"role id must not be null: {deleteRole.Name}"); @@ -102,106 +105,154 @@ private static async Task UpdateAndDeleteRoles(KeycloakClient keycloak, string r public async Task UpdateCompositeRoles(string keycloakInstanceName, CancellationToken cancellationToken) { - var keycloak = _keycloakFactory.CreateKeycloakClient(keycloakInstanceName); - var realm = _seedData.Realm; + var keycloak = keycloakFactory.CreateKeycloakClient(keycloakInstanceName); + var realm = seedDataHandler.Realm; + var seederConfig = seedDataHandler.GetSpecificConfiguration(ConfigurationKey.Roles); - foreach (var (clientId, updateRoles) in _seedData.ClientRoles) + foreach (var (clientId, updateRoles) in seedDataHandler.ClientRoles) { - var id = _seedData.GetIdOfClient(clientId); + var id = seedDataHandler.GetIdOfClient(clientId); await UpdateCompositeRolesInner( - () => keycloak.GetRolesAsync(realm, id, cancellationToken: cancellationToken), + keycloak, + realm, + seederConfig, updateRoles, + () => keycloak.GetRolesAsync(realm, id, cancellationToken: cancellationToken), (name, roles) => keycloak.RemoveCompositesFromRoleAsync(realm, id, name, roles, cancellationToken), - (name, roles) => keycloak.AddCompositesToRoleAsync(realm, id, name, roles, cancellationToken)).ConfigureAwait(ConfigureAwaitOptions.None); + (name, roles) => keycloak.AddCompositesToRoleAsync(realm, id, name, roles, cancellationToken), + cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } await UpdateCompositeRolesInner( + keycloak, + realm, + seederConfig, + seedDataHandler.RealmRoles, () => keycloak.GetRolesAsync(realm, cancellationToken: cancellationToken), - _seedData.RealmRoles, (name, roles) => keycloak.RemoveCompositesFromRoleAsync(realm, name, roles, cancellationToken), - (name, roles) => keycloak.AddCompositesToRoleAsync(realm, name, roles, cancellationToken)).ConfigureAwait(ConfigureAwaitOptions.None); + (name, roles) => keycloak.AddCompositesToRoleAsync(realm, name, roles, cancellationToken), + cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + } - async Task UpdateCompositeRolesInner( - Func>> getRoles, - IEnumerable updateRoles, - Func, Task> removeCompositeRoles, - Func, Task> addCompositeRoles) + public async Task UpdateCompositeRolesInner( + KeycloakClient keycloak, + string realm, + KeycloakSeederConfigModel seederConfig, + IEnumerable updateRoles, + Func>> getRoles, + Func, Task> removeCompositeRoles, + Func, Task> addCompositeRoles, + CancellationToken cancellationToken) + { + var roles = await getRoles().ConfigureAwait(ConfigureAwaitOptions.None); + + await RemoveAddCompositeRolesInner<(string ContainerId, string Name)>( + keycloak, + realm, + seederConfig, + updateRoles, + roles, + removeCompositeRoles, + addCompositeRoles, + roleModel => roleModel.Composites?.Client?.Any() ?? false, + role => role.Composites?.Client?.Any() ?? false, + role => role.ClientRole ?? false, + roleModel => roleModel.Composites?.Client? + .FilterNotNullValues() + .Select(x => ( + Id: seedDataHandler.GetIdOfClient(x.Key), + Names: x.Value)) + .SelectMany(x => x.Names.Select(name => (x.Id, name))) ?? throw new ConflictException($"roleModel.Composites.Client is null: {roleModel.Id} {roleModel.Name}"), + role => ( + role.ContainerId ?? throw new ConflictException($"role.ContainerId is null: {role.Id} {role.Name}"), + role.Name ?? throw new ConflictException($"role.Name is null: {role.Id}")), + async x => await keycloak.GetRoleByNameAsync(realm, x.ContainerId, x.Name, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None), + cancellationToken + ).ConfigureAwait(ConfigureAwaitOptions.None); + + await RemoveAddCompositeRolesInner( + keycloak, + realm, + seederConfig, + updateRoles, + roles, + removeCompositeRoles, + addCompositeRoles, + roleModel => roleModel.Composites?.Realm?.Any() ?? false, + role => role.Composites?.Realm?.Any() ?? false, + role => !(role.ClientRole ?? false), + roleModel => roleModel.Composites?.Realm ?? throw new ConflictException($"roleModel.Composites.Realm is null: {roleModel.Id} {roleModel.Name}"), + role => role.Name ?? throw new ConflictException($"role.Name is null: {role.Id}"), + async name => await keycloak.GetRoleByNameAsync(realm, name, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None), + cancellationToken + ).ConfigureAwait(ConfigureAwaitOptions.None); + } + + private static async Task RemoveAddCompositeRolesInner( + KeycloakClient keycloak, + string realm, + KeycloakSeederConfigModel seederConfig, + IEnumerable updateRoles, + IEnumerable roles, + Func, Task> removeCompositeRoles, + Func, Task> addCompositeRoles, + Func compositeRolesUpdatePredicate, + Func compositeRolesPredicate, + Func rolePredicate, + Func> joinUpdateSelector, + Func joinUpdateKey, + Func> getRoleByName, + CancellationToken cancellationToken) + { + var updateComposites = updateRoles.Where(x => compositeRolesUpdatePredicate(x)); + var removeComposites = roles.Where(x => compositeRolesPredicate(x)).ExceptBy(updateComposites.Select(roleModel => roleModel.Name), role => role.Name); + + await RemoveRoles(keycloak, realm, removeCompositeRoles, rolePredicate, removeComposites, cancellationToken); + + var joinedComposites = roles.Join( + updateComposites, + role => role.Name, + roleModel => roleModel.Name, + (role, roleModel) => ( + Role: role, + Update: joinUpdateSelector(roleModel))); + + foreach (var (role, updates) in joinedComposites) { - var roles = await getRoles().ConfigureAwait(ConfigureAwaitOptions.None); - - await RemoveAddCompositeRolesInner<(string ContainerId, string Name)>( - roleModel => roleModel.Composites?.Client?.Any() ?? false, - 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)) - .SelectMany(x => x.Names.Select(name => (x.Id, name))) ?? throw new ConflictException($"roleModel.Composites.Client is null: {roleModel.Id} {roleModel.Name}"), - role => ( - role.ContainerId ?? throw new ConflictException($"role.ContainerId is null: {role.Id} {role.Name}"), - role.Name ?? throw new ConflictException($"role.Name is null: {role.Id}")), - async x => await keycloak.GetRoleByNameAsync(realm, x.ContainerId, x.Name, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None) - ).ConfigureAwait(ConfigureAwaitOptions.None); - - await RemoveAddCompositeRolesInner( - roleModel => roleModel.Composites?.Realm?.Any() ?? false, - role => role.Composites?.Realm?.Any() ?? false, - role => !(role.ClientRole ?? false), - roleModel => roleModel.Composites?.Realm ?? throw new ConflictException($"roleModel.Composites.Realm is null: {roleModel.Id} {roleModel.Name}"), - role => role.Name ?? throw new ConflictException($"role.Name is null: {role.Id}"), - async name => await keycloak.GetRoleByNameAsync(realm, name, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None) - ).ConfigureAwait(ConfigureAwaitOptions.None); - - async Task RemoveAddCompositeRolesInner( - Func compositeRolesUpdatePredicate, - Func compositeRolesPredicate, - Func rolePredicate, - Func> joinUpdateSelector, - Func joinUpdateKey, - Func> getRoleByName) - { - var updateComposites = updateRoles.Where(x => compositeRolesUpdatePredicate(x)); - var removeComposites = roles.Where(x => compositeRolesPredicate(x)).ExceptBy(updateComposites.Select(roleModel => roleModel.Name), role => role.Name); + if (role.Id == null || role.Name == null) + throw new ConflictException($"role.id or role.name must not be null {role.Id} {role.Name}"); + var composites = (await keycloak.GetRoleChildrenAsync(realm, role.Id, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)).Where(role => rolePredicate(role)); + composites.Where(role => role.ContainerId == null || role.Name == null).IfAny( + invalid => throw new ConflictException($"composites roles containerId or name must not be null: {string.Join(" ", invalid.Select(x => $"[{string.Join(",", x.Id, x.Name, x.Description, x.ContainerId)}]"))}")); + + var remove = composites.ExceptBy(updates, role => joinUpdateKey(role)).Where(x => seederConfig.ModificationAllowed(ModificationType.Delete, role.Name) || seederConfig.ModificationAllowed(ModificationType.Delete, x.Name)); + await removeCompositeRoles(role.Name, remove).ConfigureAwait(ConfigureAwaitOptions.None); - foreach (var remove in removeComposites) - { - if (remove.Id == null || remove.Name == null) - throw new ConflictException($"role.id or role.name must not be null {remove.Id} {remove.Name}"); + var add = await updates.Except(composites.Select(role => joinUpdateKey(role))) + .ToAsyncEnumerable() + .SelectAwait(x => getRoleByName(x)) + .ToListAsync(cancellationToken) + .ConfigureAwait(false); + await addCompositeRoles(role.Name, add.Where(x => seederConfig.ModificationAllowed(ModificationType.Create, role.Name) || seederConfig.ModificationAllowed(ModificationType.Create, x.Name))).ConfigureAwait(ConfigureAwaitOptions.None); + } + } - var composites = (await keycloak.GetRoleChildrenAsync(realm, remove.Id, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)).Where(role => rolePredicate(role)); - await removeCompositeRoles(remove.Name, composites).ConfigureAwait(ConfigureAwaitOptions.None); - } + private static async Task RemoveRoles( + KeycloakClient keycloak, + string realm, + Func, Task> removeCompositeRoles, + Func rolePredicate, + IEnumerable removeComposites, + CancellationToken cancellationToken) + { + foreach (var remove in removeComposites) + { + if (remove.Id == null || remove.Name == null) + throw new ConflictException($"role.id or role.name must not be null {remove.Id} {remove.Name}"); - var joinedComposites = roles.Join( - updateComposites, - role => role.Name, - roleModel => roleModel.Name, - (role, roleModel) => ( - Role: role, - Update: joinUpdateSelector(roleModel))); - - foreach (var (role, updates) in joinedComposites) - { - if (role.Id == null || role.Name == null) - throw new ConflictException($"role.id or role.name must not be null {role.Id} {role.Name}"); - var composites = (await keycloak.GetRoleChildrenAsync(realm, role.Id, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)).Where(role => rolePredicate(role)); - composites.Where(role => role.ContainerId == null || role.Name == null).IfAny( - invalid => throw new ConflictException($"composites roles containerId or name must not be null: {string.Join(" ", invalid.Select(x => $"[{string.Join(",", x.Id, x.Name, x.Description, x.ContainerId)}]"))}")); - - var remove = composites.ExceptBy(updates, role => joinUpdateKey(role)); - await removeCompositeRoles(role.Name, remove).ConfigureAwait(ConfigureAwaitOptions.None); - - var add = await updates.Except(composites.Select(role => joinUpdateKey(role))) - .ToAsyncEnumerable() - .SelectAwait(x => getRoleByName(x)) - .ToListAsync(cancellationToken) - .ConfigureAwait(false); - await addCompositeRoles(role.Name, add).ConfigureAwait(ConfigureAwaitOptions.None); - } - } + var composites = (await keycloak.GetRoleChildrenAsync(realm, remove.Id, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)).Where(role => rolePredicate(role)); + await removeCompositeRoles(remove.Name, composites).ConfigureAwait(ConfigureAwaitOptions.None); } } diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/SeedDataHandler.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/SeedDataHandler.cs index 49977033d8..6d6528022c 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/SeedDataHandler.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/SeedDataHandler.cs @@ -20,6 +20,7 @@ using Org.Eclipse.TractusX.Portal.Backend.Framework.Async; using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; using Org.Eclipse.TractusX.Portal.Backend.Framework.Linq; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Extensions; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; using System.Collections.Immutable; using System.Text.Json; @@ -37,6 +38,8 @@ public class SeedDataHandler : ISeedDataHandler private KeycloakRealm? _keycloakRealm; private IReadOnlyDictionary? _idOfClients; + private SeederConfigurationModel? _defaultConfiguration; + private IReadOnlyDictionary? _flatConfiguration; public async Task Import(KeycloakRealmSettings realmSettings, CancellationToken cancellationToken) { @@ -46,6 +49,8 @@ public async Task Import(KeycloakRealmSettings realmSettings, CancellationToken async (importRealm, path) => importRealm.Merge(await ReadJsonRealm(path, realmSettings.Realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)), cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)) .Merge(realmSettings.ToModel()); + _defaultConfiguration = realmSettings.GetConfigurationDictionaries(); + _flatConfiguration = realmSettings.GetFlatDictionary(); _idOfClients = null; } @@ -58,6 +63,7 @@ private static async Task ReadJsonRealm(string path, string realm jsonRealm = await JsonSerializer.DeserializeAsync(stream, Options, cancellationToken) .ConfigureAwait(false) ?? throw new ConfigurationException($"cannot deserialize realm from {path}"); } + if (jsonRealm.Realm != null && jsonRealm.Realm != realm) throw new ConfigurationException($"json realm {jsonRealm.Realm} doesn't match the configured realm: {realm}"); @@ -142,6 +148,7 @@ public async Task SetClientInternalIds(IAsyncEnumerable<(string ClientId, string { clientIds[clientId] = id; } + _idOfClients = clientIds.ToImmutableDictionary(); } @@ -157,4 +164,14 @@ public IEnumerable GetAuthenticationExecutions(str public AuthenticatorConfigModel GetAuthenticatorConfig(string? alias) => _keycloakRealm?.AuthenticatorConfig?.SingleOrDefault(x => x.Alias == (alias ?? throw new ConflictException("alias is null"))) ?? throw new ConflictException($"authenticatorConfig {alias} does not exist"); + + public KeycloakSeederConfigModel GetSpecificConfiguration(ConfigurationKey configKey) => + new KeycloakSeederConfigModel( + _defaultConfiguration ?? throw new ConflictException("configuration must not be null"), + _defaultConfiguration.SeederConfigurations?.TryGetValue(configKey.ToString(), out var specificConfiguration) ?? false ? specificConfiguration : null); + + public bool IsModificationAllowed(ConfigurationKey configKey) => + _flatConfiguration?.TryGetValue(configKey, out var result) ?? false + ? result + : (_defaultConfiguration ?? throw new ConflictException("configuration must not be null")).Create || _defaultConfiguration.Update || _defaultConfiguration.Delete; } diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/UserProfileUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/UserProfileUpdater.cs index 812abf9692..7dcb8c47b4 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/UserProfileUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/UserProfileUpdater.cs @@ -20,6 +20,8 @@ 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 Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Extensions; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; using System.Text.Json; using System.Text.Json.Serialization; @@ -42,6 +44,11 @@ public async Task UpdateUserProfile(string keycloakInstanceName, CancellationTok var keycloak = keycloakFactory.CreateKeycloakClient(keycloakInstanceName); var realm = seedDataHandler.Realm; var userProfiles = seedDataHandler.RealmComponents.Where(x => x.ProviderType == UserProfileType); + var defaultConfig = seedDataHandler.GetSpecificConfiguration(ConfigurationKey.UserProfile); + if (!defaultConfig.ModificationAllowed(ModificationType.Update)) + { + return; + } var userProfile = await keycloak.GetUsersProfile(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/UsersUpdater.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/UsersUpdater.cs index f0d9a64ec3..af712e3304 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/UsersUpdater.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/UsersUpdater.cs @@ -24,35 +24,36 @@ using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.RealmsAdmin; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Roles; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Users; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Extensions; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic; -public class UsersUpdater : IUsersUpdater +public class UsersUpdater(IKeycloakFactory keycloakFactory, ISeedDataHandler seedDataHandler) + : IUsersUpdater { - private readonly IKeycloakFactory _keycloakFactory; - private readonly ISeedDataHandler _seedData; - - public UsersUpdater(IKeycloakFactory keycloakFactory, ISeedDataHandler seedDataHandler) - { - _keycloakFactory = keycloakFactory; - _seedData = seedDataHandler; - } - public async Task UpdateUsers(string keycloakInstanceName, CancellationToken cancellationToken) { - var realm = _seedData.Realm; - var keycloak = _keycloakFactory.CreateKeycloakClient(keycloakInstanceName); - var clientsDictionary = _seedData.ClientsDictionary; + var realm = seedDataHandler.Realm; + var keycloak = keycloakFactory.CreateKeycloakClient(keycloakInstanceName); + var clientsDictionary = seedDataHandler.ClientsDictionary; + var seederConfig = seedDataHandler.GetSpecificConfiguration(ConfigurationKey.Users); - foreach (var seedUser in _seedData.Users) + foreach (var seedUser in seedDataHandler.Users) { if (seedUser.Username == null) throw new ConflictException($"username must not be null {seedUser.Id}"); + var createAllowed = seederConfig.ModificationAllowed(ModificationType.Create, seedUser.Username); + var updateAllowed = seederConfig.ModificationAllowed(ModificationType.Update, seedUser.Username); + if (!createAllowed && !updateAllowed) + { + continue; + } + var user = (await keycloak.GetUsersAsync(realm, username: seedUser.Username, cancellationToken: cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)).SingleOrDefault(x => x.UserName == seedUser.Username); - if (user == null) + if (user == null && createAllowed) { var result = await keycloak.RealmPartialImportAsync(realm, CreatePartialImportUser(seedUser), cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); if (result.Overwritten != 0 || result.Added != 1 || result.Skipped != 0) @@ -60,7 +61,7 @@ public async Task UpdateUsers(string keycloakInstanceName, CancellationToken can throw new ConflictException($"PartialImport failed to add user id: {seedUser.Id}, userName: {seedUser.Username}"); } } - else + else if (user != null && updateAllowed) { await UpdateUser( keycloak, @@ -68,17 +69,21 @@ await UpdateUser( user, seedUser, clientsDictionary, + seederConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } } } - private static async Task UpdateUser(KeycloakClient keycloak, string realm, User user, UserModel seedUser, IReadOnlyDictionary clientsDictionary, CancellationToken cancellationToken) + private static async Task UpdateUser(KeycloakClient keycloak, string realm, User user, UserModel seedUser, IReadOnlyDictionary clientsDictionary, KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { if (user.Id == null) throw new ConflictException($"user.Id must not be null: userName {seedUser.Username}"); - if (!CompareUser(user, seedUser)) + if (user.UserName == null) + throw new ConflictException($"user.UserName must not be null: userName {seedUser.Username}"); + + if (!CompareUser(user, seedUser) && seederConfig.ModificationAllowed(ModificationType.Update, user.UserName)) { await keycloak.UpdateUserAsync( realm, @@ -98,8 +103,10 @@ await UpdateClientAndRealmRoles( await UpdateFederatedIdentities( keycloak, realm, + user.UserName, user.Id, seedUser.FederatedIdentities ?? Enumerable.Empty(), + seederConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } @@ -210,17 +217,19 @@ private static bool CompareFederatedIdentity(FederatedIdentity identity, Federat identity.UserId == update.UserId && identity.UserName == update.UserName; - private static async Task UpdateFederatedIdentities(KeycloakClient keycloak, string realm, string userId, IEnumerable updates, CancellationToken cancellationToken) + private static async Task UpdateFederatedIdentities(KeycloakClient keycloak, string realm, string username, string userId, IEnumerable updates, KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { var identities = await keycloak.GetUserSocialLoginsAsync(realm, userId).ConfigureAwait(ConfigureAwaitOptions.None); - await DeleteObsoleteFederatedIdentities(keycloak, realm, userId, identities, updates, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await CreateMissingFederatedIdentities(keycloak, realm, userId, identities, updates, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - await UpdateExistingFederatedIdentities(keycloak, realm, userId, identities, updates, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await DeleteObsoleteFederatedIdentities(keycloak, realm, username, userId, identities, updates, seederConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await CreateMissingFederatedIdentities(keycloak, realm, username, userId, identities, updates, seederConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await UpdateExistingFederatedIdentities(keycloak, realm, username, userId, identities, updates, seederConfig, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); } - private static async Task DeleteObsoleteFederatedIdentities(KeycloakClient keycloak, string realm, string userId, IEnumerable identities, IEnumerable updates, CancellationToken cancellationToken) + private static async Task DeleteObsoleteFederatedIdentities(KeycloakClient keycloak, string realm, string username, string userId, IEnumerable identities, IEnumerable updates, KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { - foreach (var identity in identities.ExceptBy(updates.Select(x => x.IdentityProvider), x => x.IdentityProvider)) + foreach (var identity in identities + .Where(x => seederConfig.ModificationAllowed(username, ConfigurationKey.FederatedIdentities, ModificationType.Delete, x.IdentityProvider)) + .ExceptBy(updates.Select(x => x.IdentityProvider), x => x.IdentityProvider)) { await keycloak.RemoveUserSocialLoginProviderAsync( realm, @@ -230,9 +239,11 @@ await keycloak.RemoveUserSocialLoginProviderAsync( } } - private static async Task CreateMissingFederatedIdentities(KeycloakClient keycloak, string realm, string userId, IEnumerable identities, IEnumerable updates, CancellationToken cancellationToken) + private static async Task CreateMissingFederatedIdentities(KeycloakClient keycloak, string realm, string username, string userId, IEnumerable identities, IEnumerable updates, KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { - foreach (var update in updates.ExceptBy(identities.Select(x => x.IdentityProvider), x => x.IdentityProvider)) + foreach (var update in updates + .Where(x => seederConfig.ModificationAllowed(username, ConfigurationKey.FederatedIdentities, ModificationType.Create, x.IdentityProvider)) + .ExceptBy(identities.Select(x => x.IdentityProvider), x => x.IdentityProvider)) { await keycloak.AddUserSocialLoginProviderAsync( realm, @@ -248,9 +259,10 @@ await keycloak.AddUserSocialLoginProviderAsync( } } - private static async Task UpdateExistingFederatedIdentities(KeycloakClient keycloak, string realm, string userId, IEnumerable identities, IEnumerable updates, CancellationToken cancellationToken) + private static async Task UpdateExistingFederatedIdentities(KeycloakClient keycloak, string realm, string username, string userId, IEnumerable identities, IEnumerable updates, KeycloakSeederConfigModel seederConfig, CancellationToken cancellationToken) { foreach (var (identity, update) in identities + .Where(x => seederConfig.ModificationAllowed(username, ConfigurationKey.FederatedIdentities, ModificationType.Update, x.IdentityProvider)) .Join( updates, x => x.IdentityProvider, diff --git a/src/keycloak/Keycloak.Seeding/Extensions/KeycloakRealmSettingsExtensions.cs b/src/keycloak/Keycloak.Seeding/Extensions/KeycloakRealmSettingsExtensions.cs new file mode 100644 index 0000000000..de48e354fa --- /dev/null +++ b/src/keycloak/Keycloak.Seeding/Extensions/KeycloakRealmSettingsExtensions.cs @@ -0,0 +1,56 @@ +/******************************************************************************** + * 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.Keycloak.Seeding.Models; +using System.Collections.Immutable; + +namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Extensions; + +public static class KeycloakRealmSettingsExtensions +{ + public static IReadOnlyDictionary? GetFlatDictionary(this KeycloakRealmSettings realmSettings) => + realmSettings.SeederConfigurations? + .Join( + Enum.GetValues(), + config => config.Key, + key => key.ToString(), + (SeederConfiguration config, ConfigurationKey key) => KeyValuePair.Create(key, GetFlat(config))) + .ToImmutableDictionary(); + + private static bool GetFlat(SeederConfiguration config) => + config.Create || config.Update || config.Delete || (config.SeederConfigurations != null && config.SeederConfigurations.Any(GetFlat)); + + public static SeederConfigurationModel GetConfigurationDictionaries(this KeycloakRealmSettings realmSettings) => + new( + realmSettings.Create, + realmSettings.Update, + realmSettings.Delete, + realmSettings.SeederConfigurations?.ToImmutableDictionary(sc => + sc.Key, + ConvertSeederConfigToSeederConfigurationModel)); + + private static SeederConfigurationModel ConvertSeederConfigToSeederConfigurationModel(this SeederConfiguration seederConfig) => + new( + seederConfig.Create, + seederConfig.Update, + seederConfig.Delete, + seederConfig.SeederConfigurations?.ToImmutableDictionary(sc => + sc.Key, + ConvertSeederConfigToSeederConfigurationModel)); +} diff --git a/src/keycloak/Keycloak.Seeding/Extensions/SeederConfigurationExtensions.cs b/src/keycloak/Keycloak.Seeding/Extensions/SeederConfigurationExtensions.cs new file mode 100644 index 0000000000..facbe9ff71 --- /dev/null +++ b/src/keycloak/Keycloak.Seeding/Extensions/SeederConfigurationExtensions.cs @@ -0,0 +1,88 @@ +/******************************************************************************** + * 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.Keycloak.Seeding.Models; + +namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Extensions; + +public static class SeederConfigurationExtensions +{ + public static bool ModificationAllowed(this KeycloakSeederConfigModel config, ModificationType modificationType) => + config.ModificationAllowed(modificationType, null); + + public static bool ModificationAllowed(this KeycloakSeederConfigModel config, ModificationType modificationType, string? entityKey) + { + var (defaultConfig, specificConfig) = config; + if (entityKey is null) + { + return specificConfig?.ModifyAllowed(modificationType) ?? defaultConfig.ModifyAllowed(modificationType); + } + + // If we have a configuration for a specific entry return its value + if (specificConfig?.SeederConfigurations?.TryGetValue(entityKey, out var specificEntry) ?? false) + { + return specificEntry.ModifyAllowed(modificationType); + } + + // If we don't have a specific value return the specific configuration value if we have one + return specificConfig?.ModifyAllowed(modificationType) ?? defaultConfig.ModifyAllowed(modificationType); + } + + public static bool ModificationAllowed(this KeycloakSeederConfigModel config, string containingEntityKey, ConfigurationKey configKey, ModificationType modificationType) => + config.ModificationAllowed(containingEntityKey, configKey, modificationType, null); + + public static bool ModificationAllowed(this KeycloakSeederConfigModel config, string containingEntityKey, ConfigurationKey configKey, ModificationType modificationType, string? entityKey) + { + // Check if the specific configuration contains the entity key + // e.g. for the users configuration check for a specific user configuration + if (config.SpecificConfiguration?.SeederConfigurations?.TryGetValue(containingEntityKey, out var containingEntityKeyConfiguration) ?? false) + { + // check if the specific entity configuration has a configuration for the section + // e.g. for the specific user configuration is there a section for federated identities + if (!(containingEntityKeyConfiguration.SeederConfigurations?.TryGetValue(configKey.ToString(), out var containingEntityTypeConfig) ?? false)) + { + return (config with { SpecificConfiguration = config.DefaultSettings.SeederConfigurations?.TryGetValue(configKey.ToString(), out var specificConfig) ?? false ? specificConfig : null }) + .ModificationAllowed(modificationType, entityKey); + } + + // if the entity key isn't set check the configuration for the type + if (entityKey is null) + { + return containingEntityTypeConfig.ModifyAllowed(modificationType); + } + + // If we have a configuration for a specific entry return its value otherwise take the section configuration + return (containingEntityTypeConfig.SeederConfigurations?.TryGetValue(entityKey, out var entity) ?? false ? entity?.ModifyAllowed(modificationType) : null) + ?? containingEntityTypeConfig.ModifyAllowed(modificationType); + } + + // if no configuration isn't set check the top level configuration + return (config with { SpecificConfiguration = config.DefaultSettings.SeederConfigurations?.TryGetValue(configKey.ToString(), out var topLevelSpecificConfig) ?? false ? topLevelSpecificConfig : null }) + .ModificationAllowed(modificationType, entityKey); + } + + private static bool ModifyAllowed(this SeederConfigurationModel configuration, ModificationType modificationType) => + modificationType switch + { + ModificationType.Create => configuration.Create, + ModificationType.Update => configuration.Update, + ModificationType.Delete => configuration.Delete, + _ => throw new ArgumentOutOfRangeException(nameof(modificationType), modificationType, null) + }; +} diff --git a/src/keycloak/Keycloak.Seeding/Models/ConfigurationKey.cs b/src/keycloak/Keycloak.Seeding/Models/ConfigurationKey.cs new file mode 100644 index 0000000000..edbc1b6aae --- /dev/null +++ b/src/keycloak/Keycloak.Seeding/Models/ConfigurationKey.cs @@ -0,0 +1,40 @@ +/******************************************************************************** + * 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.Models; + +public enum ConfigurationKey +{ + Roles = 1, + Localizations = 2, + UserProfile = 3, + ClientScopes = 4, + Clients = 5, + IdentityProviders = 6, + IdentityProviderMappers = 7, + Users = 8, + FederatedIdentities = 9, + ClientScopeMappers = 10, + ProtocolMappers = 11, + AuthenticationFlows = 12, + ClientProtocolMapper = 13, + ClientRoles = 14, + AuthenticationFlowExecution = 15, + AuthenticatorConfig = 16 +} diff --git a/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettings.cs b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettings.cs index 5ac852d022..bb4acfdf09 100644 --- a/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettings.cs +++ b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettings.cs @@ -32,6 +32,11 @@ public class KeycloakRealmSettings [Required] [DistinctValues] public IEnumerable DataPaths { get; set; } = null!; + public bool Create { get; set; } + public bool Update { get; set; } + public bool Delete { get; set; } + [DistinctValues("x => x.Key")] + public IEnumerable? SeederConfigurations { get; set; } public string? Id { get; set; } public string? DisplayName { get; set; } public string? DisplayNameHtml { get; set; } diff --git a/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettingsExtentions.cs b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettingsExtensions.cs similarity index 99% rename from src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettingsExtentions.cs rename to src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettingsExtensions.cs index 932badf53e..c5850d6857 100644 --- a/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettingsExtentions.cs +++ b/src/keycloak/Keycloak.Seeding/Models/KeycloakRealmSettingsExtensions.cs @@ -22,7 +22,7 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; -public static class KeycloakRealmSettingsExtentions +public static class KeycloakRealmSettingsExtensions { public static KeycloakRealm ToModel(this KeycloakRealmSettings keycloakRealmSettings) => new() diff --git a/src/keycloak/Keycloak.Seeding/Models/KeycloakSeederConfigModel.cs b/src/keycloak/Keycloak.Seeding/Models/KeycloakSeederConfigModel.cs new file mode 100644 index 0000000000..9337636143 --- /dev/null +++ b/src/keycloak/Keycloak.Seeding/Models/KeycloakSeederConfigModel.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.Models; + +public record KeycloakSeederConfigModel( + SeederConfigurationModel DefaultSettings, + SeederConfigurationModel? SpecificConfiguration +); diff --git a/src/keycloak/Keycloak.Seeding/Models/ModificationType.cs b/src/keycloak/Keycloak.Seeding/Models/ModificationType.cs new file mode 100644 index 0000000000..759faf70e2 --- /dev/null +++ b/src/keycloak/Keycloak.Seeding/Models/ModificationType.cs @@ -0,0 +1,27 @@ +/******************************************************************************** + * 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.Models; + +public enum ModificationType +{ + Create = 1, + Update = 2, + Delete = 3 +} diff --git a/src/keycloak/Keycloak.Seeding/Models/SeederConfiguration.cs b/src/keycloak/Keycloak.Seeding/Models/SeederConfiguration.cs new file mode 100644 index 0000000000..dc66e7c713 --- /dev/null +++ b/src/keycloak/Keycloak.Seeding/Models/SeederConfiguration.cs @@ -0,0 +1,35 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Org.Eclipse.TractusX.Portal.Backend.Framework.Models.Validation; +using System.ComponentModel.DataAnnotations; + +namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; + +public class SeederConfiguration +{ + [Required(AllowEmptyStrings = false)] + public string Key { get; set; } = null!; + public bool Create { get; set; } + public bool Update { get; set; } + public bool Delete { get; set; } + + [DistinctValues] + public IEnumerable? SeederConfigurations { get; set; } +} diff --git a/src/keycloak/Keycloak.Seeding/Models/SeederConfigurationModel.cs b/src/keycloak/Keycloak.Seeding/Models/SeederConfigurationModel.cs new file mode 100644 index 0000000000..9cbe4f6cef --- /dev/null +++ b/src/keycloak/Keycloak.Seeding/Models/SeederConfigurationModel.cs @@ -0,0 +1,27 @@ +/******************************************************************************** + * 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.Models; + +public record SeederConfigurationModel( + bool Create, + bool Update, + bool Delete, + IReadOnlyDictionary? SeederConfigurations +); diff --git a/src/keycloak/Keycloak.Seeding/README.md b/src/keycloak/Keycloak.Seeding/README.md new file mode 100644 index 0000000000..c9e95685b6 --- /dev/null +++ b/src/keycloak/Keycloak.Seeding/README.md @@ -0,0 +1,151 @@ +# Seeding Configuration + +The Keycloak seeder has the possibility to be configured to only create, update and delete specific types or even specific entities for each realm. +The settings for the seeding can be made via the configuration. In each role config there is the possibility to set the SeederConfiguration. + +## Default Configuration + +In the Seeder configuration you must have one Default entry where the following values needs to be set: + +**Example**: + +```json + "Realms": [ + "Create": true, + "Update": true, + "Delete": true, + "SeederConfigurations": [] + ] +``` + +with this the general logic to create, update, delete entries can either be enabled or disabled. + +## Type Specific Configuration + +To be able to enable or disable the functionality for specific types the SeederConfigurations array in the seeder configuration can be used. + +**Example**: + +```json + "SeederConfigurations": [ + { + "Key": "Localizations", + "Create": false, + "Update": false, + "Delete": false, + } + ] +``` + +with this example configuration all entities would be created, updated and deleted, but for all entities that are `Localization` the seeding wouldn't do anything. + +### Possible Types + +The following types can be configured: + +- `Roles` +- `Localizations` +- `UserProfile` +- `ClientScopes` +- `Clients` +- `IdentityProviders` +- `IdentityProviderMappers` +- `Users` +- `FederatedIdentities` +- `ClientScopeMappers` +- `ProtocolMappers` +- `AuthenticationFlows` +- `ClientProtocolMapper` +- `ClientRoles` +- `AuthenticationFlowExecution` +- `AuthenticatorConfig` + +## Entry Specific Configuration + +To be able to enable or disable the seeding for specific values the configuration can be adjusted as follows: + +**Example** + +```json + "SeederConfigurations": [ + { + "Key": "Localizations", + "Create": true, + "Update": false, + "Delete": true, + "SeederConfigurations": [ + { + "Key": "profile.attributes.organisation", + "Create": true, + "Update": true, + "Delete": true + } + ] + } + ] +``` + +In the example above you can see that the default settings as well as the specific type settings for update are disabled. +But for localizations with the key `profile.attributes.organisation` the update is enabled. With this option you can enable the modification specifically for only the entities you want to modify with the seeding. + +**Note**: The key defers for the specific types e.g. for `Localization` it is a string for `User` it is a uuid. Keys are case-sensitive. + +## Entity Specific Type Configurations + +For some entities there is a specific entry type configuration in place. E.g. FederatedIdentities can be configured for a specific user. + +**Example** + +```json + "SeederConfigurations": [ + { + "Key": "Users", + "Create": true, + "Update": false, + "Delete": false, + "SeederConfigurations": [ + { + "Key": "e69c1397-eee8-434a-b83b-dc7944bb9bdd", + "Create": true, + "Update": true, + "Delete": false, + "SeederConfigurations": [ + { + "Key": "FederatedIdentities", + "Create": false, + "Update": false, + "Delete": false, + "SeederConfigurations": [ + { + "Key": "CX-Operator", + "Create": true, + "Update": true, + "Delete": true + } + ] + } + ] + } + ] + } + ] +``` + +## Example Configuration + +For further reference you can have a look at the [example appsettings](./appsettings.example.json) + +## Not supported modifications + +- UserProfiles can only be updated. The deletion and creation of userProfiles isn't supported +- Clients can't be deleted since it isn't supported by the api +- IdentityProviders can't be deleted yet +- Users can't be deleted yet + +## NOTICE + +This work is licensed under the [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0). + +- SPDX-License-Identifier: Apache-2.0 +- SPDX-FileCopyrightText: 2024 Contributors to the Eclipse Foundation +- Source URL: diff --git a/src/keycloak/Keycloak.Seeding/appsettings.example.json b/src/keycloak/Keycloak.Seeding/appsettings.example.json new file mode 100644 index 0000000000..b6193cba7f --- /dev/null +++ b/src/keycloak/Keycloak.Seeding/appsettings.example.json @@ -0,0 +1,530 @@ +{ + "Serilog": { + "Using": [ "Serilog.Sinks.Console" ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "WriteTo": [ + { "Name": "Console" } + ], + "Enrich": [ + "FromLogContext", + "WithMachineName", + "WithProcessId", + "WithThreadId", + "WithCorrelationId" + ], + "Properties": { + "Application": "Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding" + } + }, + "Keycloak": { + "central": { + "ConnectionString": "https://centralidp.tx.test", + "User": "admin", + "Password": "testPw", + "AuthRealm": "master", + "UseAuthTrail": true + } + }, + "KeycloakSeeding": { + "Realms": [ + { + "Realm": "CX-Central", + "InstanceName": "central", + "DataPaths": [ + "Seeding/CX-Central-realm.json" + ], + "Create": true, + "Update": false, + "Delete": true, + "SeederConfigurations": [ + { + "Key": "Roles", + "Create": false, + "Update": false, + "Delete": false, + "SeederConfigurations": [] + }, + { + "Key": "Localizations", + "Create": true, + "Update": false, + "Delete": true, + "SeederConfigurations": [ + { + "Key": "profile.attributes.organisation", + "Create": true, + "Update": true, + "Delete": true + } + ] + }, + { + "Key": "UserProfile", + "Create": false, + "Update": true, + "Delete": false + }, + { + "Key": "ClientScopes", + "Create": true, + "Update": true, + "Delete": false + }, + { + "Key": "Clients", + "Create": true, + "Update": true, + "Delete": false + }, + { + "Key": "IdentityProviders", + "Create": true, + "Update": true, + "Delete": false + }, + { + "Key": "IdentityProviderMappers", + "Create": true, + "Update": true, + "Delete": true + }, + { + "Key": "Users", + "Create": true, + "Update": false, + "Delete": false, + "SeederConfigurations": [ + { + "Key": "e69c1397-eee8-434a-b83b-dc7944bb9bdd", + "Create": true, + "Update": true, + "Delete": false, + "SeederConfigurations": [ + { + "Key": "FederatedIdentities", + "Create": false, + "Update": false, + "Delete": false, + "SeederConfigurations": [ + { + "Key": "CX-Operator", + "Create": true, + "Update": true, + "Delete": true + } + ] + } + ] + } + ] + }, + { + "Key": "FederatedIdentities", + "Create": false, + "Update": false, + "Delete": false, + "SeederConfigurations": [] + }, + { + "Key": "ClientScopeMappers", + "Create": true, + "Update": true, + "Delete": true, + "SeederConfigurations": [] + }, + { + "Key": "ProtocolMappers", + "Create": true, + "Update": true, + "Delete": true, + "SeederConfigurations": [] + }, + { + "Key": "AuthenticationFlows", + "Create": true, + "Update": true, + "Delete": true, + "SeederConfigurations": [] + }, + { + "Key": "AuthenticationFlowExecution", + "Create": true, + "Update": true, + "Delete": true, + "SeederConfigurations": [] + }, + { + "Key": "ClientProtocolMappers", + "Create": true, + "Update": true, + "Delete": true, + "SeederConfigurations": [] + }, + { + "Key": "ClientRoles", + "Create": true, + "Update": true, + "Delete": true, + "SeederConfigurations": [] + }, + { + "Key": "AuthenticatorConfig", + "Create": true, + "Update": true, + "Delete": true, + "SeederConfigurations": [] + } + ], + "Clients": [ + { + "ClientId": "Cl1-CX-Registration", + "RedirectUris": [ + "http://portal.tx.test/*", + "http://localhost:3000/*" + ] + }, + { + "ClientId": "Cl2-CX-Portal", + "RedirectUris": [ + "http://portal.tx.test/*", + "http://localhost:3000/*" + ], + "RootUrl": "http://portal.tx.test/home" + }, + { + "ClientId": "Cl3-CX-Semantic", + "RedirectUris": [ + "http://portal.tx.test/*" + ], + "RootUrl": "http://portal.tx.test/home" + }, + { + "ClientId": "Cl5-CX-Custodian", + "RedirectUris": [ + "http://managed-identity-wallets.tx.test/*" + ], + "Secret": "test" + }, + { + "ClientId": "Cl7-CX-BPDM", + "RedirectUris": [ + "http://partners-pool.tx.test/*" + ], + "Secret": "test" + }, + { + "ClientId": "Cl16-CX-BPDMGate", + "RedirectUris": [ + "http://partners-gate.tx.test/*" + ], + "Secret": "test" + }, + { + "ClientId": "Cl25-CX-BPDM-Orchestrator", + "Secret": "test" + }, + { + "ClientId": "sa-cl1-reg-2", + "Secret": "test" + }, + { + "ClientId": "sa-cl2-01", + "Secret": "test" + }, + { + "ClientId": "sa-cl2-02", + "Secret": "test" + }, + { + "ClientId": "sa-cl2-03", + "Secret": "test" + }, + { + "ClientId": "sa-cl2-04", + "Secret": "test" + }, + { + "ClientId": "sa-cl2-05", + "Secret": "test" + }, + { + "ClientId": "sa-cl3-cx-1", + "Secret": "test" + }, + { + "ClientId": "sa-cl5-custodian-2", + "Secret": "test" + }, + { + "ClientId": "sa-cl7-cx-1", + "Secret": "test" + }, + { + "ClientId": "sa-cl7-cx-5", + "Secret": "test" + }, + { + "ClientId": "sa-cl7-cx-7", + "Secret": "test" + }, + { + "ClientId": "sa-cl8-cx-1", + "Secret": "test" + }, + { + "ClientId": "sa-cl21-01", + "Secret": "test" + }, + { + "ClientId": "sa-cl22-01", + "Secret": "test" + }, + { + "ClientId": "sa-cl24-01", + "Secret": "test" + }, + { + "ClientId": "sa-cl25-cx-1", + "Secret": "test" + }, + { + "ClientId": "sa-cl25-cx-2", + "Secret": "test" + }, + { + "ClientId": "sa-cl25-cx-3", + "Secret": "test" + } + ], + "IdentityProviders": [ + { + "Alias": "CX-Operator", + "Config": { + "AuthorizationUrl": "http://sharedidp.tx.test/auth/realms/CX-Operator/protocol/openid-connect/auth", + "JwksUrl": "http://sharedidp.tx.test/auth/realms/CX-Operator/protocol/openid-connect/certs", + "LogoutUrl": "http://sharedidp.tx.test/auth/realms/CX-Operator/protocol/openid-connect/logout", + "TokenUrl": "http://sharedidp.tx.test/auth/realms/CX-Operator/protocol/openid-connect/token" + } + } + ], + "Users": [ + { + "Username": "ac1cf001-7fbc-1f2f-817f-bce058020006", + "Attributes": [ + { + "Name": "bpn", + "Values": [ + "BPNL00000003CRHK" + ] + } + ] + }, + { + "Username": "service-account-sa-cl1-reg-2", + "Attributes": [ + { + "Name": "bpn", + "Values": [ + "BPNL00000003CRHK" + ] + } + ] + }, + { + "Username": "service-account-sa-cl7-cx-5", + "Attributes": [ + { + "Name": "bpn", + "Values": [ + "BPNL00000003CRHK" + ] + } + ] + }, + { + "Username": "service-account-sa-cl7-cx-7", + "Attributes": [ + { + "Name": "bpn", + "Values": [ + "BPNL00000003CRHK" + ] + } + ] + }, + { + "Username": "service-account-sa-cl8-cx-1", + "Attributes": [ + { + "Name": "bpn", + "Values": [ + "BPNL00000003CRHK" + ] + } + ] + }, + { + "Username": "service-account-sa-cl21-01", + "Attributes": [ + { + "Name": "bpn", + "Values": [ + "BPNL00000003CRHK" + ] + } + ] + }, + { + "Username": "service-account-sa-cl22-01", + "Attributes": [ + { + "Name": "bpn", + "Values": [ + "BPNL00000003CRHK" + ] + } + ] + }, + { + "Username": "service-account-sa-cl24-01", + "Attributes": [ + { + "Name": "bpn", + "Values": [ + "BPNL00000003CRHK" + ] + } + ] + }, + { + "Username": "service-account-sa-cl25-cx-1", + "Attributes": [ + { + "Name": "bpn", + "Values": [ + "BPNL00000003CRHK" + ] + } + ] + }, + { + "Username": "service-account-sa-cl25-cx-2", + "Attributes": [ + { + "Name": "bpn", + "Values": [ + "BPNL00000003CRHK" + ] + } + ] + }, + { + "Username": "service-account-sa-cl25-cx-3", + "Attributes": [ + { + "Name": "bpn", + "Values": [ + "BPNL00000003CRHK" + ] + } + ] + }, + { + "Username": "service-account-sa-cl2-01", + "Attributes": [ + { + "Name": "bpn", + "Values": [ + "BPNL00000003CRHK" + ] + } + ] + }, + { + "Username": "service-account-sa-cl2-02", + "Attributes": [ + { + "Name": "bpn", + "Values": [ + "BPNL00000003CRHK" + ] + } + ] + }, + { + "Username": "service-account-sa-cl2-03", + "Attributes": [ + { + "Name": "bpn", + "Values": [ + "BPNL00000003CRHK" + ] + } + ] + }, + { + "Username": "service-account-sa-cl2-04", + "Attributes": [ + { + "Name": "bpn", + "Values": [ + "BPNL00000003CRHK" + ] + } + ] + }, + { + "Username": "service-account-sa-cl2-05", + "Attributes": [ + { + "Name": "bpn", + "Values": [ + "BPNL00000003CRHK" + ] + } + ] + }, + { + "Username": "service-account-sa-cl3-cx-1", + "Attributes": [ + { + "Name": "bpn", + "Values": [ + "BPNL00000003CRHK" + ] + } + ] + }, + { + "Username": "service-account-sa-cl5-custodian-2", + "Attributes": [ + { + "Name": "bpn", + "Values": [ + "BPNL00000003CRHK" + ] + } + ] + }, + { + "Username": "service-account-sa-cl7-cx-1", + "Attributes": [ + { + "Name": "bpn", + "Values": [ + "BPNL00000003CRHK" + ] + } + ] + } + ] + } + ] + } +} diff --git a/src/keycloak/Keycloak.Seeding/appsettings.json b/src/keycloak/Keycloak.Seeding/appsettings.json index eba5322645..9d9c34c565 100644 --- a/src/keycloak/Keycloak.Seeding/appsettings.json +++ b/src/keycloak/Keycloak.Seeding/appsettings.json @@ -36,7 +36,10 @@ { "Realm": "", "InstanceName": "", - "DataPaths": [] + "DataPaths": [], + "Create": true, + "Update": true, + "Delete": true } ] } diff --git a/tests/keycloak/Keycloak.Seeding.Tests/Extensions/KeycloakRealmSettingsTests.cs b/tests/keycloak/Keycloak.Seeding.Tests/Extensions/KeycloakRealmSettingsTests.cs new file mode 100644 index 0000000000..7152596a6e --- /dev/null +++ b/tests/keycloak/Keycloak.Seeding.Tests/Extensions/KeycloakRealmSettingsTests.cs @@ -0,0 +1,189 @@ +/******************************************************************************** + * 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.Keycloak.Seeding.Extensions; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; + +namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Tests.Extensions; + +using System.Collections.Generic; +using Xunit; + +public class KeycloakRealmSettingsTests +{ + [Fact] + public void GetFlatDictionary_WithInDepthConfiguration_DeeperOneIsTaken() + { + // Arrange + var realmSettings = new KeycloakRealmSettings + { + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = "Roles", + Create = true, + Update = false, + Delete = false + }, + new() + { + Key = "Localizations", + Create = false, + Update = true, + Delete = false + }, + new() + { + Key = "UserProfile", + Create = false, + Update = false, + Delete = true + }, + new() + { + Key = "FederatedIdentities", + Create = false, + Update = false, + Delete = false + }, + new() + { + Key = "FEDERATEDIdentities", + Create = true, + Update = true, + Delete = true + }, + new() + { + Key = "Users", + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = "testUser1", + Create = false, + Update = false, + Delete = false + }, + new() + { + Key = "testUser2", + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = "FederatedIdentities", + Create = true, + Update = true, + Delete = false + } + ] + } + ] + }, + new() + { + Key = "Clients", + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = "testClient", + Create = false, + Update = false, + Delete = false + } + ] + } + ] + }; + + // Act + var result = realmSettings.GetFlatDictionary(); + + // Assert + result.Should().HaveCount(6).And.Satisfy( + x => x.Key == ConfigurationKey.Roles && x.Value, + x => x.Key == ConfigurationKey.Localizations && x.Value, + x => x.Key == ConfigurationKey.UserProfile && x.Value, + x => x.Key == ConfigurationKey.FederatedIdentities && !x.Value, + x => x.Key == ConfigurationKey.Users && x.Value, + x => x.Key == ConfigurationKey.Clients && !x.Value + ); + } + + [Fact] + public void GetConfigurationDictionaries_WithNestedConfigurations_ReturnsExpected() + { + // Arrange + var realmSettings = new KeycloakRealmSettings + { + Create = true, + Update = false, + Delete = true, + SeederConfigurations = + [ + new() + { + Key = "Users", + Create = true, + Update = false, + Delete = true, + SeederConfigurations = + [ + new() + { + Key = "testUser", + Create = false, + Update = true, + Delete = false + } + ] + } + ] + }; + + // Act + var result = realmSettings.GetConfigurationDictionaries(); + + // Assert + result.Create.Should().BeTrue(); + result.Update.Should().BeFalse(); + result.Delete.Should().BeTrue(); + result.SeederConfigurations.Should().ContainSingle().And.Satisfy( + x => x.Key == "Users" && x.Value.Create && !x.Value.Update && x.Value.Delete && + x.Value.SeederConfigurations != null && x.Value.SeederConfigurations.Count == 1 && x.Value.SeederConfigurations.ContainsKey("testUser") && + !x.Value.SeederConfigurations.Single().Value.Create && + x.Value.SeederConfigurations.Single().Value.Update && + !x.Value.SeederConfigurations.Single().Value.Delete); + } +} diff --git a/tests/keycloak/Keycloak.Seeding.Tests/Extensions/SeederConfigurationExtensionsTests.cs b/tests/keycloak/Keycloak.Seeding.Tests/Extensions/SeederConfigurationExtensionsTests.cs new file mode 100644 index 0000000000..3c3a0f1f8c --- /dev/null +++ b/tests/keycloak/Keycloak.Seeding.Tests/Extensions/SeederConfigurationExtensionsTests.cs @@ -0,0 +1,511 @@ +/******************************************************************************** + * 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.Keycloak.Seeding.Extensions; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; + +namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Tests.Extensions; + +public class SeederConfigurationExtensionsTests +{ + [Fact] + public void ModifyAllowed_InvalidModificationType_ThrowsException() + { + var defaultConfig = new SeederConfigurationModel(false, false, false, new Dictionary()); + var config = new KeycloakSeederConfigModel(defaultConfig, null); + + Assert.Throws(() => config.ModificationAllowed((ModificationType)666)); + } + + [Theory] + [InlineData(true, false, false, ModificationType.Create, true)] + [InlineData(false, false, false, ModificationType.Create, false)] + [InlineData(false, true, false, ModificationType.Update, true)] + [InlineData(false, false, false, ModificationType.Update, false)] + [InlineData(false, false, true, ModificationType.Delete, true)] + [InlineData(false, false, false, ModificationType.Delete, false)] + public void ModifyAllowed_WithExpected_ReturnsExpected(bool create, bool update, bool delete, ModificationType modificationType, bool expectedResult) + { + var defaultConfig = new SeederConfigurationModel(create, update, delete, new Dictionary()); + var config = new KeycloakSeederConfigModel(defaultConfig, null); + + var result = config.ModificationAllowed(modificationType); + + result.Should().Be(expectedResult); + } + + [Fact] + public void ModificationAllowed_DefaultConfigAllows_ReturnsTrue() + { + var settings = new KeycloakRealmSettings + { + Create = true, + Update = false, + Delete = false + }; + var defaultSettings = settings.GetConfigurationDictionaries(); + var config = new KeycloakSeederConfigModel(defaultSettings, null); + + var result = config.ModificationAllowed(ModificationType.Create); + + result.Should().BeTrue(); + } + + [Fact] + public void ModificationAllowed_DefaultConfigDeniesCreate_ReturnsFalse() + { + var settings = new KeycloakRealmSettings + { + Create = false, + Update = false, + Delete = false + }; + var defaultSettings = settings.GetConfigurationDictionaries(); + var config = new KeycloakSeederConfigModel(defaultSettings, null); + + var result = config.ModificationAllowed(ModificationType.Create); + + result.Should().BeFalse(); + } + + [Fact] + public void ModificationAllowed_WithSpecificConfigurationOverwrites_ReturnsSpecificConfiguration() + { + var settings = new KeycloakRealmSettings + { + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = ConfigurationKey.Users.ToString(), + Create = true, + Update = false, + Delete = false + } + ] + }; + var defaultSettings = settings.GetConfigurationDictionaries(); + defaultSettings.SeederConfigurations.Should().NotBeNull(); + + var config = new KeycloakSeederConfigModel(defaultSettings, defaultSettings.SeederConfigurations!["Users"]); + + var result = config.ModificationAllowed(ModificationType.Create); + + result.Should().BeTrue(); + } + + [Fact] + public void ModificationAllowed_WithEntityKeySpecificConfig_ReturnsEntitySpecificKey() + { + var settings = new KeycloakRealmSettings + { + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = ConfigurationKey.Users.ToString(), + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = "testUser", + Create = true, + Update = false, + Delete = false + } + ] + } + ] + }; + var defaultSettings = settings.GetConfigurationDictionaries(); + defaultSettings.SeederConfigurations.Should().NotBeNull(); + + var config = new KeycloakSeederConfigModel(defaultSettings, defaultSettings.SeederConfigurations!["Users"]); + + var result = config.ModificationAllowed(ModificationType.Create, "testUser"); + + result.Should().BeTrue(); + } + + [Fact] + public void ModificationAllowed_EntityKeyNotInSpecificConfig_UsesSpecificConfig() + { + var settings = new KeycloakRealmSettings + { + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = ConfigurationKey.Users.ToString(), + Create = true, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = "testUser", + Create = false, + Update = false, + Delete = false + } + ] + } + ] + }; + var defaultSettings = settings.GetConfigurationDictionaries(); + defaultSettings.SeederConfigurations.Should().NotBeNull(); + + var config = new KeycloakSeederConfigModel(defaultSettings, defaultSettings.SeederConfigurations!["Users"]); + + var result = config.ModificationAllowed(ModificationType.Create, "nonexistent"); + + result.Should().BeTrue(); + } + + [Fact] + public void ModificationAllowed_NestedEntityConfigAllowsCreate_ReturnsTrue() + { + var settings = new KeycloakRealmSettings + { + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = ConfigurationKey.Users.ToString(), + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = "testUser", + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = ConfigurationKey.FederatedIdentities.ToString(), + Create = true, + Update = false, + Delete = false, + } + ] + } + ] + } + ] + }; + var defaultSettings = settings.GetConfigurationDictionaries(); + defaultSettings.SeederConfigurations.Should().NotBeNull(); + + var config = new KeycloakSeederConfigModel(defaultSettings, defaultSettings.SeederConfigurations!["Users"]); + + var result = config.ModificationAllowed("testUser", ConfigurationKey.FederatedIdentities, ModificationType.Create); + + result.Should().BeTrue(); + } + + [Fact] + public void ModificationAllowed_NestedSpecificEntityConfigAllowsCreate_ReturnsTrue() + { + var settings = new KeycloakRealmSettings + { + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = ConfigurationKey.Users.ToString(), + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = "testUser", + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = ConfigurationKey.FederatedIdentities.ToString(), + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = "fi1", + Create = true, + Update = false, + Delete = false, + } + ] + } + ] + } + ] + } + ] + }; + var defaultSettings = settings.GetConfigurationDictionaries(); + defaultSettings.SeederConfigurations.Should().NotBeNull(); + + var config = new KeycloakSeederConfigModel(defaultSettings, defaultSettings.SeederConfigurations!["Users"]); + + var result = config.ModificationAllowed("testUser", ConfigurationKey.FederatedIdentities, ModificationType.Create, "fi1"); + + result.Should().BeTrue(); + } + + [Fact] + public void ModificationAllowed_NestedSpecificEntityNotFoundConfigAllowsCreate_ReturnsNestedEntity() + { + var settings = new KeycloakRealmSettings + { + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = ConfigurationKey.Users.ToString(), + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = "testUser", + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = ConfigurationKey.FederatedIdentities.ToString(), + Create = true, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = "fi1", + Create = false, + Update = false, + Delete = false, + } + ] + } + ] + } + ] + } + ] + }; + var defaultSettings = settings.GetConfigurationDictionaries(); + defaultSettings.SeederConfigurations.Should().NotBeNull(); + + var config = new KeycloakSeederConfigModel(defaultSettings, defaultSettings.SeederConfigurations!["Users"]); + + var result = config.ModificationAllowed("testUser", ConfigurationKey.FederatedIdentities, ModificationType.Create, "finotfound"); + + result.Should().BeTrue(); + } + + [Fact] + public void ModificationAllowed_WithoutNestedConfigAndSpecificEntry_ReturnsTopLevelSpecific() + { + var settings = new KeycloakRealmSettings + { + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = ConfigurationKey.Users.ToString(), + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = "testUser", + Create = false, + Update = false, + Delete = false + } + ] + }, + new() + { + Key = ConfigurationKey.FederatedIdentities.ToString(), + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = "fi", + Create = true, + Update = false, + Delete = false, + } + ] + } + ] + }; + var defaultSettings = settings.GetConfigurationDictionaries(); + defaultSettings.SeederConfigurations.Should().NotBeNull(); + + var config = new KeycloakSeederConfigModel(defaultSettings, defaultSettings.SeederConfigurations!["Users"]); + + var result = config.ModificationAllowed("testUser", ConfigurationKey.FederatedIdentities, ModificationType.Create, "fi"); + + result.Should().BeTrue(); + } + + [Fact] + public void ModificationAllowed_WithoutNestedConfig_ReturnsTopLevel() + { + var settings = new KeycloakRealmSettings + { + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = ConfigurationKey.Users.ToString(), + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = "testUser", + Create = false, + Update = false, + Delete = false + } + ] + }, + new() + { + Key = ConfigurationKey.FederatedIdentities.ToString(), + Create = true, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = "fi", + Create = false, + Update = false, + Delete = false, + } + ] + } + ] + }; + var defaultSettings = settings.GetConfigurationDictionaries(); + defaultSettings.SeederConfigurations.Should().NotBeNull(); + + var config = new KeycloakSeederConfigModel(defaultSettings, defaultSettings.SeederConfigurations!["Users"]); + + var result = config.ModificationAllowed("testUser", ConfigurationKey.FederatedIdentities, ModificationType.Create, "missing"); + + result.Should().BeTrue(); + } + + [Fact] + public void ModificationAllowed_WithoutConfig_ReturnsDefault() + { + var settings = new KeycloakRealmSettings + { + Create = true, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = ConfigurationKey.Users.ToString(), + Create = false, + Update = false, + Delete = false, + SeederConfigurations = + [ + new() + { + Key = "testUser", + Create = false, + Update = false, + Delete = false + } + ] + } + ] + }; + var defaultSettings = settings.GetConfigurationDictionaries(); + defaultSettings.SeederConfigurations.Should().NotBeNull(); + + var config = new KeycloakSeederConfigModel(defaultSettings, defaultSettings.SeederConfigurations!["Users"]); + + var result = config.ModificationAllowed("xy", ConfigurationKey.FederatedIdentities, ModificationType.Create, "missing"); + + result.Should().BeTrue(); + } +} diff --git a/tests/keycloak/Keycloak.Seeding.Tests/KeycloakRealmModelTests.cs b/tests/keycloak/Keycloak.Seeding.Tests/KeycloakRealmModelTests.cs index b88d09090d..afa2dfa0a5 100644 --- a/tests/keycloak/Keycloak.Seeding.Tests/KeycloakRealmModelTests.cs +++ b/tests/keycloak/Keycloak.Seeding.Tests/KeycloakRealmModelTests.cs @@ -20,6 +20,7 @@ using Org.Eclipse.TractusX.Portal.Backend.Framework.Linq; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; + namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Tests; public class KeycloakRealmModelTests