Skip to content

Commit

Permalink
Consumer API: Anonymization of Message Participants when Relationship…
Browse files Browse the repository at this point in the history
… Terminated (#727)

* feat: add domain event handler for relationships ready for deletion

* refactor: change approach no avoid unnecessary database calls and relationship queries

* test: add unit tests for sanitize after relationship deleted

* refactor: remove unnecessary methods

* refactor: use expression for message query filter

* fix: status property name changed after merge with main

* fix: add missing registration of event handlers for messages module

* test: add expression unit tests

* refactor: rename variables and add logging

* test: use fluent assertions and create test helper methods

* refactor: change logging

* test: improve tests

* fix: compiler error

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Timo Notheisen <[email protected]>
  • Loading branch information
3 people authored Jul 5, 2024
1 parent 7c899a2 commit 951ebc6
Show file tree
Hide file tree
Showing 14 changed files with 305 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Text;
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus;
using Backbone.DevelopmentKit.Identity.ValueObjects;
using Backbone.Modules.Messages.Application.Infrastructure.Persistence.Repository;
using Backbone.Modules.Messages.Domain.DomainEvents.Incoming;
using Backbone.Modules.Messages.Domain.Entities;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Backbone.Modules.Messages.Application.DomainEvents.Incoming.RelationshipStatusChanged;

public class RelationshipStatusChangedDomainEventHandler : IDomainEventHandler<RelationshipStatusChangedDomainEvent>
{
private const string DELETED_IDENTITY_STRING = "deleted identity";
private readonly IMessagesRepository _messagesRepository;
private readonly ILogger<RelationshipStatusChangedDomainEventHandler> _logger;
private readonly ApplicationOptions _applicationOptions;

public RelationshipStatusChangedDomainEventHandler(IMessagesRepository messagesRepository, IOptions<ApplicationOptions> applicationOptions,
ILogger<RelationshipStatusChangedDomainEventHandler> logger)
{
_messagesRepository = messagesRepository;
_logger = logger;
_applicationOptions = applicationOptions.Value;
}

public async Task Handle(RelationshipStatusChangedDomainEvent @event)
{
if (@event.NewStatus != RelationshipStatus.ReadyForDeletion.ToString())
{
_logger.LogTrace("Relationship status changed to {newStatus}. No Message anonymization required.", @event.NewStatus);
return;
}

var anonymizedIdentityAddress = IdentityAddress.Create(Encoding.Unicode.GetBytes(DELETED_IDENTITY_STRING), _applicationOptions.DidDomainName);
var messagesExchangedBetweenRelationshipParticipants = (await _messagesRepository.Find(Message.WasExchangedBetween(@event.Initiator, @event.Peer), CancellationToken.None)).ToList();
foreach (var message in messagesExchangedBetweenRelationshipParticipants)
{
message.SanitizeAfterRelationshipDeleted(@event.Initiator, @event.Peer, anonymizedIdentityAddress);
}

await _messagesRepository.Update(messagesExchangedBetweenRelationshipParticipants);
}
}

internal static partial class RelationshipStatusChangedLogs
{
[LoggerMessage(
EventName = "Messages.RelationshipStatusChangedDomainEventHandler.RelationshipStatusChanged",
Level = LogLevel.Debug,
Message = "Relationship status changed to {newStatus}. No Message anonymization required.")]
public static partial void RelationshipStatusChanged(this ILogger logger, string newStatus);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus;
using Backbone.Modules.Messages.Application.DomainEvents.Incoming.RelationshipStatusChanged;
using Backbone.Modules.Messages.Domain.DomainEvents.Incoming;

namespace Backbone.Modules.Messages.Application.Extensions;

public static class IEventBusExtensions
{
public static IEventBus AddMessagesDomainEventSubscriptions(this IEventBus eventBus)
{
eventBus.Subscribe<RelationshipStatusChangedDomainEvent, RelationshipStatusChangedDomainEventHandler>();
return eventBus;
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Reflection;
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus;
using Backbone.BuildingBlocks.Application.MediatR;
using Backbone.Modules.Messages.Application.AutoMapper;
using Backbone.Modules.Messages.Application.Messages.Commands.SendMessage;
Expand All @@ -18,5 +20,25 @@ public static void AddApplication(this IServiceCollection services)
);
services.AddAutoMapper(typeof(AutoMapperProfile).Assembly);
services.AddValidatorsFromAssembly(typeof(SendMessageCommandValidator).Assembly);
services.AddEventHandlers();
}

private static void AddEventHandlers(this IServiceCollection services)
{
foreach (var eventHandler in GetAllDomainEventHandlers())
{
services.AddTransient(eventHandler);
}
}

private static IEnumerable<Type> GetAllDomainEventHandlers()
{
var domainEventHandlerTypes =
from t in Assembly.GetExecutingAssembly().GetTypes()
from i in t.GetInterfaces()
where t.IsClass && !t.IsAbstract && i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDomainEventHandler<>)
select t;

return domainEventHandlerTypes;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ namespace Backbone.Modules.Messages.Application.Infrastructure.Persistence.Repos
public interface IMessagesRepository
{
Task<DbPaginationResult<Message>> FindMessagesWithIds(IEnumerable<MessageId> ids, IdentityAddress requiredParticipant, PaginationFilter paginationFilter, CancellationToken cancellationToken, bool track = false);

Task<Message> Find(MessageId id, IdentityAddress requiredParticipant, CancellationToken cancellationToken, bool track = false, bool fillBody = true);
Task Add(Message message, CancellationToken cancellationToken);
Task<int> CountUnreceivedMessagesFromSenderToRecipient(IdentityAddress sender, IdentityAddress recipient, CancellationToken cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using Backbone.Modules.Messages.Application;
using Backbone.Modules.Messages.Application.Extensions;
using Backbone.Modules.Messages.Infrastructure.Persistence;
using Backbone.Tooling.Extensions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -35,5 +34,6 @@ public override void ConfigureServices(IServiceCollection services, IConfigurati

public override void ConfigureEventBus(IEventBus eventBus)
{
eventBus.AddMessagesDomainEventSubscriptions();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Backbone.BuildingBlocks.Domain.Events;

namespace Backbone.Modules.Messages.Domain.DomainEvents.Incoming;

public class RelationshipStatusChangedDomainEvent : DomainEvent
{
public required string RelationshipId { get; set; }
public required string NewStatus { get; set; }
public required string Initiator { get; set; }
public required string Peer { get; set; }
}
38 changes: 33 additions & 5 deletions Modules/Messages/src/Messages.Domain/Entities/Message.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,45 @@ public void LoadBody(byte[] bytes)
public void ReplaceIdentityAddress(IdentityAddress oldIdentityAddress, IdentityAddress newIdentityAddress)
{
if (CreatedBy == oldIdentityAddress)
{
CreatedBy = newIdentityAddress;
}
AnonymizeSender(newIdentityAddress);

var recipient = Recipients.FirstOrDefault(r => r.Address == oldIdentityAddress);
AnonymizeRecipient(oldIdentityAddress, newIdentityAddress);
}

public void SanitizeAfterRelationshipDeleted(string participantOne, string participantTwo, IdentityAddress anonymizedIdentityAddress)
{
AnonymizeRecipient(participantOne, anonymizedIdentityAddress);
AnonymizeRecipient(participantTwo, anonymizedIdentityAddress);

recipient?.UpdateAddress(newIdentityAddress);
if (CanAnonymizeSender(anonymizedIdentityAddress))
AnonymizeSender(anonymizedIdentityAddress);
}

public static Expression<Func<Message, bool>> WasCreatedBy(IdentityAddress identityAddress)
{
return i => i.CreatedBy == identityAddress.ToString();
}

public static Expression<Func<Message, bool>> WasExchangedBetween(IdentityAddress identityAddress1, IdentityAddress identityAddress2)
{
return m =>
(m.CreatedBy == identityAddress1 && m.Recipients.Any(r => r.Address == identityAddress2)) ||
(m.CreatedBy == identityAddress2 && m.Recipients.Any(r => r.Address == identityAddress1));
}

private void AnonymizeRecipient(string participantAddress, IdentityAddress anonymizedIdentityAddress)
{
var recipient = Recipients.FirstOrDefault(r => r.Address == participantAddress);
recipient?.UpdateAddress(anonymizedIdentityAddress);
}

private bool CanAnonymizeSender(IdentityAddress anonymizedIdentityAddress)
{
return Recipients.All(r => r.Address == anonymizedIdentityAddress);
}

private void AnonymizeSender(IdentityAddress anonymizedIdentityAddress)
{
CreatedBy = anonymizedIdentityAddress;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,7 @@ public enum RelationshipStatus
Active = 20,
Rejected = 30,
Revoked = 40,
Terminated = 50
Terminated = 50,
DeletionProposed = 60,
ReadyForDeletion = 70
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,24 @@ namespace Backbone.Modules.Messages.Infrastructure.Persistence.Database.Reposito

public class RelationshipsRepository : IRelationshipsRepository
{
private readonly MessagesDbContext _dbContext;
private readonly IQueryable<Relationship> _readOnlyRelationships;

public RelationshipsRepository(MessagesDbContext dbContext)
{
_dbContext = dbContext;
_readOnlyRelationships = dbContext.Relationships.AsNoTracking();
}

public Task<RelationshipId?> GetIdOfRelationshipBetweenSenderAndRecipient(IdentityAddress sender, IdentityAddress recipient)
{
return _dbContext.Relationships
.AsNoTracking()
return _readOnlyRelationships
.WithParticipants(sender, recipient)
.Select(r => r.Id)
.FirstOrDefaultAsync();
}

public Task<Relationship?> FindYoungestRelationship(IdentityAddress sender, IdentityAddress recipient, CancellationToken cancellationToken)
{
return _dbContext.Relationships
.AsNoTracking()
return _readOnlyRelationships
.WithParticipants(sender, recipient)
.OrderByDescending(r => r.CreatedAt)
.FirstOrDefaultAsync(cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="8.3.0" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0"/>
<PackageReference Include="xunit" Version="2.8.1"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="xunit" Version="2.8.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Backbone.Modules.Messages.Domain.Entities;
using Backbone.UnitTestTools.BaseClasses;
using Backbone.UnitTestTools.Data;
using FluentAssertions;
using Xunit;

namespace Backbone.Modules.Messages.Domain.Tests.Messages;

public class ExpressionTests : AbstractTestsBase
{
#region WasExchangedBetween

[Fact]
public void WasExchangedBetween_with_true_assertion()
{
// Arrange
var senderAddress = TestDataGenerator.CreateRandomIdentityAddress();
var recipientAddress = TestDataGenerator.CreateRandomIdentityAddress();
var recipient = new RecipientInformation(recipientAddress, []);
var message = new Message(senderAddress, TestDataGenerator.CreateRandomDeviceId(), [], [], [recipient]);

// Act
var resultOne = message.EvaluateWasExchangedBetweenExpression(senderAddress, recipientAddress);
var resultTwo = message.EvaluateWasExchangedBetweenExpression(recipientAddress, senderAddress);

// Assert
resultOne.Should().BeTrue();
resultTwo.Should().BeTrue();
}

[Fact]
public void WasExchangedBetween_with_false_assertion()
{
// Arrange
var senderAddress = TestDataGenerator.CreateRandomIdentityAddress();
var recipientAddress = TestDataGenerator.CreateRandomIdentityAddress();
var recipient = new RecipientInformation(recipientAddress, []);
var message = new Message(senderAddress, TestDataGenerator.CreateRandomDeviceId(), [], [], [recipient]);

// Act
var result = message.EvaluateWasExchangedBetweenExpression(senderAddress, TestDataGenerator.CreateRandomIdentityAddress());

// Assert
result.Should().BeFalse();
}

#endregion
}

file static class MessageExtensions
{
public static bool EvaluateWasExchangedBetweenExpression(this Message message, string identityAddressOne, string identityAddressTwo)
{
var expression = Message.WasExchangedBetween(identityAddressOne, identityAddressTwo);
return expression.Compile()(message);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
using Backbone.Modules.Messages.Domain.DomainEvents.Outgoing;
using Backbone.Modules.Messages.Domain.Entities;
using Backbone.Modules.Messages.Domain.Ids;
using Backbone.Modules.Messages.Domain.Tests.TestHelpers;
using Backbone.UnitTestTools.BaseClasses;
using Backbone.UnitTestTools.Data;
using Backbone.UnitTestTools.Extensions;
using Backbone.UnitTestTools.FluentAssertions.Extensions;
using FluentAssertions;
using Xunit;
Expand Down Expand Up @@ -34,4 +35,56 @@ public void Raises_MessageCreatedDomainEvent_when_created()
domainEvent.Recipients.Should().HaveCount(1);
domainEvent.Recipients.First().Should().Be(recipient.Address);
}

[Fact]
public void SanitizeAfterRelationshipDeleted_anonymizes_recipient_and_sender_when_there_is_only_one_recipient()
{
// Arrange
var anonymizedAddress = TestDataGenerator.CreateRandomIdentityAddress();
var message = TestData.CreateMessageWithOneRecipient();

// Act
message.SanitizeAfterRelationshipDeleted(message.CreatedBy, message.Recipients.First().Address, anonymizedAddress);

// Assert
message.CreatedBy.Should().Be(anonymizedAddress);
message.Recipients.First().Address.Should().Be(anonymizedAddress);
}

[Fact]
public void SanitizeAfterRelationshipDeleted_does_not_anonymize_sender_as_long_as_there_are_still_unanonymized_recipients()
{
// Arrange
var senderAddress = TestDataGenerator.CreateRandomIdentityAddress();
var anonymizedAddress = TestDataGenerator.CreateRandomIdentityAddress();
var message = TestData.CreateMessageWithTwoRecipients(senderAddress);

// Act
message.SanitizeAfterRelationshipDeleted(senderAddress, message.Recipients.First().Address, anonymizedAddress);

// Assert
message.CreatedBy.Should().Be(senderAddress);

message.Recipients.First().Address.Should().Be(anonymizedAddress);
}

[Fact]
public void SanitizeAfterRelationshipDeleted_anonymizes_sender_when_there_are_no_more_unanonymized_recipients()
{
// Arrange
var senderAddress = TestDataGenerator.CreateRandomIdentityAddress();
var anonymizedAddress = TestDataGenerator.CreateRandomIdentityAddress();
var message = TestData.CreateMessageWithTwoRecipients(senderAddress);

message.SanitizeAfterRelationshipDeleted(senderAddress, message.Recipients.First().Address, anonymizedAddress);

// Act
message.SanitizeAfterRelationshipDeleted(senderAddress, message.Recipients.Second().Address, anonymizedAddress);

// Assert
message.CreatedBy.Should().Be(anonymizedAddress);

message.Recipients.First().Address.Should().Be(anonymizedAddress);
message.Recipients.Second().Address.Should().Be(anonymizedAddress);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Backbone.DevelopmentKit.Identity.ValueObjects;
using Backbone.Modules.Messages.Domain.Entities;
using Backbone.UnitTestTools.Data;

namespace Backbone.Modules.Messages.Domain.Tests.TestHelpers;

public static class TestData
{
public static Message CreateMessageWithOneRecipient(IdentityAddress? senderAddress = null, IdentityAddress? recipientAddress = null)
{
senderAddress ??= TestDataGenerator.CreateRandomIdentityAddress();
recipientAddress ??= TestDataGenerator.CreateRandomIdentityAddress();

var recipient = new RecipientInformation(recipientAddress, []);
return new Message(senderAddress, TestDataGenerator.CreateRandomDeviceId(), [], [], [recipient]);
}

public static Message CreateMessageWithTwoRecipients(IdentityAddress? senderAddress = null, IdentityAddress? recipient1Address = null, IdentityAddress? recipient2Address = null)
{
senderAddress ??= TestDataGenerator.CreateRandomIdentityAddress();
recipient1Address ??= TestDataGenerator.CreateRandomIdentityAddress();
recipient2Address ??= TestDataGenerator.CreateRandomIdentityAddress();

return new Message(senderAddress, TestDataGenerator.CreateRandomDeviceId(), [], [], [new RecipientInformation(recipient1Address, []), new RecipientInformation(recipient2Address, [])]);
}
}
Loading

0 comments on commit 951ebc6

Please sign in to comment.