From 7a6a86d3aafbb3eccac1255773f1eb7926190473 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Fri, 13 Sep 2024 10:31:22 +0200 Subject: [PATCH] chore(resources): Archive DelegatedResourceOwners when org unit is deleted (#697) - [ ] New feature - [ ] Bug fix - [ ] High impact **Description of work:** 55599 - Adds the command ArchiveDelegatedResourceOwners that archives and removes roles of the specified delegated resource owners - Adds a new event handler for when an org unit is deleted which calls the new command **Testing:** - [x] Can be tested - [x] Automatic tests created / updated - [x] Local tests are passing Wrote a unit test for the command. Tested by creating and executing a temporary admin endpoint which calls the archive command. **Checklist:** - [x] Considered automated tests - [x] Considered updating specification / documentation - [x] Considered work items - [x] Considered security - [x] Performed developer testing - [x] Checklist finalized / ready for review --- .../ExpiredDelegatedRolesHostedService.cs | 14 +--- ...DbDelegatedDepartmentResponsibleHistory.cs | 18 +++++ .../ArchiveDelegatedResourceOwners.cs | 73 +++++++++++++++++++ ...d.ArchiveDelegatedResourceOwnersHandler.cs | 45 ++++++++++++ .../DepartmentTests.cs | 65 +++++++++++++++++ .../DbTestFixture.cs | 2 + 6 files changed, 204 insertions(+), 13 deletions(-) create mode 100644 src/backend/api/Fusion.Resources.Domain/Commands/Departments/ArchiveDelegatedResourceOwners.cs create mode 100644 src/backend/api/Fusion.Resources.Domain/Notifications/System/OrgUnitDeleted.ArchiveDelegatedResourceOwnersHandler.cs create mode 100644 src/backend/tests/Fusion.Resources.Domain.Tests/DepartmentTests.cs diff --git a/src/backend/api/Fusion.Resources.Api/HostedServices/ExpiredDelegatedRolesHostedService.cs b/src/backend/api/Fusion.Resources.Api/HostedServices/ExpiredDelegatedRolesHostedService.cs index f6002d10e..4156049fe 100644 --- a/src/backend/api/Fusion.Resources.Api/HostedServices/ExpiredDelegatedRolesHostedService.cs +++ b/src/backend/api/Fusion.Resources.Api/HostedServices/ExpiredDelegatedRolesHostedService.cs @@ -90,19 +90,7 @@ private static async Task CheckDelegatedDepartmentResponsibleExpirationAsync(Tel db.DelegatedDepartmentResponsibles.RemoveRange(toExpire); - var expiredRecords = toExpire.Select(x => new DbDelegatedDepartmentResponsibleHistory - { - Id = Guid.NewGuid(), - Archived = DateTimeOffset.UtcNow, - DateTo = x.DateTo, - DepartmentId = x.DepartmentId, - ResponsibleAzureObjectId = x.ResponsibleAzureObjectId, - DateCreated = x.DateCreated, - DateFrom = x.DateFrom, - DateUpdated = x.DateUpdated, - Reason = x.Reason, - UpdatedBy = x.UpdatedBy - }); + var expiredRecords = toExpire.Select(x => new DbDelegatedDepartmentResponsibleHistory(x)); await db.DelegatedDepartmentResponsiblesHistory.AddRangeAsync(expiredRecords); await db.SaveChangesAsync(); } diff --git a/src/backend/api/Fusion.Resources.Database/Entities/DbDelegatedDepartmentResponsibleHistory.cs b/src/backend/api/Fusion.Resources.Database/Entities/DbDelegatedDepartmentResponsibleHistory.cs index 6019d3214..5d73e1307 100644 --- a/src/backend/api/Fusion.Resources.Database/Entities/DbDelegatedDepartmentResponsibleHistory.cs +++ b/src/backend/api/Fusion.Resources.Database/Entities/DbDelegatedDepartmentResponsibleHistory.cs @@ -18,6 +18,24 @@ public class DbDelegatedDepartmentResponsibleHistory public Guid? UpdatedBy { get; set; } public string? Reason { get; set; } + public DbDelegatedDepartmentResponsibleHistory() + { + } + + public DbDelegatedDepartmentResponsibleHistory(DbDelegatedDepartmentResponsible dbDelegatedDepartmentResponsible) + { + Id = Guid.NewGuid(); + Archived = DateTimeOffset.UtcNow; + DepartmentId = dbDelegatedDepartmentResponsible.DepartmentId; + ResponsibleAzureObjectId = dbDelegatedDepartmentResponsible.ResponsibleAzureObjectId; + DateFrom = dbDelegatedDepartmentResponsible.DateFrom; + DateTo = dbDelegatedDepartmentResponsible.DateTo; + DateCreated = dbDelegatedDepartmentResponsible.DateCreated; + DateUpdated = dbDelegatedDepartmentResponsible.DateUpdated; + UpdatedBy = dbDelegatedDepartmentResponsible.UpdatedBy; + Reason = dbDelegatedDepartmentResponsible.Reason; + } + internal static void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/src/backend/api/Fusion.Resources.Domain/Commands/Departments/ArchiveDelegatedResourceOwners.cs b/src/backend/api/Fusion.Resources.Domain/Commands/Departments/ArchiveDelegatedResourceOwners.cs new file mode 100644 index 000000000..7cb682fec --- /dev/null +++ b/src/backend/api/Fusion.Resources.Domain/Commands/Departments/ArchiveDelegatedResourceOwners.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Fusion.Integration.Profile; +using Fusion.Integration.Roles; +using Fusion.Resources.Database; +using Fusion.Resources.Database.Entities; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Fusion.Resources.Domain.Commands.Departments; + +/// Archive delegated resource owners for the department and remove their roles. +public class ArchiveDelegatedResourceOwners : TrackableRequest +{ + public LineOrgId DepartmentId { get; private init; } + public ICollection? ResourceOwnersToArchive { get; private set; } + + + public ArchiveDelegatedResourceOwners(LineOrgId departmentId) + { + DepartmentId = departmentId; + } + + /// Only archive the resource owners with the provided Azure Object Ids + public ArchiveDelegatedResourceOwners WhereResourceOwnersAzureId(ICollection resourceOwners) + { + ArgumentNullException.ThrowIfNull(resourceOwners); + ResourceOwnersToArchive = resourceOwners; + return this; + } + + + public class ArchiveDelegatedResourceOwnersHandler : IRequestHandler + { + private readonly ResourcesDbContext db; + private readonly IFusionRolesClient rolesClient; + + + public ArchiveDelegatedResourceOwnersHandler(ResourcesDbContext db, IFusionRolesClient rolesClient) + { + this.db = db; + this.rolesClient = rolesClient; + } + + public async Task Handle(ArchiveDelegatedResourceOwners request, CancellationToken cancellationToken) + { + var delegatedResourceOwnersToArchive = await db.DelegatedDepartmentResponsibles + .Where(r => r.DepartmentId == request.DepartmentId.FullDepartment) + .Where(r => request.ResourceOwnersToArchive == null || request.ResourceOwnersToArchive.Contains(r.ResponsibleAzureObjectId)) + .ToListAsync(cancellationToken); + + foreach (var resourceOwner in delegatedResourceOwnersToArchive) + { + await rolesClient.DeleteRolesAsync( + new PersonIdentifier(resourceOwner.ResponsibleAzureObjectId), + q => q.WhereRoleName(AccessRoles.ResourceOwner).WhereScopeValue(request.DepartmentId.FullDepartment) + ); + + db.DelegatedDepartmentResponsibles.Remove(resourceOwner); + + var archivedDelegateResourceOwner = new DbDelegatedDepartmentResponsibleHistory(resourceOwner); + + db.DelegatedDepartmentResponsiblesHistory.Add(archivedDelegateResourceOwner); + + await db.SaveChangesAsync(CancellationToken.None); + cancellationToken.ThrowIfCancellationRequested(); + } + } + } +} \ No newline at end of file diff --git a/src/backend/api/Fusion.Resources.Domain/Notifications/System/OrgUnitDeleted.ArchiveDelegatedResourceOwnersHandler.cs b/src/backend/api/Fusion.Resources.Domain/Notifications/System/OrgUnitDeleted.ArchiveDelegatedResourceOwnersHandler.cs new file mode 100644 index 000000000..fa12ab36e --- /dev/null +++ b/src/backend/api/Fusion.Resources.Domain/Notifications/System/OrgUnitDeleted.ArchiveDelegatedResourceOwnersHandler.cs @@ -0,0 +1,45 @@ +using System; +using Fusion.Resources.Database; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Fusion.Integration.Profile; +using Fusion.Integration.Roles; +using Fusion.Resources.Database.Entities; +using Fusion.Resources.Domain.Commands.Departments; + +namespace Fusion.Resources.Domain.Notifications.System; + +public partial class OrgUnitDeleted +{ + /// + /// Archive all delegated resource owners for the deleted department and remove their roles. + /// + public class ArchiveDelegatedResourceOwnersHandler : INotificationHandler + { + private readonly ILogger logger; + private readonly IMediator mediator; + + public ArchiveDelegatedResourceOwnersHandler(ILogger logger, IMediator mediator) + { + this.logger = logger; + this.mediator = mediator; + } + + public async Task Handle(OrgUnitDeleted notification, CancellationToken cancellationToken) + { + logger.LogInformation("Archiving delegated resource owners for deleted department {FullDepartment}", notification.FullDepartment); + + using var systemAccountScope = mediator.SystemAccountScope(); + + await mediator.Send(new ArchiveDelegatedResourceOwners(new LineOrgId() + { + FullDepartment = notification.FullDepartment, + SapId = notification.SapId + }), cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/backend/tests/Fusion.Resources.Domain.Tests/DepartmentTests.cs b/src/backend/tests/Fusion.Resources.Domain.Tests/DepartmentTests.cs new file mode 100644 index 000000000..6c75f10f4 --- /dev/null +++ b/src/backend/tests/Fusion.Resources.Domain.Tests/DepartmentTests.cs @@ -0,0 +1,65 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Fusion.Resources.Database; +using Fusion.Resources.Domain.Commands.Departments; +using Fusion.Resources.Test.Core; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Fusion.Resources.Domain.Tests; + +public class DepartmentTests : DbTestFixture +{ + [Fact] + public async Task ArchiveDelegatedResourceOwners_ShouldArchiveResourceOwners() + { + var mediator = serviceProvider.GetRequiredService(); + var db = serviceProvider.GetRequiredService(); + + var departmentId = new LineOrgId() + { + FullDepartment = "PRD TDI", + SapId = "123456" + }; + var toBeArchivedResourceOwner = CreateTestPerson("toBeArchivedResourceOwner"); + var resourceOwner = CreateTestPerson("resourceOwner"); + + var createCommand = new AddDelegatedResourceOwner(departmentId, toBeArchivedResourceOwner.AzureUniqueId) + { + Reason = "Test", + DateFrom = DateTimeOffset.Now, + DateTo = DateTimeOffset.Now.AddMonths(1) + }; + + + var createCommand2 = new AddDelegatedResourceOwner(departmentId, resourceOwner.AzureUniqueId) + { + Reason = "Test2", + DateFrom = DateTimeOffset.Now, + DateTo = DateTimeOffset.Now.AddMonths(1) + }; + + await mediator.Send(createCommand); + await mediator.Send(createCommand2); + + var command = new ArchiveDelegatedResourceOwners(departmentId).WhereResourceOwnersAzureId([toBeArchivedResourceOwner.AzureUniqueId]); + + await mediator.Send(command); + + var remainingResourceOwners = await db.DelegatedDepartmentResponsibles + .Where(r => r.DepartmentId == departmentId.FullDepartment) + .ToListAsync(); + + remainingResourceOwners.Should().HaveCount(1); + remainingResourceOwners.Should().ContainSingle(r => r.ResponsibleAzureObjectId == resourceOwner.AzureUniqueId); + + var archivedResourceOwners = await db.DelegatedDepartmentResponsiblesHistory.ToListAsync(); + + archivedResourceOwners.Should().HaveCount(1); + archivedResourceOwners.Should().ContainSingle(r => r.ResponsibleAzureObjectId == toBeArchivedResourceOwner.AzureUniqueId); + } +} \ No newline at end of file diff --git a/src/backend/tests/Fusion.Resources.Test.Core/DbTestFixture.cs b/src/backend/tests/Fusion.Resources.Test.Core/DbTestFixture.cs index 998ffef40..c588a4083 100644 --- a/src/backend/tests/Fusion.Resources.Test.Core/DbTestFixture.cs +++ b/src/backend/tests/Fusion.Resources.Test.Core/DbTestFixture.cs @@ -14,6 +14,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Fusion.Integration.Roles; namespace Fusion.Resources.Test.Core { @@ -50,6 +51,7 @@ protected virtual void ConfigureServices(ServiceCollection services) services.AddSingleton(new Mock(MockBehavior.Loose).Object); services.AddSingleton(new Mock(MockBehavior.Loose).Object); services.AddSingleton(new Mock(MockBehavior.Loose).Object); + services.AddSingleton(new Mock(MockBehavior.Loose).Object); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(TestTrackableRequestBehaviour<,>));