Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consumer Api: Decompose Relationships during Identity Deletion #940

Merged
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
526ad3a
feat: Add relationship audit log reason for decomposition due to iden…
MH321Productions Nov 8, 2024
6d5f477
feat: Make relationship participants writable internally and write sk…
MH321Productions Nov 8, 2024
8f114d3
Merge branch 'main' into decompose-relationships-during-identity-dele…
MH321Productions Nov 8, 2024
b8814bf
Merge branch 'main' into decompose-relationships-during-identity-dele…
MH321Productions Nov 12, 2024
b87c915
Merge branch 'main' into decompose-relationships-during-identity-dele…
MH321Productions Nov 13, 2024
1209a9c
chore: Rename DeleteRelationshipsOfIdentityCommand to DecomposeRelati…
MH321Productions Nov 14, 2024
a32e753
feat: Make CreatedBy property in Relationship Audit Log nullable
MH321Productions Nov 14, 2024
7fb11e4
chore: Clear cache when deleting Relationships and Relationship Templ…
MH321Productions Nov 14, 2024
db3eb7e
fix: Include every properties when querying for relationships
MH321Productions Nov 14, 2024
33d73bb
feat: Add method to decompose relationship due to identity deletion
MH321Productions Nov 14, 2024
6c162c7
feat: Add methods and command for anonymizing a relationship
MH321Productions Nov 14, 2024
dbbd831
feat: Use the decomposition and anonymization commands
MH321Productions Nov 14, 2024
75bbc0a
Merge branch 'main' into decompose-relationships-during-identity-dele…
MH321Productions Nov 14, 2024
e2bfc52
Merge branch 'main' into decompose-relationships-during-identity-dele…
mergify[bot] Nov 15, 2024
3031858
Merge branch 'main' into decompose-relationships-during-identity-dele…
mergify[bot] Nov 15, 2024
7f250b4
chore: Rename tests, methods and variables, resolve pr comments
MH321Productions Nov 15, 2024
68abbd3
chore: Merge Decomposition and Anonymization into one command
MH321Productions Nov 18, 2024
f3849dd
chore: Remove cache clearing from repositories
MH321Productions Nov 18, 2024
e9b91f5
chore: Write unit tests for Relationship Decomposition due to Identit…
MH321Productions Nov 18, 2024
2df8e9e
Merge branch 'main' into decompose-relationships-during-identity-dele…
mergify[bot] Nov 18, 2024
dc8736b
Merge branch 'main' into decompose-relationships-during-identity-dele…
mergify[bot] Nov 19, 2024
7f65900
Merge branch 'main' into decompose-relationships-during-identity-dele…
mergify[bot] Nov 19, 2024
a1e7bd7
chore: Include Anonymization method in the Decomposition, remove unne…
MH321Productions Nov 19, 2024
28b40a4
Merge branch 'main' into decompose-relationships-during-identity-dele…
mergify[bot] Nov 20, 2024
bf5fc24
chore: Add unit test for anonymization#
MH321Productions Nov 20, 2024
1461f41
fix: Fix formatting
MH321Productions Nov 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Backbone.BuildingBlocks.Application.Identities;
using Backbone.Modules.Relationships.Application.Relationships.Commands.DeleteRelationshipsOfIdentity;
using Backbone.Modules.Relationships.Application.Relationships.Commands.DecomposeAndAnonymizeRelationshipsOfIdentity;
using Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.AnonymizeRelationshipTemplateAllocationsAllocatedByIdentity;
using Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.DeleteRelationshipTemplatesOfIdentity;
using FakeItEasy;
Expand All @@ -23,7 +23,7 @@ public async Task Deleter_calls_correct_command()

// Assert
A.CallTo(() => mockMediator.Send(
A<DeleteRelationshipsOfIdentityCommand>.That.Matches(i => i.IdentityAddress == identityAddress),
A<DecomposeAndAnonymizeRelationshipsOfIdentityCommand>.That.Matches(i => i.IdentityAddress == identityAddress),
A<CancellationToken>._)).MustHaveHappenedOnceExactly();
A.CallTo(() => mockMediator.Send(
A<DeleteRelationshipTemplatesOfIdentityCommand>.That.Matches(i => i.IdentityAddress == identityAddress),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Backbone.BuildingBlocks.Application.Identities;
using Backbone.DevelopmentKit.Identity.ValueObjects;
using Backbone.Modules.Relationships.Application.Relationships.Commands.DeleteRelationshipsOfIdentity;
using Backbone.Modules.Relationships.Application.Relationships.Commands.DecomposeAndAnonymizeRelationshipsOfIdentity;
using Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.AnonymizeRelationshipTemplateAllocationsAllocatedByIdentity;
using Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.DeleteRelationshipTemplatesOfIdentity;
using MediatR;
Expand All @@ -20,7 +20,7 @@ public IdentityDeleter(IMediator mediator, IDeletionProcessLogger deletionProces

public async Task Delete(IdentityAddress identityAddress)
{
await _mediator.Send(new DeleteRelationshipsOfIdentityCommand(identityAddress));
await _mediator.Send(new DecomposeAndAnonymizeRelationshipsOfIdentityCommand(identityAddress));
await _deletionProcessLogger.LogDeletion(identityAddress, "Relationships");
await _mediator.Send(new DeleteRelationshipTemplatesOfIdentityCommand(identityAddress));
await _deletionProcessLogger.LogDeletion(identityAddress, "RelationshipTemplates");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using MediatR;

namespace Backbone.Modules.Relationships.Application.Relationships.Commands.DecomposeAndAnonymizeRelationshipsOfIdentity;

public class DecomposeAndAnonymizeRelationshipsOfIdentityCommand : IRequest
{
public DecomposeAndAnonymizeRelationshipsOfIdentityCommand(string identityAddress)
{
IdentityAddress = identityAddress;
}

public string IdentityAddress { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Backbone.Modules.Relationships.Application.Infrastructure.Persistence.Repository;
using Backbone.Modules.Relationships.Domain.Aggregates.Relationships;
using FakeItEasy;
using Microsoft.Extensions.Options;

namespace Backbone.Modules.Relationships.Application.Relationships.Commands.DecomposeAndAnonymizeRelationshipsOfIdentity;

public class HandlerTests : AbstractTestsBase
{
[Fact]
public async Task Command_calls_update_on_repository()
{
// Arrange
var mockRelationshipTemplatesRepository = A.Fake<IRelationshipsRepository>();
var mockOptions = A.Dummy<IOptions<ApplicationOptions>>();

var handler = new Handler(mockRelationshipTemplatesRepository, mockOptions);
var request = new DecomposeAndAnonymizeRelationshipsOfIdentityCommand(CreateRandomIdentityAddress());

// Act
await handler.Handle(request, CancellationToken.None);

// Assert
A.CallTo(() => mockRelationshipTemplatesRepository.Update(A<IEnumerable<Relationship>>._)).MustHaveHappened();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Backbone.Modules.Relationships.Application.Infrastructure.Persistence.Repository;
using Backbone.Modules.Relationships.Domain.Aggregates.Relationships;
using MediatR;
using Microsoft.Extensions.Options;

namespace Backbone.Modules.Relationships.Application.Relationships.Commands.DecomposeAndAnonymizeRelationshipsOfIdentity;

public class Handler : IRequestHandler<DecomposeAndAnonymizeRelationshipsOfIdentityCommand>
{
private readonly IRelationshipsRepository _relationshipsRepository;
private readonly ApplicationOptions _applicationOptions;

public Handler(IRelationshipsRepository relationshipsRepository, IOptions<ApplicationOptions> applicationOptions)
{
_relationshipsRepository = relationshipsRepository;
_applicationOptions = applicationOptions.Value;
}

public async Task Handle(DecomposeAndAnonymizeRelationshipsOfIdentityCommand request, CancellationToken cancellationToken)
{
var relationships = (await _relationshipsRepository.FindRelationships(Relationship.HasParticipant(request.IdentityAddress), cancellationToken)).ToList();

foreach (var relationship in relationships)
relationship.DecomposeDueToIdentityDeletion(request.IdentityAddress, _applicationOptions.DidDomainName);

await _relationshipsRepository.Update(relationships);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Backbone.BuildingBlocks.Application.Extensions;
using Backbone.DevelopmentKit.Identity.ValueObjects;
using FluentValidation;

namespace Backbone.Modules.Relationships.Application.Relationships.Commands.DecomposeAndAnonymizeRelationshipsOfIdentity;

public class Validator : AbstractValidator<DecomposeAndAnonymizeRelationshipsOfIdentityCommand>
{
public Validator()
{
RuleFor(x => x.IdentityAddress).ValidId<DecomposeAndAnonymizeRelationshipsOfIdentityCommand, IdentityAddress>();
}
}

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ public RelationshipAuditLogEntryDTO(RelationshipAuditLogEntry entry)
{
CreatedAt = entry.CreatedAt;
CreatedBy = entry.CreatedBy;
CreatedByDevice = entry.CreatedByDevice;
CreatedByDevice = entry.CreatedByDevice?.Value;
Reason = entry.Reason.ToString();
OldStatus = entry.OldStatus.ToDtoString();
NewStatus = entry.NewStatus.ToDtoString();
}

public DateTime CreatedAt { get; set; }
public string CreatedBy { get; set; }
public string CreatedByDevice { get; set; }
public string? CreatedByDevice { get; set; }
public string Reason { get; set; }

public string? OldStatus { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using Backbone.BuildingBlocks.Domain.Exceptions;
using Backbone.DevelopmentKit.Identity.ValueObjects;
using Backbone.UnitTestTools.Extensions;
using static Backbone.Modules.Relationships.Domain.TestHelpers.TestData;

namespace Backbone.Modules.Relationships.Domain.Aggregates.Relationships;

public class RelationshipDecomposeDueToIdentityDeletionTests : AbstractTestsBase
{
private const string DID_DOMAIN_NAME = "localhost";

[Theory]
[InlineData(RelationshipStatus.Pending)]
[InlineData(RelationshipStatus.Active)]
[InlineData(RelationshipStatus.Rejected)]
[InlineData(RelationshipStatus.Revoked)]
[InlineData(RelationshipStatus.Terminated)]
public void Decomposition_can_be_performed_from_multiple_statuses(RelationshipStatus status)
{
// Arrange
var relationship = CreateRelationshipInStatus(status, IDENTITY_1, IDENTITY_2);

// Act
var acting = () => relationship.DecomposeDueToIdentityDeletion(IDENTITY_1, DID_DOMAIN_NAME);

// Assert
acting.Should().NotThrow();
relationship.Status.Should().Be(RelationshipStatus.DeletionProposed);
relationship.FromHasDecomposed.Should().BeTrue();
relationship.ToHasDecomposed.Should().BeFalse();
}

[Fact]
public void Decomposition_can_not_be_called_by_the_same_identity_twice()
{
// Arrange
var relationship = CreateRelationshipDecomposedByFrom(IDENTITY_1, IDENTITY_2);

// Act
var acting = () => relationship.DecomposeDueToIdentityDeletion(IDENTITY_1, DID_DOMAIN_NAME);

// Assert
acting.Should().Throw<DomainException>().WithError("error.platform.validation.relationship.relationshipAlreadyDecomposed");
}

[Fact]
public void Decomposition_can_not_be_performed_by_other_identities()
{
// Arrange
var relationship = CreateActiveRelationship(IDENTITY_1, IDENTITY_2);

// Act
var acting = () => relationship.DecomposeDueToIdentityDeletion(CreateRandomIdentityAddress(), DID_DOMAIN_NAME);

// Assert
acting.Should().Throw<DomainException>().WithError("error.platform.validation.relationship.requestingIdentityDoesNotBelongToRelationship");
}

[Fact]
public void Decomposition_by_both_identities_transitions_relationship_to_status_ReadyForDeletion()
{
// Arrange
var relationship = CreateActiveRelationship(IDENTITY_1, IDENTITY_2);
relationship.DecomposeDueToIdentityDeletion(IDENTITY_1, DID_DOMAIN_NAME);

// Act
var acting = () => relationship.DecomposeDueToIdentityDeletion(IDENTITY_2, DID_DOMAIN_NAME);

// Assert
acting.Should().NotThrow();
relationship.Status.Should().Be(RelationshipStatus.ReadyForDeletion);
}

[Fact]
public void Anonymize_anonymizes_the_audit_logs()
{
// Arrange
var relationship = CreateActiveRelationship(IDENTITY_1, IDENTITY_2);
var anonymousIdentity = IdentityAddress.GetAnonymized(DID_DOMAIN_NAME);

// Act
relationship.DecomposeDueToIdentityDeletion(IDENTITY_1, DID_DOMAIN_NAME);

// Assert
relationship.AuditLog.Should().HaveCount(3);
relationship.AuditLog[0].CreatedBy.Should().Be(anonymousIdentity);
relationship.AuditLog[2].CreatedBy.Should().Be(anonymousIdentity);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ private static void EnsureTargetIsNotSelf(RelationshipTemplate relationshipTempl
public RelationshipTemplateId? RelationshipTemplateId { get; }
public RelationshipTemplate? RelationshipTemplate { get; }

public IdentityAddress From { get; }
public IdentityAddress To { get; }
public IdentityAddress From { get; private set; }
public IdentityAddress To { get; private set; }

public DateTime CreatedAt { get; }

Expand Down Expand Up @@ -296,23 +296,35 @@ public void Decompose(IdentityAddress activeIdentity, DeviceId activeDevice)
EnsureStatus(RelationshipStatus.Rejected, RelationshipStatus.Revoked, RelationshipStatus.Terminated, RelationshipStatus.DeletionProposed);

if (Status is RelationshipStatus.Terminated or RelationshipStatus.Rejected or RelationshipStatus.Revoked)
DecomposeAsFirstParticipant(activeIdentity, activeDevice);
DecomposeAsFirstParticipant(activeIdentity, activeDevice, RelationshipAuditLogEntryReason.Decomposition);
else
DecomposeAsSecondParticipant(activeIdentity, activeDevice);
DecomposeAsSecondParticipant(activeIdentity, activeDevice, RelationshipAuditLogEntryReason.Decomposition);

RaiseDomainEvent(new RelationshipStatusChangedDomainEvent(this));
}

private void DecomposeAsFirstParticipant(IdentityAddress activeIdentity, DeviceId activeDevice)
public void DecomposeDueToIdentityDeletion(IdentityAddress identityToBeDeleted, string didDomainName)
{
EnsureStatus(RelationshipStatus.Terminated, RelationshipStatus.Rejected, RelationshipStatus.Revoked);
EnsureHasParticipant(identityToBeDeleted);
EnsureRelationshipNotDecomposedBy(identityToBeDeleted);

if (Status is RelationshipStatus.DeletionProposed)
DecomposeAsSecondParticipant(identityToBeDeleted, null, RelationshipAuditLogEntryReason.DecompositionDueToIdentityDeletion);
else
DecomposeAsFirstParticipant(identityToBeDeleted, null, RelationshipAuditLogEntryReason.DecompositionDueToIdentityDeletion);

AnonymizeParticipant(identityToBeDeleted, didDomainName);
RaiseDomainEvent(new RelationshipStatusChangedDomainEvent(this));
}

private void DecomposeAsFirstParticipant(IdentityAddress activeIdentity, DeviceId? activeDevice, RelationshipAuditLogEntryReason reason)
{
var oldStatus = Status;

Status = RelationshipStatus.DeletionProposed;

var auditLogEntry = new RelationshipAuditLogEntry(
RelationshipAuditLogEntryReason.Decomposition,
reason,
oldStatus,
RelationshipStatus.DeletionProposed,
activeIdentity,
Expand All @@ -326,14 +338,14 @@ private void DecomposeAsFirstParticipant(IdentityAddress activeIdentity, DeviceI
ToHasDecomposed = true;
}

private void DecomposeAsSecondParticipant(IdentityAddress activeIdentity, DeviceId activeDevice)
private void DecomposeAsSecondParticipant(IdentityAddress activeIdentity, DeviceId? activeDevice, RelationshipAuditLogEntryReason reason)
{
EnsureStatus(RelationshipStatus.DeletionProposed);

Status = RelationshipStatus.ReadyForDeletion;

var auditLogEntry = new RelationshipAuditLogEntry(
RelationshipAuditLogEntryReason.Decomposition,
reason,
RelationshipStatus.DeletionProposed,
RelationshipStatus.ReadyForDeletion,
activeIdentity,
Expand All @@ -357,6 +369,23 @@ private void EnsureHasParticipant(IdentityAddress activeIdentity)
throw new DomainException(DomainErrors.RequestingIdentityDoesNotBelongToRelationship());
}

private void AnonymizeParticipant(IdentityAddress identityToAnonymize, string didDomainName)
{
EnsureHasParticipant(identityToAnonymize);
EnsureStatus(RelationshipStatus.DeletionProposed, RelationshipStatus.ReadyForDeletion);

var anonymousIdentity = IdentityAddress.GetAnonymized(didDomainName);

if (From == identityToAnonymize)
From = anonymousIdentity;
else
To = anonymousIdentity;

foreach (var auditLogEntry in AuditLog)
if (auditLogEntry.CreatedBy == identityToAnonymize)
auditLogEntry.AnonymizeIdentity(anonymousIdentity);
}

public void ParticipantIsToBeDeleted(IdentityAddress identityToBeDeleted, DateTime gracePeriodEndsAt)
{
var peer = GetPeerOf(identityToBeDeleted);
Expand Down
Loading