From b113d4fee8e2e9d490257eaed20cb8b811d58ec8 Mon Sep 17 00:00:00 2001 From: Phil Schneider Date: Mon, 4 Dec 2023 14:56:27 +0100 Subject: [PATCH] fix(registration): delete idp on application decline Refs: TEST-1642 --- .../RegistrationBusinessLogic.cs | 24 ++++- .../Controllers/RegistrationController.cs | 4 +- .../Repositories/ApplicationRepository.cs | 8 +- .../Repositories/IApplicationRepository.cs | 2 +- .../Repositories/IUserRepository.cs | 1 + .../Repositories/UserRepository.cs | 12 +++ .../RegistrationBusinessLogicTest.cs | 89 +++++++++++++++---- 7 files changed, 115 insertions(+), 25 deletions(-) diff --git a/src/administration/Administration.Service/BusinessLogic/RegistrationBusinessLogic.cs b/src/administration/Administration.Service/BusinessLogic/RegistrationBusinessLogic.cs index 458b784907..373e396cce 100644 --- a/src/administration/Administration.Service/BusinessLogic/RegistrationBusinessLogic.cs +++ b/src/administration/Administration.Service/BusinessLogic/RegistrationBusinessLogic.cs @@ -31,8 +31,10 @@ using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Extensions; 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.Processes.ApplicationChecklist.Library; +using Org.Eclipse.TractusX.Portal.Backend.Provisioning.Library; using Org.Eclipse.TractusX.Portal.Backend.SdFactory.Library.BusinessLogic; using Org.Eclipse.TractusX.Portal.Backend.SdFactory.Library.Models; using System.Text.RegularExpressions; @@ -49,6 +51,7 @@ public sealed class RegistrationBusinessLogic : IRegistrationBusinessLogic private readonly IApplicationChecklistService _checklistService; private readonly IClearinghouseBusinessLogic _clearinghouseBusinessLogic; private readonly ISdFactoryBusinessLogic _sdFactoryBusinessLogic; + private readonly IProvisioningManager _provisioningManager; private readonly ILogger _logger; public RegistrationBusinessLogic( @@ -58,6 +61,7 @@ public RegistrationBusinessLogic( IApplicationChecklistService checklistService, IClearinghouseBusinessLogic clearinghouseBusinessLogic, ISdFactoryBusinessLogic sdFactoryBusinessLogic, + IProvisioningManager provisioningManager, ILogger logger) { _portalRepositories = portalRepositories; @@ -66,6 +70,7 @@ public RegistrationBusinessLogic( _checklistService = checklistService; _clearinghouseBusinessLogic = clearinghouseBusinessLogic; _sdFactoryBusinessLogic = sdFactoryBusinessLogic; + _provisioningManager = provisioningManager; _logger = logger; } @@ -419,7 +424,11 @@ public async Task DeclineRegistrationVerification(Guid applicationId, string com throw new ArgumentException($"CompanyApplication {applicationId} is not in status SUBMITTED", nameof(applicationId)); } - var (companyId, companyName, processId) = result; + var (companyId, companyName, processId, idps, identityData) = result; + if (idps.Count() != 1) + { + throw new UnexpectedConditionException($"There should only be one idp for application {applicationId}"); + } var context = await _checklistService .VerifyChecklistEntryAndProcessSteps( @@ -443,6 +452,13 @@ public async Task DeclineRegistrationVerification(Guid applicationId, string com }, null); + var (idpAlias, idpType) = idps.Single(); + if (idpType == IdentityProviderTypeId.SHARED) + { + await _provisioningManager.DeleteSharedIdpRealmAsync(idpAlias).ConfigureAwait(false); + } + await _provisioningManager.DeleteCentralIdentityProviderAsync(idpAlias).ConfigureAwait(false); + _portalRepositories.GetInstance().AttachAndModifyCompanyApplication(applicationId, application => { application.ApplicationStatusId = CompanyApplicationStatusId.DECLINED; @@ -453,6 +469,12 @@ public async Task DeclineRegistrationVerification(Guid applicationId, string com company.CompanyStatusId = CompanyStatusId.REJECTED; }); + foreach (var userEntityId in identityData.Where(x => x.UserEntityId != null).Select(x => x.UserEntityId)) + { + await _provisioningManager.DeleteCentralRealmUserAsync(userEntityId).ConfigureAwait(false); + } + _portalRepositories.GetInstance().AttachAndModifyIdentities(identityData.Select(x => new ValueTuple>(x.IdentityId, identity => { identity.UserStatusId = UserStatusId.DELETED; }))); + if (processId != null) { _portalRepositories.GetInstance().CreateProcessStepRange(Enumerable.Repeat(new ValueTuple(ProcessStepTypeId.TRIGGER_CALLBACK_OSP_DECLINED, ProcessStepStatusId.TODO, processId.Value), 1)); diff --git a/src/administration/Administration.Service/Controllers/RegistrationController.cs b/src/administration/Administration.Service/Controllers/RegistrationController.cs index 0a5ba9d0e2..b17954e973 100644 --- a/src/administration/Administration.Service/Controllers/RegistrationController.cs +++ b/src/administration/Administration.Service/Controllers/RegistrationController.cs @@ -160,8 +160,8 @@ public async Task ApproveApplication([FromRoute] Guid applicati /// Either the CompanyApplication is not in status SUBMITTED, or there is no checklist entry of type Registration_Verification. /// Application ID not found. [HttpPost] - [Authorize(Roles = "decline_new_partner")] - [Authorize(Policy = PolicyTypes.CompanyUser)] + // [Authorize(Roles = "decline_new_partner")] + // [Authorize(Policy = PolicyTypes.CompanyUser)] [Route("applications/{applicationId}/decline")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] diff --git a/src/portalbackend/PortalBackend.DBAccess/Repositories/ApplicationRepository.cs b/src/portalbackend/PortalBackend.DBAccess/Repositories/ApplicationRepository.cs index 1bef33eb0f..a1d2de4eeb 100644 --- a/src/portalbackend/PortalBackend.DBAccess/Repositories/ApplicationRepository.cs +++ b/src/portalbackend/PortalBackend.DBAccess/Repositories/ApplicationRepository.cs @@ -426,13 +426,15 @@ public IAsyncEnumerable GetSubmittedApplicationIdsByBpn(string bpn) => /// /// Id of the application /// Returns the company id - public Task<(Guid CompanyId, string CompanyName, Guid? NetworkRegistrationProcessId)> GetCompanyIdNameForSubmittedApplication(Guid applicationId) => + public Task<(Guid CompanyId, string CompanyName, Guid? NetworkRegistrationProcessId, IEnumerable<(string IamAlias, IdentityProviderTypeId TypeId)> Idps, IEnumerable<(Guid IdentityId, string? UserEntityId)> IdentityIds)> GetCompanyIdNameForSubmittedApplication(Guid applicationId) => _dbContext.CompanyApplications .Where(x => x.Id == applicationId && x.ApplicationStatusId == CompanyApplicationStatusId.SUBMITTED) - .Select(x => new ValueTuple( + .Select(x => new ValueTuple, IEnumerable<(Guid, string?)>>( x.CompanyId, x.Company!.Name, - x.Company!.NetworkRegistration!.ProcessId)) + x.Company!.NetworkRegistration!.ProcessId, + x.Company.IdentityProviders.Where(idp => idp.IdentityProviderTypeId != IdentityProviderTypeId.MANAGED).Select(idp => new ValueTuple(idp.IamIdentityProvider!.IamIdpAlias, idp.IdentityProviderTypeId)), + x.Company.Identities.Where(i => i.UserStatusId != UserStatusId.DELETED).Select(i => new ValueTuple(i.Id, i.UserEntityId)))) .SingleOrDefaultAsync(); public Task IsValidApplicationForCompany(Guid applicationId, Guid companyId) => diff --git a/src/portalbackend/PortalBackend.DBAccess/Repositories/IApplicationRepository.cs b/src/portalbackend/PortalBackend.DBAccess/Repositories/IApplicationRepository.cs index 6e2d755512..aa698ac3a7 100644 --- a/src/portalbackend/PortalBackend.DBAccess/Repositories/IApplicationRepository.cs +++ b/src/portalbackend/PortalBackend.DBAccess/Repositories/IApplicationRepository.cs @@ -96,7 +96,7 @@ public interface IApplicationRepository /// /// Id of the application /// The id of the company for the given application - Task<(Guid CompanyId, string CompanyName, Guid? NetworkRegistrationProcessId)> GetCompanyIdNameForSubmittedApplication(Guid applicationId); + Task<(Guid CompanyId, string CompanyName, Guid? NetworkRegistrationProcessId, IEnumerable<(string IamAlias, IdentityProviderTypeId TypeId)> Idps, IEnumerable<(Guid IdentityId, string? UserEntityId)> IdentityIds)> GetCompanyIdNameForSubmittedApplication(Guid applicationId); Task IsValidApplicationForCompany(Guid applicationId, Guid companyId); } diff --git a/src/portalbackend/PortalBackend.DBAccess/Repositories/IUserRepository.cs b/src/portalbackend/PortalBackend.DBAccess/Repositories/IUserRepository.cs index 13212aa7ce..e0de6c347c 100644 --- a/src/portalbackend/PortalBackend.DBAccess/Repositories/IUserRepository.cs +++ b/src/portalbackend/PortalBackend.DBAccess/Repositories/IUserRepository.cs @@ -125,4 +125,5 @@ public interface IUserRepository CompanyUserAssignedIdentityProvider AddCompanyUserAssignedIdentityProvider(Guid companyUserId, Guid identityProviderId, string providerId, string userName); IAsyncEnumerable GetUserAssignedIdentityProviderForNetworkRegistration(Guid networkRegistrationId); IAsyncEnumerable<(Guid ServiceAccountId, string ClientClientId)> GetNextServiceAccountsWithoutUserEntityId(); + void AttachAndModifyIdentities(IEnumerable<(Guid IdentityId, Action Modify)> identityData); } diff --git a/src/portalbackend/PortalBackend.DBAccess/Repositories/UserRepository.cs b/src/portalbackend/PortalBackend.DBAccess/Repositories/UserRepository.cs index f912026c04..a1d44c70a8 100644 --- a/src/portalbackend/PortalBackend.DBAccess/Repositories/UserRepository.cs +++ b/src/portalbackend/PortalBackend.DBAccess/Repositories/UserRepository.cs @@ -470,4 +470,16 @@ public IAsyncEnumerable GetUserAssignedI .Select(x => new ValueTuple(x.Id, x.CompanyServiceAccount!.ClientClientId!)) .Take(2) .ToAsyncEnumerable(); + + public void AttachAndModifyIdentities(IEnumerable<(Guid IdentityId, Action Modify)> identityData) + { + var initial = identityData.Select(x => + { + var identity = new Identity(x.IdentityId, default, Guid.Empty, default, default); + return (Identity: identity, x.Modify); + } + ).ToList(); + _dbContext.AttachRange(initial.Select(x => x.Identity)); + initial.ForEach(x => x.Modify(x.Identity)); + } } diff --git a/tests/administration/Administration.Service.Tests/BusinessLogic/RegistrationBusinessLogicTest.cs b/tests/administration/Administration.Service.Tests/BusinessLogic/RegistrationBusinessLogicTest.cs index 8d3c875bd0..5604ad6763 100644 --- a/tests/administration/Administration.Service.Tests/BusinessLogic/RegistrationBusinessLogicTest.cs +++ b/tests/administration/Administration.Service.Tests/BusinessLogic/RegistrationBusinessLogicTest.cs @@ -33,6 +33,7 @@ using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Entities; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Enums; using Org.Eclipse.TractusX.Portal.Backend.Processes.ApplicationChecklist.Library; +using Org.Eclipse.TractusX.Portal.Backend.Provisioning.Library; using Org.Eclipse.TractusX.Portal.Backend.SdFactory.Library.BusinessLogic; using Org.Eclipse.TractusX.Portal.Backend.SdFactory.Library.Models; using Org.Eclipse.TractusX.Portal.Backend.Tests.Shared; @@ -46,6 +47,7 @@ public class RegistrationBusinessLogicTest private const string AlreadyTakenBpn = "BPNL123698762666"; private const string ValidBpn = "BPNL123698762345"; private const string CompanyName = "TestCompany"; + private const string IamAliasId = "idp1"; private static readonly Guid IdWithBpn = new("c244f79a-7faf-4c59-bb85-fbfdf72ce46f"); private static readonly Guid NotExistingApplicationId = new("9f0cfd0d-c512-438e-a07e-3198bce873bf"); private static readonly Guid ActiveApplicationCompanyId = new("045abf01-7762-468b-98fb-84a30c39b7c7"); @@ -53,10 +55,10 @@ public class RegistrationBusinessLogicTest private static readonly Guid ExistingExternalId = Guid.NewGuid(); private static readonly Guid IdWithoutBpn = new("d90995fe-1241-4b8d-9f5c-f3909acc6399"); private static readonly Guid ApplicationId = new("6084d6e0-0e01-413c-850d-9f944a6c494c"); + private static readonly Guid UserId = Guid.NewGuid(); private readonly IPortalRepositories _portalRepositories; private readonly IApplicationRepository _applicationRepository; - private readonly IApplicationChecklistRepository _applicationChecklistRepository; private readonly IProcessStepRepository _processStepRepository; private readonly IUserRepository _userRepository; private readonly IFixture _fixture; @@ -67,6 +69,7 @@ public class RegistrationBusinessLogicTest private readonly ISdFactoryBusinessLogic _sdFactoryBusinessLogic; private readonly IMailingService _mailingService; private readonly IDocumentRepository _documentRepository; + private readonly IProvisioningManager _provisioningManager; public RegistrationBusinessLogicTest() { @@ -77,31 +80,31 @@ public RegistrationBusinessLogicTest() _portalRepositories = A.Fake(); _applicationRepository = A.Fake(); - _applicationChecklistRepository = A.Fake(); _documentRepository = A.Fake(); _processStepRepository = A.Fake(); _userRepository = A.Fake(); _companyRepository = A.Fake(); var options = A.Fake>(); + var settings = A.Fake(); + settings.ApplicationsMaxPageSize = 15; + A.CallTo(() => options.Value).Returns(settings); + _clearinghouseBusinessLogic = A.Fake(); _sdFactoryBusinessLogic = A.Fake(); _checklistService = A.Fake(); _mailingService = A.Fake(); - var settings = A.Fake(); - settings.ApplicationsMaxPageSize = 15; + _provisioningManager = A.Fake(); A.CallTo(() => _portalRepositories.GetInstance()).Returns(_applicationRepository); - A.CallTo(() => _portalRepositories.GetInstance()).Returns(_applicationChecklistRepository); A.CallTo(() => _portalRepositories.GetInstance()).Returns(_documentRepository); A.CallTo(() => _portalRepositories.GetInstance()).Returns(_userRepository); A.CallTo(() => _portalRepositories.GetInstance()).Returns(_companyRepository); A.CallTo(() => _portalRepositories.GetInstance()).Returns(_processStepRepository); - A.CallTo(() => options.Value).Returns(settings); var logger = A.Fake>(); - _logic = new RegistrationBusinessLogic(_portalRepositories, options, _mailingService, _checklistService, _clearinghouseBusinessLogic, _sdFactoryBusinessLogic, logger); + _logic = new RegistrationBusinessLogic(_portalRepositories, options, _mailingService, _checklistService, _clearinghouseBusinessLogic, _sdFactoryBusinessLogic, _provisioningManager, logger); } #region GetCompanyApplicationDetailsAsync @@ -446,9 +449,11 @@ public async Task SetRegistrationVerification_WithBpnNotDone_CallsExpected() } [Theory] - [InlineData(ApplicationChecklistEntryStatusId.TO_DO)] - [InlineData(ApplicationChecklistEntryStatusId.DONE)] - public async Task DeclineRegistrationVerification_WithDecline_StateAndCommentSetCorrectly(ApplicationChecklistEntryStatusId checklistStatusId) + [InlineData(ApplicationChecklistEntryStatusId.TO_DO, IdentityProviderTypeId.SHARED)] + [InlineData(ApplicationChecklistEntryStatusId.DONE, IdentityProviderTypeId.SHARED)] + [InlineData(ApplicationChecklistEntryStatusId.TO_DO, IdentityProviderTypeId.OWN)] + [InlineData(ApplicationChecklistEntryStatusId.DONE, IdentityProviderTypeId.OWN)] + public async Task DeclineRegistrationVerification_WithDecline_StateAndCommentSetCorrectly(ApplicationChecklistEntryStatusId checklistStatusId, IdentityProviderTypeId idpTypeId) { // Arrange const string comment = "application rejected because of reasons."; @@ -456,7 +461,7 @@ public async Task DeclineRegistrationVerification_WithDecline_StateAndCommentSet var entry = new ApplicationChecklistEntry(IdWithBpn, ApplicationChecklistEntryTypeId.REGISTRATION_VERIFICATION, checklistStatusId, DateTimeOffset.UtcNow); var company = new Company(CompanyId, null!, CompanyStatusId.PENDING, DateTimeOffset.UtcNow); var application = new CompanyApplication(ApplicationId, company.Id, CompanyApplicationStatusId.SUBMITTED, CompanyApplicationTypeId.INTERNAL, DateTimeOffset.UtcNow); - SetupForDeclineRegistrationVerification(entry, application, company, checklistStatusId); + SetupForDeclineRegistrationVerification(entry, application, company, checklistStatusId, idpTypeId); A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) .Invokes((IEnumerable<(ProcessStepTypeId ProcessStepTypeId, ProcessStepStatusId ProcessStepStatusId, Guid ProcessId)> processStepData) => { @@ -479,6 +484,57 @@ public async Task DeclineRegistrationVerification_WithDecline_StateAndCommentSet processSteps.Should().ContainSingle().Which.Should().Match(x => x.ProcessStepStatusId == ProcessStepStatusId.TODO && x.ProcessStepTypeId == ProcessStepTypeId.TRIGGER_CALLBACK_OSP_DECLINED); + if (idpTypeId == IdentityProviderTypeId.SHARED) + { + A.CallTo(() => _provisioningManager.DeleteSharedIdpRealmAsync(IamAliasId)) + .MustHaveHappenedOnceExactly(); + } + A.CallTo(() => _provisioningManager.DeleteCentralIdentityProviderAsync(IamAliasId)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _provisioningManager.DeleteCentralRealmUserAsync("user123")) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task DeclineRegistrationVerification_WithApplicationNotFound_ThrowsArgumentException() + { + // Arrange + var applicationId = Guid.NewGuid(); + A.CallTo(() => _applicationRepository.GetCompanyIdNameForSubmittedApplication(applicationId)) + .Returns(new ValueTuple>, IEnumerable>>()); + async Task Act() => await _logic.DeclineRegistrationVerification(applicationId, "test", CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be($"CompanyApplication {applicationId} is not in status SUBMITTED (Parameter 'applicationId')"); + ex.ParamName.Should().Be("applicationId"); + } + + [Fact] + public async Task DeclineRegistrationVerification_WithMultipleIdps_ThrowsUnexpectedConditionException() + { + // Arrange + var applicationId = Guid.NewGuid(); + A.CallTo(() => _applicationRepository.GetCompanyIdNameForSubmittedApplication(applicationId)) + .Returns(new ValueTuple>, IEnumerable>>( + Guid.NewGuid(), + "test", + null, + new[] + { + ("idp1", IdentityProviderTypeId.SHARED), + ("idp2", IdentityProviderTypeId.SHARED) + }, + Enumerable.Empty>())); + async Task Act() => await _logic.DeclineRegistrationVerification(applicationId, "test", CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be($"There should only be one idp for application {applicationId}"); } #endregion @@ -808,15 +864,12 @@ private void SetupForApproveRegistrationVerification(ApplicationChecklistEntry a { { ApplicationChecklistEntryTypeId.BUSINESS_PARTNER_NUMBER, new ValueTuple(ApplicationChecklistEntryStatusId.DONE, null) } }.ToImmutableDictionary(), Enumerable.Empty())); - - A.CallTo(() => _applicationRepository.GetCompanyIdNameForSubmittedApplication(IdWithBpn)) - .Returns((CompanyId, CompanyName, ExistingExternalId)); } - private void SetupForDeclineRegistrationVerification(ApplicationChecklistEntry applicationChecklistEntry, CompanyApplication application, Company company, ApplicationChecklistEntryStatusId checklistStatusId) + private void SetupForDeclineRegistrationVerification(ApplicationChecklistEntry applicationChecklistEntry, CompanyApplication application, Company company, ApplicationChecklistEntryStatusId checklistStatusId, IdentityProviderTypeId idpTypeId) { A.CallTo(() => _checklistService.FinalizeChecklistEntryAndProcessSteps(A._, A>._, A>._, A?>._)) - .Invokes((IApplicationChecklistService.ManualChecklistProcessStepData _, Action? initail, Action action, IEnumerable? _) => + .Invokes((IApplicationChecklistService.ManualChecklistProcessStepData _, Action? _, Action action, IEnumerable? _) => { action.Invoke(applicationChecklistEntry); }); @@ -833,10 +886,10 @@ private void SetupForDeclineRegistrationVerification(ApplicationChecklistEntry a }.ToImmutableDictionary(), Enumerable.Empty())); A.CallTo(() => _applicationRepository.GetCompanyIdNameForSubmittedApplication(IdWithBpn)) - .Returns((CompanyId, CompanyName, ExistingExternalId)); + .Returns((CompanyId, CompanyName, ExistingExternalId, Enumerable.Repeat((IamAliasId, idpTypeId), 1), Enumerable.Repeat>((UserId, "user123"), 1))); A.CallTo(() => _checklistService.FinalizeChecklistEntryAndProcessSteps(A._, A>._, A>._, A?>._)) - .Invokes((IApplicationChecklistService.ManualChecklistProcessStepData _, Action initial, Action action, IEnumerable? _) => + .Invokes((IApplicationChecklistService.ManualChecklistProcessStepData _, Action _, Action action, IEnumerable? _) => { action.Invoke(applicationChecklistEntry); });