Skip to content

Commit

Permalink
Merge branch 'main' into possibility-for-a-device-to-find-out-whether…
Browse files Browse the repository at this point in the history
…-its-identity-was-deleted

# Conflicts:
#	Modules/Devices/src/Devices.Infrastructure/Persistence/Repository/IdentitiesRepository.cs
  • Loading branch information
tnotheis committed Dec 2, 2024
2 parents 7b67e8c + dcec3ea commit fee4967
Show file tree
Hide file tree
Showing 36 changed files with 2,359 additions and 201 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,6 @@ Generated_Code/
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,18 @@ User creates a Device
When i sends a POST request to the /Devices endpoint with a valid signature on c
Then the response status code is 201 (Created)
And the response contains a Device

Scenario: Registering a backup Device
Given Identity i
And a Challenge c created by i
When i sends a POST request to the /Devices endpoint with a valid signature on c as a backup Device
Then the response status code is 201 (Created)
And the response contains a Device
And the created Device is a backup Device

Scenario: Registering a second backup Device is not possible
Given an Identity i with a Device d1 and a backup Device d2
And a Challenge c created by i
When i sends a POST request to the /Devices endpoint with a valid signature on c as a backup Device
Then the response status code is 400 (Bad Request)
And the response content contains an error with the error code "error.platform.validation.device.onlyOneBackupDeviceCanExist"
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Backbone.ConsumerApi.Sdk;
using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types;
using Backbone.ConsumerApi.Sdk;
using Backbone.ConsumerApi.Sdk.Authentication;
using Backbone.ConsumerApi.Sdk.Endpoints.Devices.Types.Requests;
using Backbone.ConsumerApi.Sdk.Endpoints.Devices.Types.Responses;
using Backbone.ConsumerApi.Tests.Integration.Configuration;
using Backbone.ConsumerApi.Tests.Integration.Contexts;
using Backbone.ConsumerApi.Tests.Integration.Helpers;
Expand All @@ -24,6 +26,8 @@ internal class DevicesStepDefinitions
private readonly ResponseContext _responseContext;
private readonly ClientPool _clientPool;

private ApiResponse<RegisterDeviceResponse>? _registerDeviceResponse;

public DevicesStepDefinitions(ChallengesContext challengesContext, ResponseContext responseContext, HttpClientFactory factory,
IOptions<HttpConfiguration> httpConfiguration, ClientPool clientPool)
{
Expand Down Expand Up @@ -51,8 +55,17 @@ public async Task GivenAnIdentityWithADeviceAndAnUnonboardedDevice(string identi
{
var client = await Client.CreateForNewIdentity(_httpClient, _clientCredentials, DEVICE_PASSWORD);
_clientPool.Add(client).ForIdentity(identityName).AndDevice(onboardedDeviceName);
var clientForUnOnboardedDevice = await client.OnboardNewDevice("Passw0rd");
_clientPool.Add(clientForUnOnboardedDevice).ForIdentity(identityName).AndDevice(unonboardedDeviceName);
var clientForBackupDevice = await client.OnboardNewDevice("Passw0rd");
_clientPool.Add(clientForBackupDevice).ForIdentity(identityName).AndDevice(unonboardedDeviceName);
}

[Given($"an Identity {RegexFor.SINGLE_THING} with a Device {RegexFor.SINGLE_THING} and a backup Device {RegexFor.SINGLE_THING}")]
public async Task GivenAnIdentityWithADeviceAndABackupDevice(string identityName, string onboardedDeviceName, string backupDeviceName)
{
var client = await Client.CreateForNewIdentity(_httpClient, _clientCredentials, DEVICE_PASSWORD);
_clientPool.Add(client).ForIdentity(identityName).AndDevice(onboardedDeviceName);
var clientForBackupDevice = await client.OnboardNewBackupDevice("Passw0rd");
_clientPool.Add(clientForBackupDevice).ForIdentity(identityName).AndDevice(backupDeviceName);
}

[Given($"an Identity {RegexFor.SINGLE_THING} with Devices {RegexFor.LIST_OF_THINGS}")]
Expand Down Expand Up @@ -82,10 +95,25 @@ public async Task WhenIdentitySendsAPostRequestToTheDevicesEndpointWithASignedCh
var identity = _clientPool.FirstForIdentityName(identityName);
var signedChallenge = CreateSignedChallenge(identity, _challengesContext.Challenges[challengeName]);

_responseContext.WhenResponse = await identity.Devices.RegisterDevice(new RegisterDeviceRequest
_responseContext.WhenResponse = _registerDeviceResponse = await identity.Devices.RegisterDevice(new RegisterDeviceRequest
{
DevicePassword = DEVICE_PASSWORD,
SignedChallenge = signedChallenge,
IsBackupDevice = false
});
}

[When($"{RegexFor.SINGLE_THING} sends a POST request to the /Devices endpoint with a valid signature on {RegexFor.SINGLE_THING} as a backup Device")]
public async Task WhenIdentitySendsAPostRequestToTheDevicesEndpointWithASignedChallengeAsABackupDevice(string identityName, string challengeName)
{
var identity = _clientPool.FirstForIdentityName(identityName);
var signedChallenge = CreateSignedChallenge(identity, _challengesContext.Challenges[challengeName]);

_responseContext.WhenResponse = _registerDeviceResponse = await identity.Devices.RegisterDevice(new RegisterDeviceRequest
{
DevicePassword = DEVICE_PASSWORD,
SignedChallenge = signedChallenge
SignedChallenge = signedChallenge,
IsBackupDevice = true
});
}

Expand Down Expand Up @@ -174,5 +202,11 @@ public async Task ThenTheBackboneHasPersistedAsTheNewCommunicationLanguageOfDevi
response.Result!.First().CommunicationLanguage.Should().Be(_communicationLanguage);
}

[Then("the created Device is a backup Device")]
public void ThenTheCreatedDeviceIsABackupDevice()
{
_registerDeviceResponse!.Result!.IsBackupDevice.Should().BeTrue();
}

#endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ public CustomSigninManager(
IAuthenticationSchemeProvider schemes,
IUserConfirmation<ApplicationUser> confirmation) : base(userManager, contextAccessor, claimsFactory,
optionsAccessor, logger, schemes, confirmation)
{ }
{
}

public override async Task<SignInResult> CheckPasswordSignInAsync(ApplicationUser user, string password,
bool lockoutOnFailure)
Expand All @@ -35,7 +36,7 @@ public override async Task<SignInResult> CheckPasswordSignInAsync(ApplicationUse

private async Task UpdateLastLoginDate(ApplicationUser user)
{
user.LoginOccurred();
user.Device.LoginOccurred();
await UserManager.UpdateAsync(user);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ namespace Backbone.BuildingBlocks.API.AspNetCoreIdentityCustomizations;

public class CustomUserStore : UserStore<ApplicationUser>
{
public CustomUserStore(DevicesDbContext context, IdentityErrorDescriber? describer = null) : base(context, describer) { }
public CustomUserStore(DevicesDbContext context, IdentityErrorDescriber? describer = null) : base(context, describer)
{
}

public override async Task<ApplicationUser?> FindByIdAsync(string userId, CancellationToken cancellationToken = default)
{
Expand All @@ -34,4 +36,11 @@ public CustomUserStore(DevicesDbContext context, IdentityErrorDescriber? describ

return user;
}

public override Task<IdentityResult> UpdateAsync(ApplicationUser user, CancellationToken cancellationToken = new CancellationToken())
{
Context.Attach(user.Device);
Context.Update(user.Device);
return base.UpdateAsync(user, cancellationToken);
}
}
6 changes: 6 additions & 0 deletions Modules/Devices/src/Devices.Application/ApplicationErrors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,11 @@ public static ApplicationError ClientReachedIdentitiesLimit()
return new ApplicationError("error.platform.validation.device.clientReachedIdentitiesLimit",
"The client's Identity limit has been reached. A new Identity cannot be created with this client.");
}

public static ApplicationError BackupDeviceAlreadyExists()
{
return new ApplicationError("error.platform.validation.device.onlyOneBackupDeviceCanExist",
"Only one backup device can be created per identity.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Backbone.Modules.Devices.Domain.Entities.Identities;
using MediatR;
using Microsoft.Extensions.Logging;
using ApplicationException = Backbone.BuildingBlocks.Application.Abstractions.Exceptions.ApplicationException;

namespace Backbone.Modules.Devices.Application.Devices.Commands.RegisterDevice;

Expand All @@ -31,18 +32,21 @@ public async Task<RegisterDeviceResponse> Handle(RegisterDeviceCommand command,
{
var identity = await _identitiesRepository.FindByAddress(_userContext.GetAddress(), cancellationToken, track: true) ?? throw new NotFoundException(nameof(Identity));

if (command.IsBackupDevice && await _identitiesRepository.HasBackupDevice(identity.Address, cancellationToken))
throw new ApplicationException(ApplicationErrors.Devices.BackupDeviceAlreadyExists());

await _challengeValidator.Validate(command.SignedChallenge, PublicKey.FromBytes(identity.PublicKey));
_logger.LogTrace("Successfully validated challenge.");

var communicationLanguageResult = CommunicationLanguage.Create(command.CommunicationLanguage);

var newDevice = identity.AddDevice(communicationLanguageResult.Value, _userContext.GetDeviceId());
var newDevice = identity.AddDevice(communicationLanguageResult.Value, _userContext.GetDeviceId(), command.IsBackupDevice);

await _identitiesRepository.UpdateWithNewDevice(identity, command.DevicePassword);

_logger.CreatedDevice();

return new RegisterDeviceResponse(newDevice.User);
return new RegisterDeviceResponse(newDevice);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ public class RegisterDeviceCommand : IRequest<RegisterDeviceResponse>
public required string DevicePassword { get; set; }
public required string CommunicationLanguage { get; set; }
public required SignedChallengeDTO SignedChallenge { get; set; }
public required bool IsBackupDevice { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ namespace Backbone.Modules.Devices.Application.Devices.Commands.RegisterDevice;

public class RegisterDeviceResponse
{
public RegisterDeviceResponse(ApplicationUser user)
public RegisterDeviceResponse(Device device)
{
Id = user.DeviceId;
Username = user.UserName!;
CreatedByDevice = user.Device.CreatedByDevice;
CreatedAt = user.Device.CreatedAt;
Id = device.Id.Value;
Username = device.User.UserName!;
CreatedByDevice = device.CreatedByDevice.Value;
CreatedAt = device.CreatedAt;
IsBackupDevice = device.IsBackupDevice;
}

public string Id { get; set; }
public string Username { get; set; }
public DateTime CreatedAt { get; set; }
public string CreatedByDevice { get; set; }
public bool IsBackupDevice { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public DeviceDTO(Device device)
CreatedByDevice = device.CreatedByDevice;
LastLogin = new LastLoginInformation { Time = device.User.LastLoginAt };
CommunicationLanguage = device.CommunicationLanguage;
IsBackupDevice = device.IsBackupDevice;
}

public string Id { get; set; }
Expand All @@ -20,6 +21,7 @@ public DeviceDTO(Device device)
public string CreatedByDevice { get; set; }
public LastLoginInformation LastLogin { get; set; }
public string CommunicationLanguage { get; set; }
public bool IsBackupDevice { get; set; }
}

public class LastLoginInformation
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus;
using Backbone.BuildingBlocks.Application.PushNotifications;
using Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.Device;
using Backbone.Modules.Devices.Domain.DomainEvents.Outgoing;

namespace Backbone.Modules.Devices.Application.DomainEvents.Incoming.BackupDeviceUsed;

public class BackupDeviceUsedDomainEventHandler : IDomainEventHandler<BackupDeviceUsedDomainEvent>
{
private readonly IPushNotificationSender _pushNotificationSender;

public BackupDeviceUsedDomainEventHandler(IPushNotificationSender pushNotificationSender)
{
_pushNotificationSender = pushNotificationSender;
}

public async Task Handle(BackupDeviceUsedDomainEvent @event)
{
await _pushNotificationSender.SendNotification(new BackupDeviceUsedPushNotification(), SendPushNotificationFilter.AllDevicesOf(@event.IdentityAddress), CancellationToken.None);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus;
using Backbone.Modules.Devices.Application.DomainEvents.Incoming.AnnouncementCreated;
using Backbone.Modules.Devices.Application.DomainEvents.Incoming.BackupDeviceUsed;
using Backbone.Modules.Devices.Application.DomainEvents.Incoming.DatawalletModificationCreated;
using Backbone.Modules.Devices.Application.DomainEvents.Incoming.ExternalEventCreated;
using Backbone.Modules.Devices.Application.DomainEvents.Incoming.IdentityDeletionProcessStarted;
Expand All @@ -15,6 +16,7 @@ public static class IEventBusExtensions
public static void AddDevicesDomainEventSubscriptions(this IEventBus eventBus)
{
eventBus.SubscribeToAnnouncementsEvents();
eventBus.SubscribeToDevicesEvents();
eventBus.SubscribeToSynchronizationEvents();
}

Expand All @@ -23,6 +25,11 @@ private static void SubscribeToAnnouncementsEvents(this IEventBus eventBus)
eventBus.Subscribe<AnnouncementCreatedDomainEvent, AnnouncementCreatedDomainEventHandler>();
}

private static void SubscribeToDevicesEvents(this IEventBus eventBus)
{
eventBus.Subscribe<BackupDeviceUsedDomainEvent, BackupDeviceUsedDomainEventHandler>();
}

private static void SubscribeToSynchronizationEvents(this IEventBus eventBus)
{
eventBus.Subscribe<DatawalletModifiedDomainEvent, DatawalletModifiedDomainEventHandler>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public interface IIdentitiesRepository
Task<IEnumerable<Device>> GetDevicesByIds(IEnumerable<DeviceId> deviceIds, CancellationToken cancellationToken, bool track = false);
Task Update(Device device, CancellationToken cancellationToken);
Task<T[]> FindDevices<T>(Expression<Func<Device, bool>> filter, Expression<Func<Device, T>> selector, CancellationToken cancellationToken, bool track = false);
Task<bool> HasBackupDevice(IdentityAddress identity, CancellationToken cancellationToken);

#endregion

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Backbone.BuildingBlocks.Application.PushNotifications;

namespace Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.Device;

public class BackupDeviceUsedPushNotification : IPushNotification;
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ public async Task<IActionResult> RegisterDevice(RegisterDeviceRequest request, C
{
CommunicationLanguage = request.CommunicationLanguage ?? CommunicationLanguage.DEFAULT_LANGUAGE.Value,
SignedChallenge = request.SignedChallenge,
DevicePassword = request.DevicePassword
DevicePassword = request.DevicePassword,
IsBackupDevice = request.IsBackupDevice ?? false
};

var response = await _mediator.Send(command, cancellationToken);
Expand Down Expand Up @@ -110,4 +111,5 @@ public class RegisterDeviceRequest
public required string DevicePassword { get; set; }
public string? CommunicationLanguage { get; set; }
public required SignedChallengeDTO SignedChallenge { get; set; }
public bool? IsBackupDevice { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Backbone.BuildingBlocks.Domain.Events;
using Backbone.DevelopmentKit.Identity.ValueObjects;

namespace Backbone.Modules.Devices.Domain.DomainEvents.Outgoing;

public class BackupDeviceUsedDomainEvent : DomainEvent
{
public BackupDeviceUsedDomainEvent(IdentityAddress identityAddress)
{
IdentityAddress = identityAddress;
}

public IdentityAddress IdentityAddress { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public Device Device

public bool HasLoggedIn => LastLoginAt.HasValue;

public void LoginOccurred()
internal void LoginOccurred()
{
LastLoginAt = SystemTime.UtcNow;
}
Expand Down
Loading

0 comments on commit fee4967

Please sign in to comment.