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

Possibility for a device to find out whether its identity was deleted #949

Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
08671e3
refactor: optimize SeedTestUsers
tnotheis Nov 26, 2024
268c98d
test: add integration test
tnotheis Nov 26, 2024
daf9de6
refactor: use generic Find method for audit log entries
tnotheis Nov 26, 2024
09991db
feat: add DeletionCompleted audit log entry and associate usernames a…
tnotheis Nov 26, 2024
14ee5c3
feat: add migrations
tnotheis Nov 26, 2024
3fd84b1
feat: add possibility to query whether an identity was deleted
tnotheis Nov 26, 2024
52c5b43
Merge branch 'main' into possibility-for-a-device-to-find-out-whether…
mergify[bot] Nov 26, 2024
ec2e8fd
chore: formatting
tnotheis Nov 26, 2024
f65d706
refactor: simplify Handler for SeedTestUsersCommand
tnotheis Nov 26, 2024
7e1c5eb
refactor: use NameValueCollection instead of Dictionary<string, string>
tnotheis Nov 26, 2024
0de5805
refactor: rename BelongsTo method
tnotheis Nov 26, 2024
36dab57
chore: fix archunit test errors
tnotheis Nov 26, 2024
270beb4
chore: formatting
tnotheis Nov 26, 2024
a61e31f
test: fix test by configuring fake
tnotheis Nov 26, 2024
cfe0350
refactor: cleanup HandleCompletedDeletionProcessCommand Handler
tnotheis Nov 26, 2024
fe771ac
feat: add validator for HandleCompletedDeletionProcessCommand
tnotheis Nov 26, 2024
0265c72
chore: formatting/cleanup
tnotheis Nov 26, 2024
48ab1fe
refactor: improve AssociateUsernames method
tnotheis Nov 26, 2024
3fa3812
refactor: extract variable
tnotheis Nov 26, 2024
939595c
test: change test names
tnotheis Nov 26, 2024
6f900b3
chore: remove leftover comment
tnotheis Nov 26, 2024
f56dbfc
test: make AssociateUsernames test more concrete
tnotheis Nov 26, 2024
f1ffc4d
Merge branch 'main' into possibility-for-a-device-to-find-out-whether…
mergify[bot] Nov 26, 2024
9ba8a58
Merge branch 'main' into possibility-for-a-device-to-find-out-whether…
mergify[bot] Nov 28, 2024
3517a20
refactor: use init property instead of constructor
tnotheis Nov 28, 2024
1180d00
feat: remove migration
tnotheis Nov 28, 2024
704c958
refactor: use List<string> instead of string as type for UsernameHash…
tnotheis Nov 28, 2024
872ee33
test: test for length of UsernameHashesBase64
tnotheis Nov 28, 2024
c968c1e
test: check what happens if we remove all calls to Hasher.SetHasher
tnotheis Nov 28, 2024
d3e03f9
test: check what happens if we remove all calls to Hasher.SetHasher
tnotheis Nov 28, 2024
8ed49dd
Merge branch 'main' into possibility-for-a-device-to-find-out-whether…
mergify[bot] Nov 29, 2024
6e13498
Merge remote-tracking branch 'origin/possibility-for-a-device-to-find…
MH321Productions Nov 29, 2024
3cf7b0c
feat: IsDeleted returns true if grace period is over but identity is …
tnotheis Dec 2, 2024
b957e40
Revert "test: check what happens if we remove all calls to Hasher.Set…
tnotheis Dec 2, 2024
c68cb16
test: reset hasher after the tests have finished
tnotheis Dec 2, 2024
7b67e8c
Merge branch 'main' into possibility-for-a-device-to-find-out-whether…
tnotheis Dec 2, 2024
c9e4b94
Merge branch 'main' into possibility-for-a-device-to-find-out-whether…
tnotheis Dec 2, 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
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 @@ -30,19 +29,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 @@ -92,4 +96,26 @@ public async Task WhenAPostRequestIsSentToTheIdentitiesEndpointWithAValidSignatu
}

#endregion

[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);
}

[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();
}
}
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, usernames));
}

private void LogErroringDeletionTriggers(IEnumerable<KeyValuePair<IdentityAddress, UnitResult<DomainError>>> erroringDeletionTriggers)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,17 @@ public async Task<ApiResponse<T>> Get<T>(string url, object? requestContent = nu
.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, Dictionary<string, string>? queryParameters = null, PaginationFilter? pagination = null)
{
return await Request<T>(HttpMethod.Get, url)
.WithPagination(pagination)
.WithJson(requestContent)
.Execute();
var queryBuilder = Request<T>(HttpMethod.Get, url)
.WithPagination(pagination);

foreach (var (key, value) in queryParameters ?? [])
{
queryBuilder.AddQueryParameter(key, value);
}

return await queryBuilder.Execute();
}

public async Task<ApiResponse<T>> Put<T>(string url, object? requestContent = null)
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,16 @@
using Backbone.DevelopmentKit.Identity.ValueObjects;
using MediatR;

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

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

public IdentityAddress IdentityAddress { get; }
public IEnumerable<string> Usernames { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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));

var identityAddressHash = Hasher.HashUtf8(IdentityAddress.ParseUnsafe(request.IdentityAddress));

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

var auditLogEntriesArray = auditLogEntries as IdentityDeletionProcessAuditLogEntry[] ?? 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
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));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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.Queries.IsIdentityOfUserDeleted;

public class Handler : IRequestHandler<IsIdentityOfUserDeletedQuery, IsIdentityOfUserDeletedResponse>
{
private readonly IIdentitiesRepository _identitiesRepository;

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

public async Task<IsIdentityOfUserDeletedResponse> Handle(IsIdentityOfUserDeletedQuery request, CancellationToken cancellationToken)
{
var auditLogEntries = await _identitiesRepository.GetIdentityDeletionProcessAuditLogs(
IdentityDeletionProcessAuditLogEntry.BelongsToUser(Username.Parse(request.Username)),
cancellationToken);

var deletionCompletedAuditLogEntry = auditLogEntries.FirstOrDefault(l => l.MessageKey == MessageKey.DeletionCompleted);

return new IsIdentityOfUserDeletedResponse(deletionCompletedAuditLogEntry != null, deletionCompletedAuditLogEntry?.CreatedAt);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using MediatR;

namespace Backbone.Modules.Devices.Application.Identities.Queries.IsIdentityOfUserDeleted;

public class IsIdentityOfUserDeletedQuery : IRequest<IsIdentityOfUserDeletedResponse>
{
public IsIdentityOfUserDeletedQuery(string username)
{
Username = username;
}
MH321Productions marked this conversation as resolved.
Show resolved Hide resolved

public string Username { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Backbone.Modules.Devices.Application.Identities.Queries.IsIdentityOfUserDeleted;

public class IsIdentityOfUserDeletedResponse
{
public IsIdentityOfUserDeletedResponse(bool isDeleted, DateTime? deletionDate)
{
IsDeleted = isDeleted;
DeletionDate = deletionDate;
}

public bool IsDeleted { get; set; }
public DateTime? DeletionDate { get; set; }
}
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.Devices.Application.Identities.Queries.IsIdentityOfUserDeleted;

public class Validator : AbstractValidator<IsIdentityOfUserDeletedQuery>
{
public Validator()
{
RuleFor(x => x.Username).ValidId<IsIdentityOfUserDeletedQuery, Username>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,12 @@ public interface IIdentitiesRepository

#region Deletion Process Audit Logs

Task<IEnumerable<IdentityDeletionProcessAuditLogEntry>> GetIdentityDeletionProcessAuditLogsByAddress(byte[] identityAddressHash, CancellationToken cancellationToken);
Task<IEnumerable<IdentityDeletionProcessAuditLogEntry>> GetIdentityDeletionProcessAuditLogs(Expression<Func<IdentityDeletionProcessAuditLogEntry, bool>> filter,
CancellationToken cancellationToken, bool track = false);

Task AddDeletionProcessAuditLogEntry(IdentityDeletionProcessAuditLogEntry auditLogEntry);

Task Update(IEnumerable<IdentityDeletionProcessAuditLogEntry> auditLogEntries, CancellationToken cancellationToken);

#endregion
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Backbone.DevelopmentKit.Identity.ValueObjects;
using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository;
using Backbone.Modules.Devices.Domain.Aggregates.Tier;
using Backbone.Modules.Devices.Domain.Entities.Identities;
using MediatR;
using Microsoft.Extensions.Options;
Expand All @@ -23,10 +24,29 @@ public async Task Handle(SeedTestUsersCommand request, CancellationToken cancell
{
var basicTier = await _tiersRepository.FindBasicTier(cancellationToken);

var identityA = Identity.CreateTestIdentity(IdentityAddress.Create([1, 1, 1, 1, 1], _applicationOptions.DidDomainName), [1, 1, 1, 1, 1], basicTier!.Id, "USRa");
var identityB = Identity.CreateTestIdentity(IdentityAddress.Create([2, 2, 2, 2, 2], _applicationOptions.DidDomainName), [2, 2, 2, 2, 2], basicTier.Id, "USRb");
await CreateIdentityAIfNecessary(basicTier!, cancellationToken);
await CreateIdentityBIfNecessary(basicTier!, cancellationToken);
}

await _identitiesRepository.Add(identityA, "Aaaaaaaa1!");
await _identitiesRepository.Add(identityB, "Bbbbbbbb1!");
private async Task CreateIdentityAIfNecessary(Tier basicTier, CancellationToken cancellationToken)
{
var addressOfIdentityA = IdentityAddress.Create([1, 1, 1, 1, 1], _applicationOptions.DidDomainName);
var identityA = await _identitiesRepository.FindByAddress(addressOfIdentityA, cancellationToken);
if (identityA == null)
{
identityA = Identity.CreateTestIdentity(addressOfIdentityA, [1, 1, 1, 1, 1], basicTier.Id, "USRa");
await _identitiesRepository.Add(identityA, "Aaaaaaaa1!");
}
}

private async Task CreateIdentityBIfNecessary(Tier basicTier, CancellationToken cancellationToken)
{
var addressOfIdentityB = IdentityAddress.Create([2, 2, 2, 2, 2], _applicationOptions.DidDomainName);
var identityB = await _identitiesRepository.FindByAddress(addressOfIdentityB, cancellationToken);
if (identityB == null)
{
identityB = Identity.CreateTestIdentity(addressOfIdentityB, [2, 2, 2, 2, 2], basicTier.Id, "USRb");
tnotheis marked this conversation as resolved.
Show resolved Hide resolved
await _identitiesRepository.Add(identityB, "Bbbbbbbb1!");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Backbone.Modules.Devices.Application.Identities.Queries.GetDeletionProcessAsOwner;
using Backbone.Modules.Devices.Application.Identities.Queries.GetDeletionProcessesAsOwner;
using Backbone.Modules.Devices.Application.Identities.Queries.GetOwnIdentity;
using Backbone.Modules.Devices.Application.Identities.Queries.IsIdentityOfUserDeleted;
using Backbone.Modules.Devices.Infrastructure.OpenIddict;
using MediatR;
using Microsoft.AspNetCore.Authorization;
Expand Down Expand Up @@ -128,6 +129,16 @@ public async Task<IActionResult> GetOwnIdentity(CancellationToken cancellationTo
var response = await _mediator.Send(new GetOwnIdentityQuery(), cancellationToken);
return Ok(response);
}

[HttpGet("IsDeleted")]
[ProducesResponseType(typeof(IsIdentityOfUserDeletedResponse), StatusCodes.Status200OK)]
[ProducesError(StatusCodes.Status200OK)]
[AllowAnonymous]
public async Task<IActionResult> IsIdentityOfUserDeleted([FromQuery(Name = "username")] string username, CancellationToken cancellationToken)
{
var response = await _mediator.Send(new IsIdentityOfUserDeletedQuery(username), cancellationToken);
return Ok(response);
}
}

public class CreateIdentityRequest
Expand Down
Loading
Loading