From dfeca44873b36268d15bdb471f987a5ea0443708 Mon Sep 17 00:00:00 2001 From: Phil Schneider Date: Tue, 10 Oct 2023 12:34:22 +0200 Subject: [PATCH] feat(n2n): move keycloak user creation to process (#280) * feat(n2n): move keycloak user creation to process Refs: CPLP-3295 * feat(n2n): add idp to welcome mails Refs: CPLP-2639 --------- Co-authored-by: Norbert Truchsess Reviewed-by: Norbert Truchsess --- .../BusinessLogic/NetworkBusinessLogic.cs | 46 +++++------- .../BusinessLogic/UserBusinessLogic.cs | 1 + .../EmailTemplates/osp_welcome_email.html | 2 +- .../Repositories/INetworkRepository.cs | 1 + .../Repositories/NetworkRepository.cs | 5 ++ .../NetworkRegistrationHandlerExtensions.cs | 5 +- .../INetworkRegistrationHandler.cs | 1 - .../INetworkRegistrationProcessHelper.cs | 1 - .../Models/UserMailInformation.cs | 24 +++++++ .../NetworkRegistration.Library.csproj | 1 + .../NetworkRegistrationHandler.cs | 62 ++++++++++++---- .../NetworkRegistrationProcessHelper.cs | 2 - .../Service/IUserProvisioningService.cs | 5 +- .../Service/UserProvisioningService.cs | 72 ++++++++++++------- .../NetworkBusinessLogicTests.cs | 19 ++--- .../NetworkRegistrationHandlerTests.cs | 48 ++++++++++--- 16 files changed, 200 insertions(+), 95 deletions(-) create mode 100644 src/processes/NetworkRegistration.Library/Models/UserMailInformation.cs diff --git a/src/administration/Administration.Service/BusinessLogic/NetworkBusinessLogic.cs b/src/administration/Administration.Service/BusinessLogic/NetworkBusinessLogic.cs index a181587251..51522cb5b7 100644 --- a/src/administration/Administration.Service/BusinessLogic/NetworkBusinessLogic.cs +++ b/src/administration/Administration.Service/BusinessLogic/NetworkBusinessLogic.cs @@ -23,7 +23,6 @@ using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; using Org.Eclipse.TractusX.Portal.Backend.Framework.Linq; using Org.Eclipse.TractusX.Portal.Backend.Framework.Models; -using Org.Eclipse.TractusX.Portal.Backend.Mailing.SendMail; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Models; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Repositories; @@ -46,16 +45,14 @@ public class NetworkBusinessLogic : INetworkBusinessLogic private readonly IIdentityService _identityService; private readonly IUserProvisioningService _userProvisioningService; private readonly INetworkRegistrationProcessHelper _processHelper; - private readonly IMailingService _mailingService; private readonly PartnerRegistrationSettings _settings; - public NetworkBusinessLogic(IPortalRepositories portalRepositories, IIdentityService identityService, IUserProvisioningService userProvisioningService, INetworkRegistrationProcessHelper processHelper, IMailingService mailingService, IOptions options) + public NetworkBusinessLogic(IPortalRepositories portalRepositories, IIdentityService identityService, IUserProvisioningService userProvisioningService, INetworkRegistrationProcessHelper processHelper, IOptions options) { _portalRepositories = portalRepositories; _identityService = identityService; _userProvisioningService = userProvisioningService; _processHelper = processHelper; - _mailingService = mailingService; _settings = options.Value; } @@ -69,8 +66,6 @@ public async Task HandlePartnerRegistration(PartnerRegistrationData data) var (roleData, identityProviderIdAliase, singleIdentityProviderIdAlias, allIdentityProviderIds) = await ValidatePartnerRegistrationData(data, networkRepository, identityProviderRepository, ownerCompanyId).ConfigureAwait(false); - var (_, companyName) = await companyRepository.GetCompanyNameUntrackedAsync(ownerCompanyId).ConfigureAwait(false); - var companyId = CreatePartnerCompany(companyRepository, data); var applicationId = _portalRepositories.GetInstance().CreateCompanyApplication(companyId, CompanyApplicationStatusId.CREATED, CompanyApplicationTypeId.EXTERNAL, @@ -96,13 +91,27 @@ string GetIdpAlias(Guid? identityProviderId) => ? singleIdentityProviderIdAlias?.Alias ?? throw new UnexpectedConditionException("singleIdentityProviderIdAlias should never be null here") : identityProviderIdAliase?[identityProviderId.Value] ?? throw new UnexpectedConditionException("identityProviderIdAliase should never be null here and should always contain an entry for identityProviderId"); - async IAsyncEnumerable<(Guid CompanyUserId, string UserName, string? Password, Exception? Error)> CreateUsers() + async IAsyncEnumerable<(Guid CompanyUserId, Exception? Error)> CreateUsers() { - foreach (var user in GetUserCreationData(companyId, GetIdpId, GetIdpAlias, data, roleData)) + var userRepository = _portalRepositories.GetInstance(); + await foreach (var (aliasData, creationInfos) in GetUserCreationData(companyId, GetIdpId, GetIdpAlias, data, roleData).ToAsyncEnumerable()) { - await foreach (var result in _userProvisioningService.CreateOwnCompanyIdpUsersAsync(user.AliasData, user.CreationInfos.ToAsyncEnumerable())) + foreach (var creationInfo in creationInfos) { - yield return result; + var identityId = Guid.Empty; + Exception? error = null; + try + { + var (_, companyUserId) = await _userProvisioningService.GetOrCreateCompanyUser(userRepository, aliasData.IdpAlias, + creationInfo, companyId, aliasData.IdpId, data.Bpn).ConfigureAwait(false); + identityId = companyUserId; + } + catch (Exception ex) + { + error = ex; + } + + yield return (identityId, error); } } } @@ -111,7 +120,6 @@ string GetIdpAlias(Guid? identityProviderId) => userCreationErrors.IfAny(errors => throw new ServiceException($"Errors occured while saving the users: ${string.Join("", errors.Select(x => x.Message))}", errors.First())); await _portalRepositories.SaveAsync().ConfigureAwait(false); - await SendMails(data.UserDetails.Select(x => new ValueTuple(x.Email, x.FirstName, x.LastName)), companyName).ConfigureAwait(false); } private Guid CreatePartnerCompany(ICompanyRepository companyRepository, PartnerRegistrationData data) @@ -167,22 +175,6 @@ private Guid CreatePartnerCompany(ICompanyRepository companyRepository, PartnerR return (AliasData: companyNameIdpAliasData, CreationInfos: userCreationInfos); }); - private async Task SendMails(IEnumerable<(string Email, string? FirstName, string? LastName)> companyUserWithRoleIdForCompany, string ospName) - { - foreach (var (receiver, firstName, lastName) in companyUserWithRoleIdForCompany) - { - var userName = string.Join(" ", firstName, lastName); - var mailParameters = new Dictionary - { - { "userName", !string.IsNullOrWhiteSpace(userName) ? userName : receiver }, - { "hostname", _settings.BasePortalAddress }, - { "osp", ospName }, - { "url", _settings.BasePortalAddress } - }; - await _mailingService.SendMails(receiver, mailParameters, Enumerable.Repeat("OspWelcomeMail", 1)).ConfigureAwait(false); - } - } - public Task RetriggerProcessStep(Guid externalId, ProcessStepTypeId processStepTypeId) => _processHelper.TriggerProcessStep(externalId, processStepTypeId); diff --git a/src/administration/Administration.Service/BusinessLogic/UserBusinessLogic.cs b/src/administration/Administration.Service/BusinessLogic/UserBusinessLogic.cs index bf8ad2fa48..18b9d3fc79 100644 --- a/src/administration/Administration.Service/BusinessLogic/UserBusinessLogic.cs +++ b/src/administration/Administration.Service/BusinessLogic/UserBusinessLogic.cs @@ -154,6 +154,7 @@ private Task> GetOwnCompanyUserRoleData(IEnumerable()); } + return _userProvisioningService.GetOwnCompanyPortalRoleDatas(_settings.Portal.KeycloakClientID, roles, companyId); } diff --git a/src/mailing/Mailing.Template/EmailTemplates/osp_welcome_email.html b/src/mailing/Mailing.Template/EmailTemplates/osp_welcome_email.html index b78482a1bc..f4eea5098a 100644 --- a/src/mailing/Mailing.Template/EmailTemplates/osp_welcome_email.html +++ b/src/mailing/Mailing.Template/EmailTemplates/osp_welcome_email.html @@ -92,7 +92,7 @@ style="Margin:0;padding-top:20px;padding-bottom:20px;padding-left:30px;padding-right:30px;text-align: left;">

- Dear {userName},

your registration at the Catena-X dataspace got based on your request successfully generated by {osp}.

We have created your registration request. Before the registration validation is taking place, a final check from your side confirming your registration data and confirming the terms & conditions is needed.

Please follow the link below to access your registration data and to confirm the company role related terms & conditions.
You may want to update the company roles by selecting additional roles.

+ Dear {userName},

your registration at the Catena-X dataspace got based on your request successfully generated by {osp} for idps {idpAliasse}.

We have created your registration request. Before the registration validation is taking place, a final check from your side confirming your registration data and confirming the terms & conditions is needed.

Please follow the link below to access your registration data and to confirm the company role related terms & conditions.
You may want to update the company roles by selecting additional roles.

diff --git a/src/portalbackend/PortalBackend.DBAccess/Repositories/INetworkRepository.cs b/src/portalbackend/PortalBackend.DBAccess/Repositories/INetworkRepository.cs index 90a6f94073..46e4991a9d 100644 --- a/src/portalbackend/PortalBackend.DBAccess/Repositories/INetworkRepository.cs +++ b/src/portalbackend/PortalBackend.DBAccess/Repositories/INetworkRepository.cs @@ -32,4 +32,5 @@ public interface INetworkRepository Task<(bool RegistrationIdExists, VerifyProcessData processData)> IsValidRegistration(Guid externalId, IEnumerable processStepTypeIds); Task<(bool Exists, IEnumerable<(Guid CompanyApplicationId, CompanyApplicationStatusId CompanyApplicationStatusId, string? CallbackUrl)> CompanyApplications, IEnumerable<(CompanyRoleId CompanyRoleId, IEnumerable AgreementIds)> CompanyRoleAgreementIds, Guid? ProcessId)> GetSubmitData(Guid companyId); Task<(OspDetails? OspDetails, Guid? ExternalId, string? Bpn, Guid ApplicationId, IEnumerable Comments)> GetCallbackData(Guid networkRegistrationId, ProcessStepTypeId processStepTypeId); + Task GetOspCompanyName(Guid networkRegistrationId); } diff --git a/src/portalbackend/PortalBackend.DBAccess/Repositories/NetworkRepository.cs b/src/portalbackend/PortalBackend.DBAccess/Repositories/NetworkRepository.cs index 45cf0f094e..7a7b149e59 100644 --- a/src/portalbackend/PortalBackend.DBAccess/Repositories/NetworkRepository.cs +++ b/src/portalbackend/PortalBackend.DBAccess/Repositories/NetworkRepository.cs @@ -108,4 +108,9 @@ public Task GetNetworkRegistrationDataForProcessIdAsync(Guid processId) => .Select(step => step.Message!) : new List())) .SingleOrDefaultAsync(); + + public Task GetOspCompanyName(Guid networkRegistrationId) => + _context.NetworkRegistrations.Where(x => x.Id == networkRegistrationId) + .Select(x => x.OnboardingServiceProvider!.Name) + .SingleOrDefaultAsync(); } diff --git a/src/processes/NetworkRegistration.Library/DependencyInjection/NetworkRegistrationHandlerExtensions.cs b/src/processes/NetworkRegistration.Library/DependencyInjection/NetworkRegistrationHandlerExtensions.cs index 5adc8ae9f1..fd1bdb32dd 100644 --- a/src/processes/NetworkRegistration.Library/DependencyInjection/NetworkRegistrationHandlerExtensions.cs +++ b/src/processes/NetworkRegistration.Library/DependencyInjection/NetworkRegistrationHandlerExtensions.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -32,6 +31,9 @@ public class NetworkRegistrationProcessSettings [Required] [DistinctValues("x => x.ClientId")] public IEnumerable InitialRoles { get; set; } = null!; + + [Required(AllowEmptyStrings = false)] + public string BasePortalAddress { get; set; } = null!; } public static class NetworkRegistrationHandlerExtensions @@ -41,6 +43,7 @@ public static IServiceCollection AddNetworkRegistrationHandler(this IServiceColl var section = config.GetSection("NetworkRegistration"); services.AddOptions() .Bind(section) + .ValidateDataAnnotations() .ValidateDistinctValues(section) .ValidateOnStart(); diff --git a/src/processes/NetworkRegistration.Library/INetworkRegistrationHandler.cs b/src/processes/NetworkRegistration.Library/INetworkRegistrationHandler.cs index 4f4133fb06..f562464594 100644 --- a/src/processes/NetworkRegistration.Library/INetworkRegistrationHandler.cs +++ b/src/processes/NetworkRegistration.Library/INetworkRegistrationHandler.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional diff --git a/src/processes/NetworkRegistration.Library/INetworkRegistrationProcessHelper.cs b/src/processes/NetworkRegistration.Library/INetworkRegistrationProcessHelper.cs index 413d60a6ec..8c014ee160 100644 --- a/src/processes/NetworkRegistration.Library/INetworkRegistrationProcessHelper.cs +++ b/src/processes/NetworkRegistration.Library/INetworkRegistrationProcessHelper.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional diff --git a/src/processes/NetworkRegistration.Library/Models/UserMailInformation.cs b/src/processes/NetworkRegistration.Library/Models/UserMailInformation.cs new file mode 100644 index 0000000000..d07af71a83 --- /dev/null +++ b/src/processes/NetworkRegistration.Library/Models/UserMailInformation.cs @@ -0,0 +1,24 @@ +/******************************************************************************** + * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using System.Collections.Generic; + +namespace Org.Eclipse.TractusX.Portal.Backend.Processes.NetworkRegistration.Library.Models; + +public record UserMailInformation(string Email, string? FirstName, string? LastName, IEnumerable IdpAliasse); diff --git a/src/processes/NetworkRegistration.Library/NetworkRegistration.Library.csproj b/src/processes/NetworkRegistration.Library/NetworkRegistration.Library.csproj index 4db5590cf9..9babed5eed 100644 --- a/src/processes/NetworkRegistration.Library/NetworkRegistration.Library.csproj +++ b/src/processes/NetworkRegistration.Library/NetworkRegistration.Library.csproj @@ -29,6 +29,7 @@ + diff --git a/src/processes/NetworkRegistration.Library/NetworkRegistrationHandler.cs b/src/processes/NetworkRegistration.Library/NetworkRegistrationHandler.cs index ce2df221d0..62e95ead15 100644 --- a/src/processes/NetworkRegistration.Library/NetworkRegistrationHandler.cs +++ b/src/processes/NetworkRegistration.Library/NetworkRegistrationHandler.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -20,10 +19,12 @@ using Microsoft.Extensions.Options; using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Mailing.SendMail; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Repositories; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Enums; using Org.Eclipse.TractusX.Portal.Backend.Processes.NetworkRegistration.Library.DependencyInjection; +using Org.Eclipse.TractusX.Portal.Backend.Processes.NetworkRegistration.Library.Models; using Org.Eclipse.TractusX.Portal.Backend.Provisioning.Library; using Org.Eclipse.TractusX.Portal.Backend.Provisioning.Library.Models; using Org.Eclipse.TractusX.Portal.Backend.Provisioning.Library.Service; @@ -36,16 +37,19 @@ public class NetworkRegistrationHandler : INetworkRegistrationHandler private readonly IUserProvisioningService _userProvisioningService; private readonly IProvisioningManager _provisioningManager; private readonly NetworkRegistrationProcessSettings _settings; + private readonly IMailingService _mailingService; public NetworkRegistrationHandler( IPortalRepositories portalRepositories, IUserProvisioningService userProvisioningService, IProvisioningManager provisioningManager, + IMailingService mailingService, IOptions options) { _portalRepositories = portalRepositories; _userProvisioningService = userProvisioningService; _provisioningManager = provisioningManager; + _mailingService = mailingService; _settings = options.Value; } @@ -53,6 +57,13 @@ public NetworkRegistrationHandler( public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> SynchronizeUser(Guid networkRegistrationId) { var userRepository = _portalRepositories.GetInstance(); + var userRoleRepository = _portalRepositories.GetInstance(); + var ospName = await _portalRepositories.GetInstance().GetOspCompanyName(networkRegistrationId).ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(ospName)) + { + throw new UnexpectedConditionException("Onboarding Service Provider name must be set"); + } + var companyAssignedIdentityProviders = await userRepository .GetUserAssignedIdentityProviderForNetworkRegistration(networkRegistrationId) .ToListAsync() @@ -75,19 +86,25 @@ public NetworkRegistrationHandler( try { - var userId = await _provisioningManager.GetUserByUserName(cu.CompanyUserId.ToString()).ConfigureAwait(false) ?? - await _userProvisioningService.CreateCentralUserWithProviderLinks(cu.CompanyUserId, new UserCreationRoleDataIdpInfo(cu.FirstName!, cu.LastName!, cu.Email!, roleData, string.Empty, string.Empty, UserStatusId.ACTIVE, true), cu.CompanyName, cu.Bpn, cu.ProviderLinkData.Select(x => new IdentityProviderLink(x.Alias!, x.ProviderUserId, x.UserName))); + var userId = await _provisioningManager.GetUserByUserName(cu.CompanyUserId.ToString()).ConfigureAwait(false); + if (!string.IsNullOrWhiteSpace(userId)) + { + userRepository.AttachAndModifyIdentity(cu.CompanyUserId, i => + { + i.UserStatusId = UserStatusId.PENDING; + i.UserEntityId = null; + }, + i => + { + i.UserStatusId = UserStatusId.ACTIVE; + i.UserEntityId = userId; + }); - userRepository.AttachAndModifyIdentity(cu.CompanyUserId, i => - { - i.UserStatusId = UserStatusId.PENDING; - i.UserEntityId = null; - }, - i => - { - i.UserStatusId = UserStatusId.ACTIVE; - i.UserEntityId = userId; - }); + await _userProvisioningService.AssignRolesToNewUserAsync(userRoleRepository, roleData, (userId, cu.CompanyUserId)).ConfigureAwait(false); + continue; + } + + await _userProvisioningService.HandleCentralKeycloakCreation(new UserCreationRoleDataIdpInfo(cu.FirstName!, cu.LastName!, cu.Email!, roleData, string.Empty, string.Empty, UserStatusId.ACTIVE, true), cu.CompanyUserId, cu.CompanyName, cu.Bpn, null, cu.ProviderLinkData.Select(x => new IdentityProviderLink(x.Alias!, x.ProviderUserId, x.UserName)), userRepository, userRoleRepository).ConfigureAwait(false); } catch (Exception e) { @@ -95,10 +112,29 @@ public NetworkRegistrationHandler( } } + await SendMails(companyAssignedIdentityProviders.Select(x => new UserMailInformation(x.Email!, x.FirstName, x.LastName, x.ProviderLinkData.Select(pld => pld.Alias!))), ospName).ConfigureAwait(false); return new ValueTuple?, ProcessStepStatusId, bool, string?>( null, ProcessStepStatusId.DONE, false, null); } + + private async Task SendMails(IEnumerable companyUserWithRoleIdForCompany, string ospName) + { + var templates = Enumerable.Repeat("OspWelcomeMail", 1); + foreach (var (receiver, firstName, lastName, idpAliasse) in companyUserWithRoleIdForCompany) + { + var userName = string.Join(" ", firstName, lastName); + var mailParameters = new Dictionary + { + { "userName", !string.IsNullOrWhiteSpace(userName) ? userName : receiver }, + { "hostname", _settings.BasePortalAddress }, + { "osp", ospName }, + { "url", _settings.BasePortalAddress }, + { "idpAliasse", string.Join(",", idpAliasse) } + }; + await _mailingService.SendMails(receiver, mailParameters, templates).ConfigureAwait(false); + } + } } diff --git a/src/processes/NetworkRegistration.Library/NetworkRegistrationProcessHelper.cs b/src/processes/NetworkRegistration.Library/NetworkRegistrationProcessHelper.cs index afd1380dd2..c5c968c5aa 100644 --- a/src/processes/NetworkRegistration.Library/NetworkRegistrationProcessHelper.cs +++ b/src/processes/NetworkRegistration.Library/NetworkRegistrationProcessHelper.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -20,7 +19,6 @@ using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess; -using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Models; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Repositories; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Enums; using Org.Eclipse.TractusX.Portal.Backend.Processes.Library; diff --git a/src/provisioning/Provisioning.Library/Service/IUserProvisioningService.cs b/src/provisioning/Provisioning.Library/Service/IUserProvisioningService.cs index f686052f71..3dc302b5db 100644 --- a/src/provisioning/Provisioning.Library/Service/IUserProvisioningService.cs +++ b/src/provisioning/Provisioning.Library/Service/IUserProvisioningService.cs @@ -22,7 +22,6 @@ using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Models; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Repositories; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Entities; -using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Enums; using Org.Eclipse.TractusX.Portal.Backend.Provisioning.Library.Models; namespace Org.Eclipse.TractusX.Portal.Backend.Provisioning.Library.Service; @@ -30,10 +29,12 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Provisioning.Library.Service; public interface IUserProvisioningService { IAsyncEnumerable<(Guid CompanyUserId, string UserName, string? Password, Exception? Error)> CreateOwnCompanyIdpUsersAsync(CompanyNameIdpAliasData companyNameIdpAliasData, IAsyncEnumerable userCreationInfos, CancellationToken cancellationToken = default); + Task HandleCentralKeycloakCreation(UserCreationRoleDataIdpInfo user, Guid companyUserId, string companyName, string? businessPartnerNumber, Identity? identity, IEnumerable identityProviderLinks, IUserRepository userRepository, IUserRolesRepository userRolesRepository); Task<(CompanyNameIdpAliasData IdpAliasData, string NameCreatedBy)> GetCompanyNameIdpAliasData(Guid identityProviderId, Guid companyUserId); Task<(CompanyNameIdpAliasData IdpAliasData, string NameCreatedBy)> GetCompanyNameSharedIdpAliasData(Guid companyUserId, Guid? applicationId = null); Task GetIdentityProviderDisplayName(string idpAlias); IAsyncEnumerable GetRoleDatas(IEnumerable clientRoles); Task> GetOwnCompanyPortalRoleDatas(string clientId, IEnumerable roles, Guid companyId); - Task CreateCentralUserWithProviderLinks(Guid companyUserId, UserCreationRoleDataIdpInfo user, string companyName, string? businessPartnerNumber, IEnumerable identityProviderLinks); + Task<(Identity? identity, Guid companyUserId)> GetOrCreateCompanyUser(IUserRepository userRepository, string alias, UserCreationRoleDataIdpInfo user, Guid companyId, Guid identityProviderId, string? businessPartnerNumber); + Task AssignRolesToNewUserAsync(IUserRolesRepository userRolesRepository, IEnumerable roleDatas, (string UserEntityId, Guid CompanyUserId) userdata); } diff --git a/src/provisioning/Provisioning.Library/Service/UserProvisioningService.cs b/src/provisioning/Provisioning.Library/Service/UserProvisioningService.cs index eb94a9db03..8958418f68 100644 --- a/src/provisioning/Provisioning.Library/Service/UserProvisioningService.cs +++ b/src/provisioning/Provisioning.Library/Service/UserProvisioningService.cs @@ -36,6 +36,7 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Provisioning.Library.Service; public class UserProvisioningService : IUserProvisioningService { + private static readonly IEnumerable ValidCompanyUserStatusIds = new[] { UserStatusId.ACTIVE, UserStatusId.INACTIVE, UserStatusId.PENDING }; private readonly IProvisioningManager _provisioningManager; private readonly IPortalRepositories _portalRepositories; @@ -68,35 +69,26 @@ public UserProvisioningService(IProvisioningManager provisioningManager, IPortal Exception? error = null; var nextPassword = passwordProvider.NextOptionalPassword(); - try { - var (identity, companyUserId) = await GetOrCreateCompanyUser(userRepository, alias, user, companyId, businessPartnerNumber); - - cancellationToken.ThrowIfCancellationRequested(); + var (identity, companyUserId) = await GetOrCreateCompanyUser(userRepository, alias, user, companyId, identityProviderId, businessPartnerNumber); - userRepository.AddCompanyUserAssignedIdentityProvider(companyUserId, identityProviderId, user.UserId, user.UserName); - var providerUserId = await CreateSharedIdpUserOrReturnUserId(user, alias, nextPassword, isSharedIdp).ConfigureAwait(false); - var centralUserId = await CreateCentralUserWithProviderLinks(companyUserId, user, companyName, businessPartnerNumber, Enumerable.Repeat(new IdentityProviderLink(alias, providerUserId, user.UserName), 1)); - userdata = new(centralUserId, companyUserId); - if (identity == null) + userdata.CompanyUserId = companyUserId; + if (!string.IsNullOrWhiteSpace(identity?.UserEntityId)) { - userRepository.AttachAndModifyIdentity(companyUserId, null, cu => - { - cu.UserEntityId = centralUserId; - }); - } - else - { - identity.UserEntityId = centralUserId; + userdata.UserEntityId = identity.UserEntityId; } - await AssignRolesToNewUserAsync(userRolesRepository, user.RoleDatas, userdata).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + + var providerUserId = await CreateSharedIdpUserOrReturnUserId(user, alias, nextPassword, isSharedIdp).ConfigureAwait(false); + await HandleCentralKeycloakCreation(user, companyUserId, companyName, businessPartnerNumber, identity, Enumerable.Repeat(new IdentityProviderLink(alias, providerUserId, user.UserName), 1), userRepository, userRolesRepository).ConfigureAwait(false); } catch (Exception e) when (e is not OperationCanceledException) { error = e; } + if (userdata == default && error == null) { error = new UnexpectedConditionException($"failed to create companyUser for provider userid {user.UserId}, username {user.UserName} while not throwing any error"); @@ -108,7 +100,27 @@ public UserProvisioningService(IProvisioningManager provisioningManager, IPortal } } - public async Task CreateCentralUserWithProviderLinks(Guid companyUserId, UserCreationRoleDataIdpInfo user, string companyName, string? businessPartnerNumber, IEnumerable identityProviderLinks) + public async Task HandleCentralKeycloakCreation(UserCreationRoleDataIdpInfo user, Guid companyUserId, string companyName, string? businessPartnerNumber, Identity? identity, IEnumerable identityProviderLinks, IUserRepository userRepository, IUserRolesRepository userRolesRepository) + { + var centralUserId = await CreateCentralUserWithProviderLinks(companyUserId, user, companyName, businessPartnerNumber, identityProviderLinks).ConfigureAwait(false); + if (identity == null) + { + userRepository.AttachAndModifyIdentity(companyUserId, null, cu => + { + cu.UserEntityId = centralUserId; + cu.UserStatusId = user.UserStatusId; + }); + } + else + { + identity.UserEntityId = centralUserId; + identity.UserStatusId = user.UserStatusId; + } + + await AssignRolesToNewUserAsync(userRolesRepository, user.RoleDatas, (centralUserId, companyUserId)).ConfigureAwait(false); + } + + private async Task CreateCentralUserWithProviderLinks(Guid companyUserId, UserCreationRoleDataIdpInfo user, string companyName, string? businessPartnerNumber, IEnumerable identityProviderLinks) { var centralUserId = await _provisioningManager.CreateCentralUserAsync( new UserProfile( @@ -133,11 +145,12 @@ await _provisioningManager.AddProviderUserLinkToCentralUserAsync(centralUserId, return centralUserId; } - private async Task<(Identity? identity, Guid companyUserId)> GetOrCreateCompanyUser( + public async Task<(Identity? identity, Guid companyUserId)> GetOrCreateCompanyUser( IUserRepository userRepository, string alias, UserCreationRoleDataIdpInfo user, Guid companyId, + Guid identityProviderId, string? businessPartnerNumber) { var businessPartnerRepository = _portalRepositories.GetInstance(); @@ -156,6 +169,8 @@ await _provisioningManager.AddProviderUserLinkToCentralUserAsync(centralUserId, businessPartnerRepository.CreateCompanyUserAssignedBusinessPartner(companyUserId, businessPartnerNumber); } + userRepository.AddCompanyUserAssignedIdentityProvider(companyUserId, identityProviderId, user.UserId, user.UserName); + return (identity, companyUserId); } @@ -191,6 +206,7 @@ private Task CreateSharedIdpUserOrReturnUserId(UserCreationRoleDataIdpIn { throw new ControllerArgumentException($"user {companyUserId} does not exist"); } + var (company, companyUser, identityProvider) = result; if (identityProvider.IdpAlias == null) { @@ -216,15 +232,18 @@ private Task CreateSharedIdpUserOrReturnUserId(UserCreationRoleDataIdpIn ? new ControllerArgumentException($"user {companyUserId} does not exist") : new ControllerArgumentException($"user {companyUserId} is not associated with application {applicationId}"); } + var (company, companyUser, idpAliase) = result; if (company.CompanyName == null) { throw new ConflictException($"assertion failed: companyName of company {company.CompanyId} should never be null here"); } + if (!idpAliase.Any()) { throw new ConflictException($"user {companyUserId} is not associated with any shared idp"); } + if (idpAliase.Count() > 1) { throw new ConflictException($"user {companyUserId} is associated with more than one shared idp"); @@ -243,14 +262,17 @@ private static string CreateNameString(string? firstName, string? lastName, stri { sb.Append(firstName); } + if (lastName != null) { sb.AppendFormat((firstName == null ? "{0}" : ", {0}"), lastName); } + if (email != null) { sb.AppendFormat((firstName == null && lastName == null) ? "{0}" : " ({0})", email); } + return firstName == null && lastName == null && email == null ? "Dear User" : sb.ToString(); } @@ -261,9 +283,7 @@ private async Task ValidateDuplicateIdpUsersAsync(IUserRepository userRepo { var existingCompanyUserId = Guid.Empty; - var validCompanyUserStatusIds = new[] { UserStatusId.ACTIVE, UserStatusId.INACTIVE }; - - await foreach (var (userEntityId, companyUserId) in userRepository.GetMatchingCompanyIamUsersByNameEmail(user.FirstName, user.LastName, user.Email, companyId, validCompanyUserStatusIds).ConfigureAwait(false)) + await foreach (var (userEntityId, companyUserId) in userRepository.GetMatchingCompanyIamUsersByNameEmail(user.FirstName, user.LastName, user.Email, companyId, ValidCompanyUserStatusIds).ConfigureAwait(false)) { if (userEntityId == null) { @@ -271,8 +291,10 @@ private async Task ValidateDuplicateIdpUsersAsync(IUserRepository userRepo { existingCompanyUserId = companyUserId; } + continue; } + try { if (await _provisioningManager.GetProviderUserLinkDataForCentralUserIdAsync(userEntityId).AnyAsync(link => @@ -286,10 +308,11 @@ private async Task ValidateDuplicateIdpUsersAsync(IUserRepository userRepo // when searching for duplicates this is not a validation-error } } + return existingCompanyUserId; } - private async Task AssignRolesToNewUserAsync(IUserRolesRepository userRolesRepository, IEnumerable roleDatas, (string UserEntityId, Guid CompanyUserId) userdata) + public async Task AssignRolesToNewUserAsync(IUserRolesRepository userRolesRepository, IEnumerable roleDatas, (string UserEntityId, Guid CompanyUserId) userdata) { if (roleDatas.Any()) { @@ -304,6 +327,7 @@ private async Task AssignRolesToNewUserAsync(IUserRolesRepository userRolesRepos var roleId = roleDatas.First(roleInfo => roleInfo.ClientClientId == assigned.Client && roleInfo.UserRoleText == role).UserRoleId; userRolesRepository.CreateIdentityAssignedRole(userdata.CompanyUserId, roleId); } + messages.AddRange(clientRoleNames[assigned.Client].Except(assigned.Roles).Select(roleName => $"clientId: {assigned.Client}, role: {roleName}")); } diff --git a/tests/administration/Administration.Service.Tests/BusinessLogic/NetworkBusinessLogicTests.cs b/tests/administration/Administration.Service.Tests/BusinessLogic/NetworkBusinessLogicTests.cs index d449c14eed..5c32ed6216 100644 --- a/tests/administration/Administration.Service.Tests/BusinessLogic/NetworkBusinessLogicTests.cs +++ b/tests/administration/Administration.Service.Tests/BusinessLogic/NetworkBusinessLogicTests.cs @@ -23,7 +23,6 @@ using Org.Eclipse.TractusX.Portal.Backend.Administration.Service.Models; using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; using Org.Eclipse.TractusX.Portal.Backend.Framework.Models.Configuration; -using Org.Eclipse.TractusX.Portal.Backend.Mailing.SendMail; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Models; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Repositories; @@ -52,7 +51,6 @@ public class NetworkBusinessLogicTests private readonly IIdentityService _identityService; private readonly IUserProvisioningService _userProvisioningService; private readonly INetworkRegistrationProcessHelper _networkRegistrationProcessHelper; - private readonly IMailingService _mailingService; private readonly IPortalRepositories _portalRepositories; private readonly ICompanyRepository _companyRepository; @@ -75,7 +73,6 @@ public NetworkBusinessLogicTests() _portalRepositories = A.Fake(); _identityService = A.Fake(); _networkRegistrationProcessHelper = A.Fake(); - _mailingService = A.Fake(); _companyRepository = A.Fake(); _companyRolesRepository = A.Fake(); @@ -102,7 +99,7 @@ public NetworkBusinessLogicTests() A.CallTo(() => _portalRepositories.GetInstance()).Returns(_identityProviderRepository); A.CallTo(() => _portalRepositories.GetInstance()).Returns(_countryRepository); - _sut = new NetworkBusinessLogic(_portalRepositories, _identityService, _userProvisioningService, _networkRegistrationProcessHelper, _mailingService, options); + _sut = new NetworkBusinessLogic(_portalRepositories, _identityService, _userProvisioningService, _networkRegistrationProcessHelper, options); SetupRepos(); } @@ -362,8 +359,8 @@ public async Task HandlePartnerRegistration_WithUserCreationThrowsException_Thro A.CallTo(() => _processStepRepository.CreateProcess(ProcessTypeId.PARTNER_REGISTRATION)) .Returns(new Process(processId, default, default)); - A.CallTo(() => _userProvisioningService.CreateOwnCompanyIdpUsersAsync(A._, A>._, A._)) - .Returns(new[] { (Guid.Empty, "", (string?)null, (Exception?)new UnexpectedConditionException("Test")) }.ToAsyncEnumerable()); + A.CallTo(() => _userProvisioningService.GetOrCreateCompanyUser(A._, A._, A._, A._, A._, "BPNL00000001TEST")) + .Throws(new UnexpectedConditionException("Test message")); // Act async Task Act() => await _sut.HandlePartnerRegistration(data).ConfigureAwait(false); @@ -528,13 +525,11 @@ public async Task HandlePartnerRegistration_WithIdpNotSetAndOnlyOneIdp_CallsExpe x.ProcessId == newProcessId && x.ApplicationId == newApplicationId); - A.CallTo(() => _userProvisioningService.CreateOwnCompanyIdpUsersAsync(A._, A>._, A._)) + A.CallTo(() => _userProvisioningService.GetOrCreateCompanyUser(A._, "test-alias", A._, newCompanyId, IdpId, Bpnl)) .MustHaveHappenedOnceExactly(); A.CallTo(() => _identityProviderRepository.CreateCompanyIdentityProviders(A>.That.IsSameSequenceAs(new[] { new ValueTuple(newCompanyId, IdpId) }))) .MustHaveHappenedOnceExactly(); A.CallTo(() => _portalRepositories.SaveAsync()).MustHaveHappenedOnceExactly(); - A.CallTo(() => _mailingService.SendMails(A._, A>._, A>.That.IsSameSequenceAs(new[] { "OspWelcomeMail" }))) - .MustHaveHappenedOnceExactly(); } [Fact] @@ -628,8 +623,6 @@ public async Task HandlePartnerRegistration_WithValidData_CallsExpected() { networkRegistrations.Add(new NetworkRegistration(Guid.NewGuid(), externalId, companyId, pId, ospId, companyApplicationId, DateTimeOffset.UtcNow)); }); - A.CallTo(() => _userProvisioningService.CreateOwnCompanyIdpUsersAsync(A._, A>._, A._)) - .Returns(new[] { (Guid.NewGuid(), "ironman", (string?)"testpw", (Exception?)null) }.ToAsyncEnumerable()); // Act await _sut.HandlePartnerRegistration(data).ConfigureAwait(false); @@ -663,13 +656,11 @@ public async Task HandlePartnerRegistration_WithValidData_CallsExpected() x.ProcessId == newProcessId && x.ApplicationId == newApplicationId); - A.CallTo(() => _userProvisioningService.CreateOwnCompanyIdpUsersAsync(A._, A>._, A._)) + A.CallTo(() => _userProvisioningService.GetOrCreateCompanyUser(A._, "test-alias", A._, newCompanyId, IdpId, Bpnl)) .MustHaveHappenedOnceExactly(); A.CallTo(() => _identityProviderRepository.CreateCompanyIdentityProviders(A>.That.IsSameSequenceAs(new[] { new ValueTuple(newCompanyId, IdpId) }))) .MustHaveHappenedOnceExactly(); A.CallTo(() => _portalRepositories.SaveAsync()).MustHaveHappenedOnceExactly(); - A.CallTo(() => _mailingService.SendMails(A._, A>._, A>.That.IsSameSequenceAs(new[] { "OspWelcomeMail" }))) - .MustHaveHappenedOnceExactly(); } #endregion diff --git a/tests/processes/NetworkRegistration.Library.Tests/NetworkRegistrationHandlerTests.cs b/tests/processes/NetworkRegistration.Library.Tests/NetworkRegistrationHandlerTests.cs index 93fe9a3710..6d7b38c446 100644 --- a/tests/processes/NetworkRegistration.Library.Tests/NetworkRegistrationHandlerTests.cs +++ b/tests/processes/NetworkRegistration.Library.Tests/NetworkRegistrationHandlerTests.cs @@ -21,6 +21,7 @@ using Microsoft.Extensions.Options; using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; using Org.Eclipse.TractusX.Portal.Backend.Framework.Models.Configuration; +using Org.Eclipse.TractusX.Portal.Backend.Mailing.SendMail; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Models; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Repositories; @@ -42,28 +43,47 @@ public class NetworkRegistrationHandlerTests private readonly IProvisioningManager _provisioningManger; private readonly IUserRepository _userRepository; + private readonly INetworkRepository _networkRepository; private readonly NetworkRegistrationHandler _sut; - private readonly NetworkRegistrationProcessSettings _settings; + private readonly IMailingService _mailingService; public NetworkRegistrationHandlerTests() { var portalRepositories = A.Fake(); _userRepository = A.Fake(); + _networkRepository = A.Fake(); _userProvisioningService = A.Fake(); _provisioningManger = A.Fake(); + _mailingService = A.Fake(); - _settings = new NetworkRegistrationProcessSettings + var settings = new NetworkRegistrationProcessSettings { InitialRoles = Enumerable.Repeat(new UserRoleConfig("cl1", Enumerable.Repeat("Company Admin", 1)), 1) }; var options = A.Fake>(); - A.CallTo(() => options.Value).Returns(_settings); + A.CallTo(() => options.Value).Returns(settings); A.CallTo(() => portalRepositories.GetInstance()).Returns(_userRepository); + A.CallTo(() => portalRepositories.GetInstance()).Returns(_networkRepository); - _sut = new NetworkRegistrationHandler(portalRepositories, _userProvisioningService, _provisioningManger, options); + _sut = new NetworkRegistrationHandler(portalRepositories, _userProvisioningService, _provisioningManger, _mailingService, options); + } + + [Fact] + public async Task SynchronizeUser_WithoutOspName_ThrowsUnexpectedConditionException() + { + // Arrange + A.CallTo(() => _networkRepository.GetOspCompanyName(NetworkRegistrationId)) + .Returns((string?)null); + + // Act + async Task Act() => await _sut.SynchronizeUser(NetworkRegistrationId).ConfigureAwait(false); + + // Assert + var ex = await Assert.ThrowsAsync(Act); + ex.Message.Should().Be("Onboarding Service Provider name must be set"); } [Theory] @@ -78,6 +98,8 @@ public async Task SynchronizeUser_WithUserDataNull_ThrowsConflictException(strin "123456789", "Test Company", "BPNL00000001TEST", Enumerable.Repeat(new ProviderLinkData("ironman", "idp1", "id1234"), 1)); + A.CallTo(() => _networkRepository.GetOspCompanyName(NetworkRegistrationId)) + .Returns("Onboarding Service Provider"); A.CallTo(() => _userRepository.GetUserAssignedIdentityProviderForNetworkRegistration(NetworkRegistrationId)) .Returns(new[] { @@ -102,10 +124,12 @@ public async Task SynchronizeUser_WithAliasNull_ThrowsConflictException() var user1 = new CompanyUserIdentityProviderProcessData(user1Id, "tony", "stark", "tony@stark.com", "123456789", "Test Company", "BPNL00000001TEST", Enumerable.Repeat(new ProviderLinkData("ironman", null, "id1234"), 1)); + A.CallTo(() => _networkRepository.GetOspCompanyName(NetworkRegistrationId)) + .Returns("Onboarding Service Provider"); A.CallTo(() => _userRepository.GetUserAssignedIdentityProviderForNetworkRegistration(NetworkRegistrationId)) .Returns(new[] { - user1, + user1 }.ToAsyncEnumerable()); A.CallTo(() => _userProvisioningService.GetRoleDatas(A>._)) .Returns(Enumerable.Repeat(new UserRoleData(UserRoleIds, "cl1", "Company Admin"), 1).ToAsyncEnumerable()); @@ -123,7 +147,6 @@ public async Task SynchronizeUser_WithValidData_ReturnsExpected() { // Arrange var user1Id = Guid.NewGuid().ToString(); - var user2Id = Guid.NewGuid().ToString(); var user1 = new CompanyUserIdentityProviderProcessData(Guid.NewGuid(), "tony", "stark", "tony@stark.com", "123456789", "Test Company", "BPNL00000001TEST", Enumerable.Repeat(new ProviderLinkData("ironman", "idp1", "id1234"), 1)); @@ -131,14 +154,14 @@ public async Task SynchronizeUser_WithValidData_ReturnsExpected() "steven@strange.com", "987654321", "Test Company", "BPNL00000001TEST", Enumerable.Repeat(new ProviderLinkData("drstrange", "idp1", "id9876"), 1)); + A.CallTo(() => _networkRepository.GetOspCompanyName(NetworkRegistrationId)) + .Returns("Onboarding Service Provider"); A.CallTo(() => _userRepository.GetUserAssignedIdentityProviderForNetworkRegistration(NetworkRegistrationId)) .Returns(new[] { user1, user2 }.ToAsyncEnumerable()); - A.CallTo(() => _userProvisioningService.CreateCentralUserWithProviderLinks(user2.CompanyUserId, A._, A._, A._, A>._)) - .Returns(user2Id); A.CallTo(() => _userProvisioningService.GetRoleDatas(A>._)) .Returns(Enumerable.Repeat(new UserRoleData(UserRoleIds, "cl1", "Company Admin"), 1).ToAsyncEnumerable()); A.CallTo(() => _provisioningManger.GetUserByUserName(user1.CompanyUserId.ToString())).Returns(user1Id); @@ -148,12 +171,19 @@ public async Task SynchronizeUser_WithValidData_ReturnsExpected() var result = await _sut.SynchronizeUser(NetworkRegistrationId).ConfigureAwait(false); // Assert - A.CallTo(() => _userProvisioningService.CreateCentralUserWithProviderLinks(user2.CompanyUserId, A._, A._, A._, A>._)) + A.CallTo(() => _userProvisioningService.HandleCentralKeycloakCreation(A._, user1.CompanyUserId, A._, A._, null, A>._, A._, A._)) + .MustNotHaveHappened(); + A.CallTo(() => _userProvisioningService.HandleCentralKeycloakCreation(A._, user2.CompanyUserId, A._, A._, null, A>._, A._, A._)) .MustHaveHappenedOnceExactly(); A.CallTo(() => _userRepository.AttachAndModifyIdentity(user1.CompanyUserId, A>._, A>._)) .MustHaveHappenedOnceExactly(); A.CallTo(() => _userRepository.AttachAndModifyIdentity(user2.CompanyUserId, A>._, A>._)) + .MustNotHaveHappened(); + A.CallTo(() => _mailingService.SendMails("tony@stark.com", A>._, A>._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _mailingService.SendMails("steven@strange.com", A>._, A>._)) .MustHaveHappenedOnceExactly(); + result.modified.Should().BeFalse(); result.processMessage.Should().BeNull(); result.stepStatusId.Should().Be(ProcessStepStatusId.DONE);