From 970b7b108ff9a007956abc743abb20855c3dc72f Mon Sep 17 00:00:00 2001 From: Mika Herrmann Date: Wed, 4 Dec 2024 13:35:51 +0100 Subject: [PATCH 01/16] fix: Revert wrong test user infos introduced in #949 --- .../Devices.Application/Users/Commands/SeedTestUsers/Handler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Devices/src/Devices.Application/Users/Commands/SeedTestUsers/Handler.cs b/Modules/Devices/src/Devices.Application/Users/Commands/SeedTestUsers/Handler.cs index f779f72f3f..a3553a6118 100644 --- a/Modules/Devices/src/Devices.Application/Users/Commands/SeedTestUsers/Handler.cs +++ b/Modules/Devices/src/Devices.Application/Users/Commands/SeedTestUsers/Handler.cs @@ -28,7 +28,7 @@ public async Task Handle(SeedTestUsersCommand request, CancellationToken cancell _basicTier = (await _tiersRepository.FindBasicTier(cancellationToken))!; await CreateIdentityIfNecessary([1, 1, 1, 1, 1], "USRa", "Aaaaaaaa1!"); - await CreateIdentityIfNecessary([1, 1, 1, 1, 1], "USRa", "Bbbbbbbb1!"); + await CreateIdentityIfNecessary([2, 2, 2, 2, 2], "USRb", "Bbbbbbbb1!"); } private async Task CreateIdentityIfNecessary(byte[] publicKey, string username, string password) From d3437a71742d5c85cf0014d78e4b9ac1db89a28a Mon Sep 17 00:00:00 2001 From: Mika Herrmann Date: Wed, 4 Dec 2024 13:36:59 +0100 Subject: [PATCH 02/16] feat: Add methods for anonymizing the ForIdentity property --- .../RelationshipTemplate.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.cs b/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.cs index 7e16fd2e95..a78f4464b9 100644 --- a/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.cs +++ b/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.cs @@ -50,7 +50,7 @@ public RelationshipTemplate(IdentityAddress createdBy, DeviceId createdByDevice, public DateTime CreatedAt { get; set; } - public IdentityAddress? ForIdentity { get; set; } + public IdentityAddress? ForIdentity { get; private set; } public byte[]? Password { get; set; } public List Allocations { get; set; } = []; @@ -69,6 +69,15 @@ public void AllocateFor(IdentityAddress identity, DeviceId device) Allocations.Add(new RelationshipTemplateAllocation(Id, identity, device)); } + public void AnonymizeForIdentity(string didDomainName) + { + EnsureIsPersonalized(); + + var anonymousIdentity = IdentityAddress.GetAnonymized(didDomainName); + + ForIdentity = anonymousIdentity; + } + public bool IsAllocatedBy(IdentityAddress identity) { return Allocations.Any(x => x.AllocatedBy == identity); @@ -87,6 +96,11 @@ public void EnsureCanBeDeletedBy(IdentityAddress identityAddress) if (CreatedBy != identityAddress) throw new DomainActionForbiddenException(); } + public void EnsureIsPersonalized() + { + if (ForIdentity == null) throw new DomainException(DomainErrors.RelationshipTemplateNotPersonalized()); + } + #region Expressions public static Expression> HasId(RelationshipTemplateId id) @@ -117,5 +131,10 @@ public static Expression> CanBeCollectedWithPas a.AllocatedBy == activeIdentity); // if the template has already been allocated by the active identity, it doesn't need to pass the password again; } + public static Expression> IsFor(IdentityAddress identityAddress) + { + return relationshipTemplate => relationshipTemplate.ForIdentity == identityAddress; + } + #endregion } From 1224f7037f2c0b5e293688ba6ca07af9c3fda75f Mon Sep 17 00:00:00 2001 From: Mika Herrmann Date: Wed, 4 Dec 2024 13:45:11 +0100 Subject: [PATCH 03/16] feat: Add methods for querying and updating relationship templates --- .../Repository/IRelationshipTemplatesRepository.cs | 3 +++ .../Repository/RelationshipTemplatesRepository.cs | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/Modules/Relationships/src/Relationships.Application/Infrastructure/Persistence/Repository/IRelationshipTemplatesRepository.cs b/Modules/Relationships/src/Relationships.Application/Infrastructure/Persistence/Repository/IRelationshipTemplatesRepository.cs index 7b845a82ce..2f41f02727 100644 --- a/Modules/Relationships/src/Relationships.Application/Infrastructure/Persistence/Repository/IRelationshipTemplatesRepository.cs +++ b/Modules/Relationships/src/Relationships.Application/Infrastructure/Persistence/Repository/IRelationshipTemplatesRepository.cs @@ -12,9 +12,12 @@ public interface IRelationshipTemplatesRepository Task> FindTemplates(IEnumerable queryItems, IdentityAddress activeIdentity, PaginationFilter paginationFilter, CancellationToken cancellationToken, bool track = false); + Task> FindTemplates(Expression> filter, CancellationToken cancellationToken); + Task Find(RelationshipTemplateId id, IdentityAddress identityAddress, CancellationToken cancellationToken, bool track = false); Task Add(RelationshipTemplate template, CancellationToken cancellationToken); Task Update(RelationshipTemplate template); + Task Update(IEnumerable templates, CancellationToken cancellationToken); Task Delete(Expression> filter, CancellationToken cancellationToken); Task Delete(RelationshipTemplate template, CancellationToken cancellationToken); diff --git a/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/Repository/RelationshipTemplatesRepository.cs b/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/Repository/RelationshipTemplatesRepository.cs index 4afa5fac1b..82ac567f55 100644 --- a/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/Repository/RelationshipTemplatesRepository.cs +++ b/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/Repository/RelationshipTemplatesRepository.cs @@ -79,12 +79,23 @@ public async Task> FindTemplates(IEnume return templates; } + public async Task> FindTemplates(Expression> filter, CancellationToken cancellationToken) + { + return await _templates.Where(filter).ToListAsync(cancellationToken); + } + public async Task Update(RelationshipTemplate template) { _templates.Update(template); await _dbContext.SaveChangesAsync(); } + public async Task Update(IEnumerable templates, CancellationToken cancellationToken) + { + _templates.UpdateRange(templates); + await _dbContext.SaveChangesAsync(cancellationToken); + } + public async Task> FindRelationshipTemplateAllocations(Expression> filter, CancellationToken cancellationToken) { From e5a5122f5e0e16d3469a0972bfbd1c11aa73c4a2 Mon Sep 17 00:00:00 2001 From: Mika Herrmann Date: Wed, 4 Dec 2024 13:46:38 +0100 Subject: [PATCH 04/16] feat: Add domain error for a non-personalized relationship template --- .../Relationships/src/Relationships.Domain/DomainErrors.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Modules/Relationships/src/Relationships.Domain/DomainErrors.cs b/Modules/Relationships/src/Relationships.Domain/DomainErrors.cs index 61fce6d94d..6652e1e392 100644 --- a/Modules/Relationships/src/Relationships.Domain/DomainErrors.cs +++ b/Modules/Relationships/src/Relationships.Domain/DomainErrors.cs @@ -82,4 +82,10 @@ public static DomainError RelationshipTemplateNotAllocated() return new DomainError("error.platform.validation.relationship.relationshipTemplateNotAllocated", "The relationship template has to be allocated before it can be used to establish a relationship. Send a GET request to the /RelationshipTemplates/{id} endpoint to allocate the template."); } + + public static DomainError RelationshipTemplateNotPersonalized() + { + return new DomainError("error.platform.validation.relationship.relationshipTemplateNotPersonalized", + "The relationship template has to be personalized."); + } } From ab52423123b40bbfc4284b4510a5b48740392900 Mon Sep 17 00:00:00 2001 From: Mika Herrmann Date: Wed, 4 Dec 2024 13:48:06 +0100 Subject: [PATCH 05/16] feat: Add command, handler and validator for anonymizing personalized relationship templates for an Identity --- ...RelationshipTemplatesForIdentityCommand.cs | 13 +++++++++ .../Handler.cs | 28 +++++++++++++++++++ .../Validator.cs | 14 ++++++++++ 3 files changed, 55 insertions(+) create mode 100644 Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/AnonymizeRelationshipTemplatesForIdentityCommand.cs create mode 100644 Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/Handler.cs create mode 100644 Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/Validator.cs diff --git a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/AnonymizeRelationshipTemplatesForIdentityCommand.cs b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/AnonymizeRelationshipTemplatesForIdentityCommand.cs new file mode 100644 index 0000000000..266548a344 --- /dev/null +++ b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/AnonymizeRelationshipTemplatesForIdentityCommand.cs @@ -0,0 +1,13 @@ +using MediatR; + +namespace Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.AnonymizeRelationshipTemplatesForIdentity; + +public class AnonymizeRelationshipTemplatesForIdentityCommand : IRequest +{ + public AnonymizeRelationshipTemplatesForIdentityCommand(string identityAddress) + { + IdentityAddress = identityAddress; + } + + public string IdentityAddress { get; set; } +} diff --git a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/Handler.cs b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/Handler.cs new file mode 100644 index 0000000000..429fc18f77 --- /dev/null +++ b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/Handler.cs @@ -0,0 +1,28 @@ +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Relationships.Application.Infrastructure.Persistence.Repository; +using Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates; +using MediatR; +using Microsoft.Extensions.Options; + +namespace Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.AnonymizeRelationshipTemplatesForIdentity; + +public class Handler : IRequestHandler +{ + private readonly IRelationshipTemplatesRepository _relationshipTemplatesRepository; + private readonly ApplicationOptions _applicationOptions; + + public Handler(IRelationshipTemplatesRepository relationshipTemplatesRepository, IOptions options) + { + _relationshipTemplatesRepository = relationshipTemplatesRepository; + _applicationOptions = options.Value; + } + + public async Task Handle(AnonymizeRelationshipTemplatesForIdentityCommand request, CancellationToken cancellationToken) + { + var relationshipTemplates = (await _relationshipTemplatesRepository.FindTemplates(RelationshipTemplate.IsFor(IdentityAddress.Parse(request.IdentityAddress)), cancellationToken)).ToList(); + + foreach (var relationshipTemplate in relationshipTemplates) relationshipTemplate.AnonymizeForIdentity(_applicationOptions.DidDomainName); + + await _relationshipTemplatesRepository.Update(relationshipTemplates, cancellationToken); + } +} diff --git a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/Validator.cs b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/Validator.cs new file mode 100644 index 0000000000..f68d8dfe19 --- /dev/null +++ b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/Validator.cs @@ -0,0 +1,14 @@ +using Backbone.BuildingBlocks.Application.Extensions; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using FluentValidation; + +namespace Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.AnonymizeRelationshipTemplatesForIdentity; + +public class Validator : AbstractValidator +{ + public Validator() + { + RuleFor(c => c.IdentityAddress) + .ValidId(); + } +} From 4d96e5839d892a9fd17adcd81753e06bfa7f68b7 Mon Sep 17 00:00:00 2001 From: Mika Herrmann Date: Wed, 4 Dec 2024 13:49:19 +0100 Subject: [PATCH 06/16] feat: Add anonymization command to IdentityDeleter --- .../src/Relationships.Application/Identities/IdentityDeleter.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Modules/Relationships/src/Relationships.Application/Identities/IdentityDeleter.cs b/Modules/Relationships/src/Relationships.Application/Identities/IdentityDeleter.cs index 0f5d0b023e..9df8b9239f 100644 --- a/Modules/Relationships/src/Relationships.Application/Identities/IdentityDeleter.cs +++ b/Modules/Relationships/src/Relationships.Application/Identities/IdentityDeleter.cs @@ -2,6 +2,7 @@ using Backbone.DevelopmentKit.Identity.ValueObjects; using Backbone.Modules.Relationships.Application.Relationships.Commands.DecomposeAndAnonymizeRelationshipsOfIdentity; using Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.AnonymizeRelationshipTemplateAllocationsAllocatedByIdentity; +using Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.AnonymizeRelationshipTemplatesForIdentity; using Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.DeleteRelationshipTemplatesOfIdentity; using MediatR; @@ -23,6 +24,7 @@ public async Task Delete(IdentityAddress identityAddress) await _mediator.Send(new DecomposeAndAnonymizeRelationshipsOfIdentityCommand(identityAddress)); await _deletionProcessLogger.LogDeletion(identityAddress, "Relationships"); await _mediator.Send(new DeleteRelationshipTemplatesOfIdentityCommand(identityAddress)); + await _mediator.Send(new AnonymizeRelationshipTemplatesForIdentityCommand(identityAddress)); await _deletionProcessLogger.LogDeletion(identityAddress, "RelationshipTemplates"); await _mediator.Send(new AnonymizeRelationshipTemplateAllocationsAllocatedByIdentityCommand(identityAddress)); await _deletionProcessLogger.LogDeletion(identityAddress, "RelationshipTemplateAllocations"); From 8483edf45210760193c9da6244272e52e914fa5b Mon Sep 17 00:00:00 2001 From: Mika Herrmann Date: Wed, 4 Dec 2024 13:51:12 +0100 Subject: [PATCH 07/16] test: Add unit tests for the IdentityDeleter and the relationship template anonymization method --- .../Identities/IdentityDeleter.Tests.cs | 4 ++ ...nshipTemplate.AnonymizeForIdentityTests.cs | 43 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.AnonymizeForIdentityTests.cs diff --git a/Modules/Relationships/src/Relationships.Application/Identities/IdentityDeleter.Tests.cs b/Modules/Relationships/src/Relationships.Application/Identities/IdentityDeleter.Tests.cs index 7bc65902e9..7bd59b7c74 100644 --- a/Modules/Relationships/src/Relationships.Application/Identities/IdentityDeleter.Tests.cs +++ b/Modules/Relationships/src/Relationships.Application/Identities/IdentityDeleter.Tests.cs @@ -1,6 +1,7 @@ using Backbone.BuildingBlocks.Application.Identities; using Backbone.Modules.Relationships.Application.Relationships.Commands.DecomposeAndAnonymizeRelationshipsOfIdentity; using Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.AnonymizeRelationshipTemplateAllocationsAllocatedByIdentity; +using Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.AnonymizeRelationshipTemplatesForIdentity; using Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.DeleteRelationshipTemplatesOfIdentity; using FakeItEasy; using MediatR; @@ -28,6 +29,9 @@ public async Task Deleter_calls_correct_command() A.CallTo(() => mockMediator.Send( A.That.Matches(i => i.IdentityAddress == identityAddress), A._)).MustHaveHappenedOnceExactly(); + A.CallTo(() => mockMediator.Send( + A.That.Matches(i => i.IdentityAddress == identityAddress), + A._)).MustHaveHappenedOnceExactly(); A.CallTo(() => mockMediator.Send( A.That.Matches(i => i.IdentityAddress == identityAddress), A._)).MustHaveHappenedOnceExactly(); diff --git a/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.AnonymizeForIdentityTests.cs b/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.AnonymizeForIdentityTests.cs new file mode 100644 index 0000000000..7e8b5919fb --- /dev/null +++ b/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.AnonymizeForIdentityTests.cs @@ -0,0 +1,43 @@ +using Backbone.BuildingBlocks.Domain.Exceptions; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.UnitTestTools.Extensions; + +namespace Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates; + +public class RelationshipTemplateAnonymizeForIdentityTests : AbstractTestsBase +{ + private const string DID_DOMAIN_NAME = "localhost"; + + [Fact] + public void Personalized_template_can_be_anonymized() + { + // Arrange + var creatorIdentityAddress = CreateRandomIdentityAddress(); + var forIdentityAddress = CreateRandomIdentityAddress(); + var deviceId = CreateRandomDeviceId(); + byte[] content = [1, 1, 1, 1, 1, 1, 1, 1]; + var relationshipTemplate = new RelationshipTemplate(creatorIdentityAddress, deviceId, null, null, content, forIdentityAddress); + + // Act + relationshipTemplate.AnonymizeForIdentity(DID_DOMAIN_NAME); + + // Assert + relationshipTemplate.ForIdentity.Should().Be(IdentityAddress.GetAnonymized(DID_DOMAIN_NAME)); + } + + [Fact] + public void Non_personalized_template_can_not_be_anonymized() + { + // Arrange + var creatorIdentityAddress = CreateRandomIdentityAddress(); + var deviceId = CreateRandomDeviceId(); + byte[] content = [1, 1, 1, 1, 1, 1, 1, 1]; + var relationshipTemplate = new RelationshipTemplate(creatorIdentityAddress, deviceId, null, _dateTimeNow, content); + + // Act + var acting = () => relationshipTemplate.AnonymizeForIdentity(DID_DOMAIN_NAME); + + // Assert + acting.Should().Throw().WithError("error.platform.validation.relationship.relationshipTemplateNotPersonalized"); + } +} From af7ab12a5392da6c707006f1654309777300ec3c Mon Sep 17 00:00:00 2001 From: Mika Herrmann Date: Thu, 5 Dec 2024 13:54:10 +0100 Subject: [PATCH 08/16] chore: Make loop more readable --- .../AnonymizeRelationshipTemplatesForIdentity/Handler.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/Handler.cs b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/Handler.cs index 429fc18f77..282c568cc4 100644 --- a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/Handler.cs +++ b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/Handler.cs @@ -21,7 +21,8 @@ public async Task Handle(AnonymizeRelationshipTemplatesForIdentityCommand reques { var relationshipTemplates = (await _relationshipTemplatesRepository.FindTemplates(RelationshipTemplate.IsFor(IdentityAddress.Parse(request.IdentityAddress)), cancellationToken)).ToList(); - foreach (var relationshipTemplate in relationshipTemplates) relationshipTemplate.AnonymizeForIdentity(_applicationOptions.DidDomainName); + foreach (var relationshipTemplate in relationshipTemplates) + relationshipTemplate.AnonymizeForIdentity(_applicationOptions.DidDomainName); await _relationshipTemplatesRepository.Update(relationshipTemplates, cancellationToken); } From 25049cc6feb7ea25adf8aa93179eace508f4f62a Mon Sep 17 00:00:00 2001 From: Mika Herrmann Date: Thu, 5 Dec 2024 14:00:03 +0100 Subject: [PATCH 09/16] feat: Add anonymization logic to Token class --- .../Tokens/src/Tokens.Domain/DomainErrors.cs | 12 +++++++++++ .../src/Tokens.Domain/Entities/Token.cs | 21 ++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 Modules/Tokens/src/Tokens.Domain/DomainErrors.cs diff --git a/Modules/Tokens/src/Tokens.Domain/DomainErrors.cs b/Modules/Tokens/src/Tokens.Domain/DomainErrors.cs new file mode 100644 index 0000000000..3498ca67bf --- /dev/null +++ b/Modules/Tokens/src/Tokens.Domain/DomainErrors.cs @@ -0,0 +1,12 @@ +using Backbone.BuildingBlocks.Domain.Errors; + +namespace Backbone.Modules.Tokens.Domain; + +public class DomainErrors +{ + public static DomainError TokenNotPersonalized() + { + return new DomainError("error.platform.validation.token.tokenNotPersonalized", + "The token has to be personalized."); + } +} diff --git a/Modules/Tokens/src/Tokens.Domain/Entities/Token.cs b/Modules/Tokens/src/Tokens.Domain/Entities/Token.cs index dc63d63e34..b347d57fc4 100644 --- a/Modules/Tokens/src/Tokens.Domain/Entities/Token.cs +++ b/Modules/Tokens/src/Tokens.Domain/Entities/Token.cs @@ -43,7 +43,7 @@ public Token(IdentityAddress createdBy, DeviceId createdByDevice, byte[] content public IdentityAddress CreatedBy { get; set; } public DeviceId CreatedByDevice { get; set; } - public IdentityAddress? ForIdentity { get; set; } + public IdentityAddress? ForIdentity { get; private set; } public byte[]? Password { get; set; } public byte[] Content { get; private set; } @@ -58,11 +58,25 @@ public bool CanBeCollectedUsingPassword(IdentityAddress? address, byte[]? passwo CreatedBy == address; // The owner shouldn't need a password to get the template } + public void AnonymizeForIdentity(string didDomainName) + { + EnsureIsPersonalized(); + + var anonymousIdentity = IdentityAddress.GetAnonymized(didDomainName); + + ForIdentity = anonymousIdentity; + } + public void EnsureCanBeDeletedBy(IdentityAddress identityAddress) { if (CreatedBy != identityAddress) throw new DomainActionForbiddenException(); } + public void EnsureIsPersonalized() + { + if (ForIdentity == null) throw new DomainException(DomainErrors.TokenNotPersonalized()); + } + #region Expressions public static Expression> IsNotExpired => @@ -91,5 +105,10 @@ public static Expression> CanBeCollectedWithPassword(IdentityA token.CreatedBy == address; // The owner shouldn't need a password to get the template } + public static Expression> IsFor(IdentityAddress identityAddress) + { + return token => token.ForIdentity == identityAddress; + } + #endregion } From a26641773f22f8ba5ee8d613d74d9ae5891fd762 Mon Sep 17 00:00:00 2001 From: Mika Herrmann Date: Thu, 5 Dec 2024 14:00:53 +0100 Subject: [PATCH 10/16] feat: Add didDomainName to Token application options --- Modules/Tokens/src/Tokens.Application/ApplicationOptions.cs | 5 +++++ appsettings.override.json | 3 +++ 2 files changed, 8 insertions(+) diff --git a/Modules/Tokens/src/Tokens.Application/ApplicationOptions.cs b/Modules/Tokens/src/Tokens.Application/ApplicationOptions.cs index 7137094dd6..7dd53fb8c2 100644 --- a/Modules/Tokens/src/Tokens.Application/ApplicationOptions.cs +++ b/Modules/Tokens/src/Tokens.Application/ApplicationOptions.cs @@ -6,6 +6,11 @@ public class ApplicationOptions { [Required] public PaginationOptions Pagination { get; set; } = new(); + + [Required] + [MinLength(3)] + [MaxLength(45)] + public string DidDomainName { get; set; } = null!; } public class PaginationOptions diff --git a/appsettings.override.json b/appsettings.override.json index c04b9bcf51..58436ad64b 100644 --- a/appsettings.override.json +++ b/appsettings.override.json @@ -233,6 +233,9 @@ } }, "Tokens": { + "Application": { + "DidDomainName": "localhost" + }, "Infrastructure": { "SqlDatabase": { "Provider": "Postgres", From 14c148b90007b7ac9099153a13e34eba0a7b0160 Mon Sep 17 00:00:00 2001 From: Mika Herrmann Date: Thu, 5 Dec 2024 14:01:56 +0100 Subject: [PATCH 11/16] feat: Add repository methods for querying and updating tokens --- .../Persistence/Repository/ITokensRepository.cs | 3 +++ .../Persistence/Repository/TokensRepository.cs | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/Modules/Tokens/src/Tokens.Application/Infrastructure/Persistence/Repository/ITokensRepository.cs b/Modules/Tokens/src/Tokens.Application/Infrastructure/Persistence/Repository/ITokensRepository.cs index 57ee498cfc..60beb94dfa 100644 --- a/Modules/Tokens/src/Tokens.Application/Infrastructure/Persistence/Repository/ITokensRepository.cs +++ b/Modules/Tokens/src/Tokens.Application/Infrastructure/Persistence/Repository/ITokensRepository.cs @@ -14,7 +14,10 @@ public interface ITokensRepository Task> FindTokens(IEnumerable queryItems, IdentityAddress activeIdentity, PaginationFilter paginationFilter, CancellationToken cancellationToken, bool track = false); + Task> FindTokens(Expression> filter, CancellationToken cancellationToken, bool track = false); + Task Find(TokenId tokenId, IdentityAddress? activeIdentity, CancellationToken cancellationToken, bool track = false); + Task Update(IEnumerable tokens, CancellationToken cancellationToken); Task DeleteTokens(Expression> filter, CancellationToken cancellationToken); Task DeleteToken(Token token, CancellationToken cancellationToken); } diff --git a/Modules/Tokens/src/Tokens.Infrastructure/Persistence/Repository/TokensRepository.cs b/Modules/Tokens/src/Tokens.Infrastructure/Persistence/Repository/TokensRepository.cs index 3bb69076b3..5a6ee192c3 100644 --- a/Modules/Tokens/src/Tokens.Infrastructure/Persistence/Repository/TokensRepository.cs +++ b/Modules/Tokens/src/Tokens.Infrastructure/Persistence/Repository/TokensRepository.cs @@ -48,6 +48,13 @@ public async Task> FindTokens(IEnumerable> FindTokens(Expression> filter, CancellationToken cancellationToken, bool track = false) + { + return await (track ? _tokensDbSet : _readonlyTokensDbSet) + .Where(filter) + .ToListAsync(cancellationToken); + } + public async Task Find(TokenId id, IdentityAddress? activeIdentity, CancellationToken cancellationToken, bool track = false) { var token = await _readonlyTokensDbSet @@ -86,6 +93,12 @@ public async Task Add(Token token) await _dbContext.SaveChangesAsync(); } + public async Task Update(IEnumerable tokens, CancellationToken cancellationToken) + { + _tokensDbSet.UpdateRange(tokens); + await _dbContext.SaveChangesAsync(cancellationToken); + } + public async Task DeleteTokens(Expression> filter, CancellationToken cancellationToken) { await _tokensDbSet.Where(filter).ExecuteDeleteAsync(cancellationToken); From e6d27a9a148c1f6858640fdf3bdc182e3fc17b1f Mon Sep 17 00:00:00 2001 From: Mika Herrmann Date: Thu, 5 Dec 2024 14:02:36 +0100 Subject: [PATCH 12/16] feat: Add command, handler and validator for anonymizing all tokens for an identity --- .../AnonymizeTokensForIdentityCommand.cs | 13 +++++++++ .../AnonymizeTokensForIdentity/Handler.cs | 29 +++++++++++++++++++ .../AnonymizeTokensForIdentity/Validator.cs | 14 +++++++++ 3 files changed, 56 insertions(+) create mode 100644 Modules/Tokens/src/Tokens.Application/Tokens/Commands/AnonymizeTokensForIdentity/AnonymizeTokensForIdentityCommand.cs create mode 100644 Modules/Tokens/src/Tokens.Application/Tokens/Commands/AnonymizeTokensForIdentity/Handler.cs create mode 100644 Modules/Tokens/src/Tokens.Application/Tokens/Commands/AnonymizeTokensForIdentity/Validator.cs diff --git a/Modules/Tokens/src/Tokens.Application/Tokens/Commands/AnonymizeTokensForIdentity/AnonymizeTokensForIdentityCommand.cs b/Modules/Tokens/src/Tokens.Application/Tokens/Commands/AnonymizeTokensForIdentity/AnonymizeTokensForIdentityCommand.cs new file mode 100644 index 0000000000..ed7cb1109c --- /dev/null +++ b/Modules/Tokens/src/Tokens.Application/Tokens/Commands/AnonymizeTokensForIdentity/AnonymizeTokensForIdentityCommand.cs @@ -0,0 +1,13 @@ +using MediatR; + +namespace Backbone.Modules.Tokens.Application.Tokens.Commands.AnonymizeTokensForIdentity; + +public class AnonymizeTokensForIdentityCommand : IRequest +{ + public AnonymizeTokensForIdentityCommand(string identityAddress) + { + IdentityAddress = identityAddress; + } + + public string IdentityAddress { get; set; } +} diff --git a/Modules/Tokens/src/Tokens.Application/Tokens/Commands/AnonymizeTokensForIdentity/Handler.cs b/Modules/Tokens/src/Tokens.Application/Tokens/Commands/AnonymizeTokensForIdentity/Handler.cs new file mode 100644 index 0000000000..6cb30e44b2 --- /dev/null +++ b/Modules/Tokens/src/Tokens.Application/Tokens/Commands/AnonymizeTokensForIdentity/Handler.cs @@ -0,0 +1,29 @@ +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Tokens.Application.Infrastructure.Persistence.Repository; +using Backbone.Modules.Tokens.Domain.Entities; +using MediatR; +using Microsoft.Extensions.Options; + +namespace Backbone.Modules.Tokens.Application.Tokens.Commands.AnonymizeTokensForIdentity; + +public class Handler : IRequestHandler +{ + private readonly ITokensRepository _tokensRepository; + private readonly ApplicationOptions _applicationOptions; + + public Handler(ITokensRepository tokensRepository, IOptions applicationOptions) + { + _tokensRepository = tokensRepository; + _applicationOptions = applicationOptions.Value; + } + + public async Task Handle(AnonymizeTokensForIdentityCommand request, CancellationToken cancellationToken) + { + var tokens = (await _tokensRepository.FindTokens(Token.IsFor(IdentityAddress.Parse(request.IdentityAddress)), cancellationToken)).ToList(); + + foreach (var token in tokens) + token.AnonymizeForIdentity(_applicationOptions.DidDomainName); + + await _tokensRepository.Update(tokens, cancellationToken); + } +} diff --git a/Modules/Tokens/src/Tokens.Application/Tokens/Commands/AnonymizeTokensForIdentity/Validator.cs b/Modules/Tokens/src/Tokens.Application/Tokens/Commands/AnonymizeTokensForIdentity/Validator.cs new file mode 100644 index 0000000000..f3908cc542 --- /dev/null +++ b/Modules/Tokens/src/Tokens.Application/Tokens/Commands/AnonymizeTokensForIdentity/Validator.cs @@ -0,0 +1,14 @@ +using Backbone.BuildingBlocks.Application.Extensions; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using FluentValidation; + +namespace Backbone.Modules.Tokens.Application.Tokens.Commands.AnonymizeTokensForIdentity; + +public class Validator : AbstractValidator +{ + public Validator() + { + RuleFor(c => c.IdentityAddress) + .ValidId(); + } +} From c2be360433c675bdd6993fa6d29f1773f82081aa Mon Sep 17 00:00:00 2001 From: Mika Herrmann Date: Thu, 5 Dec 2024 14:03:07 +0100 Subject: [PATCH 13/16] feat: Add anonymization command to token identity deleter --- .../Tokens/src/Tokens.Application/Identities/IdentityDeleter.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Modules/Tokens/src/Tokens.Application/Identities/IdentityDeleter.cs b/Modules/Tokens/src/Tokens.Application/Identities/IdentityDeleter.cs index 895709831b..836bd57dff 100644 --- a/Modules/Tokens/src/Tokens.Application/Identities/IdentityDeleter.cs +++ b/Modules/Tokens/src/Tokens.Application/Identities/IdentityDeleter.cs @@ -1,5 +1,6 @@ using Backbone.BuildingBlocks.Application.Identities; using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Tokens.Application.Tokens.Commands.AnonymizeTokensForIdentity; using Backbone.Modules.Tokens.Application.Tokens.Commands.DeleteTokensOfIdentity; using MediatR; @@ -19,6 +20,7 @@ public IdentityDeleter(IMediator mediator, IDeletionProcessLogger deletionProces public async Task Delete(IdentityAddress identityAddress) { await _mediator.Send(new DeleteTokensOfIdentityCommand(identityAddress)); + await _mediator.Send(new AnonymizeTokensForIdentityCommand(identityAddress)); await _deletionProcessLogger.LogDeletion(identityAddress, "Tokens"); } } From 65ced9026915f73d60e839882740870bce9c53a6 Mon Sep 17 00:00:00 2001 From: Mika Herrmann Date: Thu, 5 Dec 2024 14:03:54 +0100 Subject: [PATCH 14/16] feat: Add unit tests for anonymizing tokens --- .../Tests/Token.AnonymizeForIdentityTests.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 Modules/Tokens/test/Tokens.Domain.Tests/Tests/Token.AnonymizeForIdentityTests.cs diff --git a/Modules/Tokens/test/Tokens.Domain.Tests/Tests/Token.AnonymizeForIdentityTests.cs b/Modules/Tokens/test/Tokens.Domain.Tests/Tests/Token.AnonymizeForIdentityTests.cs new file mode 100644 index 0000000000..6159567c0c --- /dev/null +++ b/Modules/Tokens/test/Tokens.Domain.Tests/Tests/Token.AnonymizeForIdentityTests.cs @@ -0,0 +1,46 @@ +using Backbone.BuildingBlocks.Domain.Exceptions; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Tokens.Domain.Entities; +using Backbone.UnitTestTools.Extensions; + +namespace Backbone.Modules.Tokens.Domain.Tests.Tests; + +public class TokenAnonymizeForIdentityTests : AbstractTestsBase +{ + private const string DID_DOMAIN_NAME = "localhost"; + + [Fact] + public void Personalized_token_can_be_anonymized() + { + // Arrange + var creatorIdentityAddress = CreateRandomIdentityAddress(); + var forIdentityAddress = CreateRandomIdentityAddress(); + var deviceId = CreateRandomDeviceId(); + byte[] content = [1, 1, 1, 1, 1, 1, 1, 1]; + var expiresAt = _dateTimeTomorrow; + var relationshipTemplate = new Token(creatorIdentityAddress, deviceId, content, expiresAt, forIdentityAddress); + + // Act + relationshipTemplate.AnonymizeForIdentity(DID_DOMAIN_NAME); + + // Assert + relationshipTemplate.ForIdentity.Should().Be(IdentityAddress.GetAnonymized(DID_DOMAIN_NAME)); + } + + [Fact] + public void Non_personalized_token_can_not_be_anonymized() + { + // Arrange + var creatorIdentityAddress = CreateRandomIdentityAddress(); + var deviceId = CreateRandomDeviceId(); + byte[] content = [1, 1, 1, 1, 1, 1, 1, 1]; + var expiresAt = _dateTimeTomorrow; + var relationshipTemplate = new Token(creatorIdentityAddress, deviceId, content, expiresAt); + + // Act + var acting = () => relationshipTemplate.AnonymizeForIdentity(DID_DOMAIN_NAME); + + // Assert + acting.Should().Throw().WithError("error.platform.validation.token.tokenNotPersonalized"); + } +} From 9733b17d2f03e5fed24ec5daf8834f09807ca540 Mon Sep 17 00:00:00 2001 From: Mika Herrmann Date: Thu, 5 Dec 2024 14:04:34 +0100 Subject: [PATCH 15/16] chore: Add forIdentity column to bruno POST /Tokens request --- Applications/ConsumerApi/src/http/Tokens/Create Token.bru | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Applications/ConsumerApi/src/http/Tokens/Create Token.bru b/Applications/ConsumerApi/src/http/Tokens/Create Token.bru index d824ae2b52..87ef189443 100644 --- a/Applications/ConsumerApi/src/http/Tokens/Create Token.bru +++ b/Applications/ConsumerApi/src/http/Tokens/Create Token.bru @@ -13,6 +13,7 @@ post { body:json { { "content": "AAAA", - "expiresAt": "2024-12-17" + "expiresAt": "2024-12-17", + "forIdentity": "did:e:localhost:dids:8234cca0160ff05c785636" } } From 2e7d5274c4449052c810cc004996a0579f5a962b Mon Sep 17 00:00:00 2001 From: Mika Herrmann Date: Thu, 5 Dec 2024 14:16:14 +0100 Subject: [PATCH 16/16] chore: Add didDomainName to test appsettings --- .ci/appsettings.override.postgres.docker.json | 3 +++ .ci/appsettings.override.postgres.local.json | 3 +++ .ci/appsettings.override.sqlserver.docker.json | 3 +++ .ci/appsettings.override.sqlserver.local.json | 3 +++ 4 files changed, 12 insertions(+) diff --git a/.ci/appsettings.override.postgres.docker.json b/.ci/appsettings.override.postgres.docker.json index ebe8c14a63..749cedff01 100644 --- a/.ci/appsettings.override.postgres.docker.json +++ b/.ci/appsettings.override.postgres.docker.json @@ -171,6 +171,9 @@ } }, "Tokens": { + "Application": { + "DidDomainName": "localhost" + }, "Infrastructure": { "SqlDatabase": { "Provider": "Postgres", diff --git a/.ci/appsettings.override.postgres.local.json b/.ci/appsettings.override.postgres.local.json index 084c6e544c..7e4cb7fc92 100644 --- a/.ci/appsettings.override.postgres.local.json +++ b/.ci/appsettings.override.postgres.local.json @@ -171,6 +171,9 @@ } }, "Tokens": { + "Application": { + "DidDomainName": "localhost" + }, "Infrastructure": { "SqlDatabase": { "Provider": "Postgres", diff --git a/.ci/appsettings.override.sqlserver.docker.json b/.ci/appsettings.override.sqlserver.docker.json index b7941df3e6..050034be42 100644 --- a/.ci/appsettings.override.sqlserver.docker.json +++ b/.ci/appsettings.override.sqlserver.docker.json @@ -171,6 +171,9 @@ } }, "Tokens": { + "Application": { + "DidDomainName": "localhost" + }, "Infrastructure": { "SqlDatabase": { "Provider": "SqlServer", diff --git a/.ci/appsettings.override.sqlserver.local.json b/.ci/appsettings.override.sqlserver.local.json index 0c1be1b06f..eb601335ed 100644 --- a/.ci/appsettings.override.sqlserver.local.json +++ b/.ci/appsettings.override.sqlserver.local.json @@ -171,6 +171,9 @@ } }, "Tokens": { + "Application": { + "DidDomainName": "localhost" + }, "Infrastructure": { "SqlDatabase": { "Provider": "SqlServer",