Skip to content

Commit

Permalink
Possibility for a device to find out whether its identity was deleted (
Browse files Browse the repository at this point in the history
…#949)

* refactor: optimize SeedTestUsers

* test: add integration test

* refactor: use generic Find method for audit log entries

* feat: add DeletionCompleted audit log entry and associate usernames after finished deletion

* feat: add migrations

* feat: add possibility to query whether an identity was deleted

* chore: formatting

* refactor: simplify Handler for SeedTestUsersCommand

* refactor: use NameValueCollection instead of Dictionary<string, string>

* refactor: rename BelongsTo method

* chore: fix archunit test errors

* chore: formatting

* test: fix test by configuring fake

* refactor: cleanup HandleCompletedDeletionProcessCommand Handler

* feat: add validator for HandleCompletedDeletionProcessCommand

* chore: formatting/cleanup

* refactor: improve AssociateUsernames method

* refactor: extract variable

* test: change test names

* chore: remove leftover comment

* test: make AssociateUsernames test more concrete

* refactor: use init property instead of constructor

* feat: remove migration

* refactor: use List<string> instead of string as type for UsernameHashesBase64 column

* test: test for length of UsernameHashesBase64

* test: check what happens if we remove all calls to Hasher.SetHasher

* test: check what happens if we remove all calls to Hasher.SetHasher

* feat: IsDeleted returns true if grace period is over but identity is not deleted yet

* Revert "test: check what happens if we remove all calls to Hasher.SetHasher"

This reverts commit d3e03f9.

* test: reset hasher after the tests have finished

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Mika Herrmann <[email protected]>
  • Loading branch information
3 people authored Dec 2, 2024
1 parent dcec3ea commit 1035c98
Show file tree
Hide file tree
Showing 41 changed files with 2,473 additions and 74 deletions.
8 changes: 2 additions & 6 deletions Applications/ConsumerApi/src/DevicesDbContextSeeder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using Backbone.Modules.Devices.Domain.Aggregates.Tier;
using Backbone.Modules.Devices.Infrastructure.Persistence.Database;
using MediatR;
using Microsoft.EntityFrameworkCore;

namespace Backbone.ConsumerApi;

Expand All @@ -28,19 +27,16 @@ private async Task SeedEverything(DevicesDbContext context)
{
await SeedBasicTier(context);
await SeedQueuedForDeletionTier();
await SeedApplicationUsers(context);
await SeedApplicationUsers();
}

private static async Task<Tier?> GetBasicTier(DevicesDbContext context)
{
return await context.Tiers.GetBasicTier(CancellationToken.None);
}

private async Task SeedApplicationUsers(DevicesDbContext context)
private async Task SeedApplicationUsers()
{
if (await context.Users.AnyAsync())
return;

await _mediator.Send(new SeedTestUsersCommand());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Feature: POST /Challenges
User creates a Challenge

Scenario: Creating a Challenge as an anonymous user
When an anonymous user sends a POST request is sent to the /Challenges endpoint
When an anonymous user sends a POST request to the /Challenges endpoint
Then the response status code is 201 (Created)
And the response contains a Challenge
And the Challenge has an expiration date in the future
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@Integration
Feature: GET /Identities/IsDeleted

User wants to know whether its identity was deleted

Scenario: Asking whether a not-yet-deleted identity was deleted
Given an Identity i with a Device d
And an active deletion process for i exists
When an anonymous user sends a GET request to the /Identities/IsDeleted endpoint with d.Username
Then the response status code is 200 (OK)
And the response says that the identity was not deleted
And the deletion date is not set
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public async Task GivenAChallengeCreatedByAnAnonymousUser(string challengeName)

#region When

[When("an anonymous user sends a POST request is sent to the /Challenges endpoint")]
[When("an anonymous user sends a POST request to the /Challenges endpoint")]
public async Task WhenAnAnonymousUserSendsAPostRequestIsSentToTheChallengesEndpoint()
{
var client = _clientPool.Anonymous;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using Backbone.BuildingBlocks.SDK.Crypto;
using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types;
using Backbone.ConsumerApi.Sdk;
using Backbone.ConsumerApi.Sdk.Authentication;
using Backbone.ConsumerApi.Sdk.Endpoints.Devices.Types;
using Backbone.ConsumerApi.Sdk.Endpoints.Identities.Types.Requests;
using Backbone.ConsumerApi.Sdk.Endpoints.Identities.Types.Responses;
using Backbone.ConsumerApi.Tests.Integration.Configuration;
using Backbone.ConsumerApi.Tests.Integration.Contexts;
using Backbone.ConsumerApi.Tests.Integration.Helpers;
Expand All @@ -27,6 +29,8 @@ internal class IdentitiesStepDefinitions
private readonly ChallengesContext _challengesContext;
private readonly ClientPool _clientPool;

private ApiResponse<IsDeletedResponse>? _isDeletedResponse;

public IdentitiesStepDefinitions(ResponseContext responseContext, ChallengesContext challengesContext, ClientPool clientPool, HttpClientFactory factory,
IOptions<HttpConfiguration> httpConfiguration)
{
Expand Down Expand Up @@ -91,5 +95,31 @@ public async Task WhenAPostRequestIsSentToTheIdentitiesEndpointWithAValidSignatu
_responseContext.WhenResponse = await _clientPool.Anonymous.Identities.CreateIdentity(createIdentityPayload);
}

[When($@"an anonymous user sends a GET request to the /Identities/IsDeleted endpoint with {RegexFor.SINGLE_THING}.Username")]
public async Task WhenAnAnonymousUserSendsAGETRequestToTheIdentitiesIsDeletedEndpointWithDUsername(string deviceName)
{
var client = _clientPool.GetForDeviceName(deviceName);

var device = await client.Devices.GetActiveDevice();

_responseContext.WhenResponse = _isDeletedResponse = await _clientPool.Anonymous.Identities.IsDeleted(device.Result!.Username);
}

#endregion

#region Then

[Then(@"the response says that the identity was not deleted")]
public void ThenTheResponseSaysThatTheIdentityWasNotDeleted()
{
_isDeletedResponse!.Result!.IsDeleted.Should().BeFalse();
}

[Then(@"the deletion date is not set")]
public void ThenTheDeletionDateIsNotSet()
{
_isDeletedResponse!.Result!.DeletionDate.Should().BeNull();
}

#endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
using Backbone.BuildingBlocks.Application.PushNotifications;
using Backbone.BuildingBlocks.Domain.Errors;
using Backbone.DevelopmentKit.Identity.ValueObjects;
using Backbone.Modules.Devices.Application.Identities.Commands.HandleCompletedDeletionProcess;
using Backbone.Modules.Devices.Application.Identities.Commands.TriggerRipeDeletionProcesses;
using Backbone.Modules.Devices.Application.Identities.Queries.GetIdentity;
using Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess;
using CSharpFunctionalExtensions;
using MediatR;
Expand Down Expand Up @@ -78,10 +80,16 @@ await _pushNotificationSender.SendNotification(

private async Task Delete(IdentityAddress identityAddress)
{
var identity = await _mediator.Send(new GetIdentityQuery(identityAddress.Value));

foreach (var identityDeleter in _identityDeleters)
{
await identityDeleter.Delete(identityAddress);
}

var usernames = identity.Devices.Select(d => d.Username);

await _mediator.Send(new HandleCompletedDeletionProcessCommand(identityAddress.Value, usernames));
}

private void LogErroringDeletionTriggers(IEnumerable<KeyValuePair<IdentityAddress, UnitResult<DomainError>>> erroringDeletionTriggers)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
using Backbone.DevelopmentKit.Identity.ValueObjects;
using Backbone.Job.IdentityDeletion.Workers;
using Backbone.Modules.Devices.Application.Identities.Commands.TriggerRipeDeletionProcesses;
using Backbone.Modules.Devices.Application.Identities.Queries.GetIdentity;
using Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess;
using Backbone.Modules.Devices.Domain.Aggregates.Tier;
using Backbone.Modules.Devices.Domain.Entities.Identities;
using Backbone.Modules.Relationships.Application.Relationships.Queries.FindRelationshipsOfIdentity;
using CSharpFunctionalExtensions;
using FakeItEasy;
Expand Down Expand Up @@ -37,43 +40,54 @@ public async Task Calls_Deleters_For_Each_Identity()
{
// Arrange
var fakeMediator = A.Fake<IMediator>();
var identityAddress1 = CreateRandomIdentityAddress();
var identityAddress2 = CreateRandomIdentityAddress();
SetupRipeDeletionProcessesCommand(fakeMediator, identityAddress1, identityAddress2);
var identity1 = CreateIdentity();
var identity2 = CreateIdentity();
SetupRipeDeletionProcessesCommand(fakeMediator, identity1.Address, identity2.Address);

var mockIdentityDeleter = A.Fake<IIdentityDeleter>();
var worker = CreateWorker(fakeMediator, [mockIdentityDeleter]);

A.CallTo(() => fakeMediator.Send(A<FindRelationshipsOfIdentityQuery>._, A<CancellationToken>._))
.Returns(new FindRelationshipsOfIdentityResponse([]));

A.CallTo(() => fakeMediator.Send(A<GetIdentityQuery>.That.Matches(q => q.Address == identity1.Address.Value), A<CancellationToken>._))
.Returns(new GetIdentityResponse(identity1));

A.CallTo(() => fakeMediator.Send(A<GetIdentityQuery>.That.Matches(q => q.Address == identity2.Address.Value), A<CancellationToken>._))
.Returns(new GetIdentityResponse(identity2));

// Act
await worker.StartProcessing(CancellationToken.None);

// Assert
A.CallTo(() => mockIdentityDeleter.Delete(identityAddress1)).MustHaveHappenedOnceExactly();
A.CallTo(() => mockIdentityDeleter.Delete(identityAddress2)).MustHaveHappenedOnceExactly();
A.CallTo(() => mockIdentityDeleter.Delete(identity1.Address)).MustHaveHappenedOnceExactly();
A.CallTo(() => mockIdentityDeleter.Delete(identity2.Address)).MustHaveHappenedOnceExactly();
}

[Fact]
public async Task Sends_push_notification_to_each_deleted_identity()
{
// Arrange
var fakeMediator = A.Fake<IMediator>();
var identityAddress1 = CreateRandomIdentityAddress();
var identityAddress2 = CreateRandomIdentityAddress();
var identityAddress3 = CreateRandomIdentityAddress();
SetupRipeDeletionProcessesCommand(fakeMediator, identityAddress1, identityAddress2, identityAddress3);
var identity1 = CreateIdentity();
var identity2 = CreateIdentity();
SetupRipeDeletionProcessesCommand(fakeMediator, identity1.Address, identity2.Address);
A.CallTo(() => fakeMediator.Send(A<FindRelationshipsOfIdentityQuery>._, A<CancellationToken>._)).Returns(new FindRelationshipsOfIdentityResponse([]));

A.CallTo(() => fakeMediator.Send(A<GetIdentityQuery>.That.Matches(q => q.Address == identity1.Address.Value), A<CancellationToken>._))
.Returns(new GetIdentityResponse(identity1));

A.CallTo(() => fakeMediator.Send(A<GetIdentityQuery>.That.Matches(q => q.Address == identity2.Address.Value), A<CancellationToken>._))
.Returns(new GetIdentityResponse(identity2));

var mockPushNotificationSender = A.Fake<IPushNotificationSender>();
var worker = CreateWorker(fakeMediator, [], mockPushNotificationSender);

// Act
await worker.StartProcessing(CancellationToken.None);

// Assert
foreach (var identityAddress in new[] { identityAddress1, identityAddress2, identityAddress3 })
foreach (var identityAddress in new[] { identity1.Address, identity2.Address })
{
A.CallTo(() => mockPushNotificationSender.SendNotification(
A<DeletionStartsPushNotification>._,
Expand All @@ -99,4 +113,15 @@ private static ActualDeletionWorker CreateWorker(IMediator mediator,
var logger = A.Dummy<ILogger<ActualDeletionWorker>>();
return new ActualDeletionWorker(hostApplicationLifetime, identityDeleters, mediator, pushNotificationSender, logger);
}

private static Identity CreateIdentity()
{
return new Identity(
CreateRandomDeviceId(),
CreateRandomIdentityAddress(),
CreateRandomBytes(),
TierId.Generate(),
1,
CommunicationLanguage.DEFAULT_LANGUAGE);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,20 @@ public async Task<ApiResponse<T>> PostUnauthenticated<T>(string url, object? req
.Execute();
}

public async Task<ApiResponse<T>> Get<T>(string url, object? requestContent = null, PaginationFilter? pagination = null)
public async Task<ApiResponse<T>> Get<T>(string url, NameValueCollection? queryParameters = null, PaginationFilter? pagination = null)
{
return await Request<T>(HttpMethod.Get, url)
.Authenticate()
.WithPagination(pagination)
.WithJson(requestContent)
.AddQueryParameters(queryParameters)
.Execute();
}

public async Task<ApiResponse<T>> GetUnauthenticated<T>(string url, object? requestContent = null, PaginationFilter? pagination = null)
public async Task<ApiResponse<T>> GetUnauthenticated<T>(string url, NameValueCollection? queryParameters = null, PaginationFilter? pagination = null)
{
return await Request<T>(HttpMethod.Get, url)
.WithPagination(pagination)
.WithJson(requestContent)
.AddQueryParameters(queryParameters)
.Execute();
}

Expand Down Expand Up @@ -201,7 +201,9 @@ public RequestBuilder<T> WithMultipartForm(MultipartContent content)

public RequestBuilder<T> WithPagination(PaginationFilter? pagination)
{
if (pagination != null) AddQueryParameters(pagination);
if (pagination != null)
AddQueryParameters(pagination);

return this;
}

Expand Down Expand Up @@ -233,7 +235,14 @@ public RequestBuilder<T> AddQueryParameter(string key, object value)

public RequestBuilder<T> AddQueryParameters(IQueryParameterStorage parameters)
{
_queryParameters.Add(parameters.ToQueryParameters());
return AddQueryParameters(parameters.ToQueryParameters());
}

public RequestBuilder<T> AddQueryParameters(NameValueCollection? parameters)
{
if (parameters != null)
_queryParameters.Add(parameters);

return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public IdentityDeletionProcessAuditLogEntryDTO(IdentityDeletionProcessAuditLogEn
public string Id { get; set; }
public DateTime CreatedAt { get; set; }
public DeletionProcessStatus? OldStatus { get; set; }
public DeletionProcessStatus NewStatus { get; set; }
public DeletionProcessStatus? NewStatus { get; set; }
public Dictionary<string, string> AdditionalData { get; set; }
public MessageKey MessageKey { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using MediatR;

namespace Backbone.Modules.Devices.Application.Identities.Commands.HandleCompletedDeletionProcess;

public class HandleCompletedDeletionProcessCommand : IRequest
{
public HandleCompletedDeletionProcessCommand(string identityAddress, IEnumerable<string> usernames)
{
IdentityAddress = identityAddress;
Usernames = usernames;
}

public string IdentityAddress { get; }
public IEnumerable<string> Usernames { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Backbone.DevelopmentKit.Identity.ValueObjects;
using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository;
using Backbone.Modules.Devices.Domain.Entities.Identities;
using MediatR;

namespace Backbone.Modules.Devices.Application.Identities.Commands.HandleCompletedDeletionProcess;

public class Handler : IRequestHandler<HandleCompletedDeletionProcessCommand>
{
private readonly IIdentitiesRepository _identitiesRepository;

public Handler(IIdentitiesRepository identitiesRepository)
{
_identitiesRepository = identitiesRepository;
}

public async Task Handle(HandleCompletedDeletionProcessCommand request, CancellationToken cancellationToken)
{
await _identitiesRepository.AddDeletionProcessAuditLogEntry(IdentityDeletionProcessAuditLogEntry.DeletionCompleted(request.IdentityAddress));

await AssociateUsernames(request, cancellationToken);
}

private async Task AssociateUsernames(HandleCompletedDeletionProcessCommand request, CancellationToken cancellationToken)
{
var identityAddressHash = Hasher.HashUtf8(request.IdentityAddress);

var auditLogEntries = await _identitiesRepository.GetIdentityDeletionProcessAuditLogs(l => l.IdentityAddressHash == identityAddressHash, CancellationToken.None, track: true);

var auditLogEntriesArray = auditLogEntries.ToArray();

foreach (var auditLogEntry in auditLogEntriesArray)
{
auditLogEntry.AssociateUsernames(request.Usernames.Select(Username.Parse));
}

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

namespace Backbone.Modules.Devices.Application.Identities.Commands.HandleCompletedDeletionProcess;

public class Validator : AbstractValidator<HandleCompletedDeletionProcessCommand>
{
public Validator()
{
RuleFor(c => c.IdentityAddress).ValidId<HandleCompletedDeletionProcessCommand, IdentityAddress>();
RuleFor(c => c.Usernames).DetailedNotEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ public Handler(IIdentitiesRepository identityRepository)

public async Task<GetDeletionProcessesAuditLogsResponse> Handle(GetDeletionProcessesAuditLogsQuery request, CancellationToken cancellationToken)
{
var identityDeletionProcessAuditLogEntries = await _identityRepository.GetIdentityDeletionProcessAuditLogsByAddress(Hasher.HashUtf8(request.IdentityAddress), cancellationToken);
var addressHash = Hasher.HashUtf8(request.IdentityAddress);

var identityDeletionProcessAuditLogEntries = await _identityRepository.GetIdentityDeletionProcessAuditLogs(l => l.IdentityAddressHash == addressHash, cancellationToken);

return new GetDeletionProcessesAuditLogsResponse(identityDeletionProcessAuditLogEntries.OrderBy(e => e.CreatedAt));
}
}
Loading

0 comments on commit 1035c98

Please sign in to comment.