Skip to content

Commit

Permalink
Consumer API: Prevent messages to identities to be deleted (#685)
Browse files Browse the repository at this point in the history
* feat: add find all method with possible identity address collection filter

* test: unit test find all identities handler

* feat: ensure message is only sent if none of the recipients has an identity to be deleted

* refactor: add status filter to list identities query to prevent pulling too many instances into memory

* test: exclude consumer api projects from arch unit tests

* test: add integration tests for messages controller (send messages)

* refactor: use expression for identitiy filtering

* fix: arch unit tests

* test: try to make deserialization of data reusable

* test: make PeersToBeDeleted property required and rename class

* chore: formatting

* fix: update npm packages with vulnerabilties

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Timo Notheisen <[email protected]>
  • Loading branch information
3 people authored Jun 11, 2024
1 parent 8f29142 commit de940a3
Show file tree
Hide file tree
Showing 22 changed files with 280 additions and 78 deletions.
14 changes: 7 additions & 7 deletions AdminApi/src/AdminApi/ClientApp/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Backbone.Tests.ArchUnit/CleanArchitecture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ public class CleanArchitecture
.ResideInAssembly("Backbone.Modules.*", true)
.As("All Modules");

private static readonly IObjectProvider<IType> CONSUMER_API_ASSEMBLIES =
Types().That()
.ResideInAssembly("Backbone.Modules.*.ConsumerApi", true)
.As("ConsumerApi Assemblies");

private static readonly IObjectProvider<IType> APPLICATION_ASSEMBLIES =
Types().That()
.ResideInAssembly("Backbone.Modules.*.Application", true)
Expand All @@ -34,6 +39,7 @@ public void ModulesShouldNotDependOnOtherModules(IObjectProvider<IType> module)
Types()
.That().Are(module)
.And().AreNot(Backbone.TEST_TYPES)
.And().AreNot(CONSUMER_API_ASSEMBLIES)
.Should().NotDependOnAny(otherModules)
.Because("modules should be self-contained.")
.Check(Backbone.ARCHITECTURE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ private HttpError CreateHttpErrorForDomainException(DomainException domainExcept
});
}

return null;
return applicationException.AdditionalData;
}

private static HttpStatusCode GetStatusCodeForInfrastructureException(InfrastructureException _)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ namespace Backbone.BuildingBlocks.Application.Abstractions.Exceptions;

public class ApplicationError
{
public ApplicationError(string code, string message)
public ApplicationError(string code, string message, dynamic? additionalData = null)
{
Code = code;
Message = message;
AdditionalData = additionalData;
}

public string Code { get; }
public string Message { get; }
public dynamic? AdditionalData { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ public class ApplicationException : Exception
public ApplicationException(ApplicationError error) : base(error.Message)
{
Code = error.Code;
AdditionalData = error.AdditionalData;
}

public ApplicationException(ApplicationError error, Exception innerException) : base(error.Message,
innerException)
{
Code = error.Code;
AdditionalData = error.AdditionalData;
}

public string Code { get; set; }
public dynamic? AdditionalData { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Text.Json;
using System.Web;
using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types;
using Backbone.Tooling.JsonConverters;
using JsonSerializer = System.Text.Json.JsonSerializer;

namespace Backbone.BuildingBlocks.SDK.Endpoints.Common;
Expand Down Expand Up @@ -34,7 +33,6 @@ public EndpointClient(HttpClient httpClient, IAuthenticator authenticator, JsonS
_httpClient = httpClient;
_authenticator = authenticator;
_jsonSerializerOptions = jsonSerializerOptions;
jsonSerializerOptions.Converters.Add(new UrlSafeBase64ToByteArrayJsonConverter());
}

public async Task<ApiResponse<T>> Post<T>(string url, object? requestContent = null)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
namespace Backbone.BuildingBlocks.SDK.Endpoints.Common.Types;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Backbone.BuildingBlocks.SDK.Endpoints.Common.Types;

public class ApiError
{
Expand All @@ -7,4 +10,37 @@ public class ApiError
public required string Message { get; set; }
public required string Docs { get; set; }
public required DateTime Time { get; set; }
public ApiErrorData? Data { get; set; }
}

public class ApiErrorData
{
private readonly JsonElement _data;
private readonly JsonSerializerOptions _optionsUsedToDeserializeThis;

private ApiErrorData(JsonElement data, JsonSerializerOptions options)
{
_data = data;
_optionsUsedToDeserializeThis = options; // we need to keep the options to be able to deserialize the data later
}

public T As<T>()
{
var json = _data.GetRawText();
return JsonSerializer.Deserialize<T>(json, _optionsUsedToDeserializeThis)!;
}

public class JsonConverter : JsonConverter<ApiErrorData>
{
public override ApiErrorData Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var element = JsonSerializer.Deserialize<JsonElement>(ref reader, options);
return new ApiErrorData(element, options);
}

public override void Write(Utf8JsonWriter writer, ApiErrorData value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value.As<JsonElement>(), options);
}
}
}
11 changes: 10 additions & 1 deletion ConsumerApi.Sdk/Configuration.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
using System.Text.Json;
using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types;
using Backbone.ConsumerApi.Sdk.Authentication;
using Backbone.Tooling.JsonConverters;

namespace Backbone.ConsumerApi.Sdk;

public class Configuration
{
public Configuration()
{
JsonSerializerOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
JsonSerializerOptions.Converters.Add(new UrlSafeBase64ToByteArrayJsonConverter());
JsonSerializerOptions.Converters.Add(new ApiErrorData.JsonConverter());
}

public required AuthenticationConfiguration Authentication { get; init; }
public JsonSerializerOptions JsonSerializerOptions { get; init; } = new() { PropertyNameCaseInsensitive = true };
public JsonSerializerOptions JsonSerializerOptions { get; init; }

public class AuthenticationConfiguration
{
Expand Down
18 changes: 18 additions & 0 deletions ConsumerApi.Tests.Integration/Features/Messages/POST.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@Integration
Feature: POST Message

User sends a Message

Scenario: Sending a Message
Given Identities i1 and i2 with an established Relationship
When i1 sends a POST request to the /Messages endpoint with i2 as recipient
Then the response status code is 201 (Created)
And the response contains a SendMessageResponse

Scenario: Sending a Message to Identity to be deleted
Given Identities i1 and i2 with an established Relationship
And i2 is in status "ToBeDeleted"
When i1 sends a POST request to the /Messages endpoint with i2 as recipient
Then the response status code is 400 (Bad Request)
And the response content contains an error with the error code "error.platform.validation.message.recipientToBeDeleted"
And the error contains a list of Identities to be deleted that includes i2
38 changes: 38 additions & 0 deletions ConsumerApi.Tests.Integration/Helpers/Utils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Backbone.ConsumerApi.Sdk;
using Backbone.ConsumerApi.Sdk.Endpoints.Relationships.Types.Requests;
using Backbone.ConsumerApi.Sdk.Endpoints.RelationshipTemplates.Types.Requests;
using Backbone.ConsumerApi.Tests.Integration.Extensions;
using Backbone.Crypto;

namespace Backbone.ConsumerApi.Tests.Integration.Helpers;

public static class Utils
{
public static async Task EstablishRelationshipBetween(Client client1, Client client2)
{
var createRelationshipTemplateRequest = new CreateRelationshipTemplateRequest
{
Content = ConvertibleString.FromUtf8("AAA").BytesRepresentation
};

var relationshipTemplateResponse = await client1.RelationshipTemplates.CreateTemplate(createRelationshipTemplateRequest);
relationshipTemplateResponse.Should().BeASuccess();

var createRelationshipRequest = new CreateRelationshipRequest
{
RelationshipTemplateId = relationshipTemplateResponse.Result!.Id,
Content = ConvertibleString.FromUtf8("AAA").BytesRepresentation
};

var createRelationshipResponse = await client2.Relationships.CreateRelationship(createRelationshipRequest);
createRelationshipResponse.Should().BeASuccess();

var completeRelationshipChangeRequest = new CompleteRelationshipChangeRequest
{
Content = ConvertibleString.FromUtf8("AAA").BytesRepresentation
};
var acceptRelationChangeResponse =
await client1.Relationships.AcceptChange(createRelationshipResponse.Result!.Id, createRelationshipResponse.Result.Changes.First().Id, completeRelationshipChangeRequest);
acceptRelationChangeResponse.Should().BeASuccess();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types;
using Backbone.ConsumerApi.Sdk;
using Backbone.ConsumerApi.Sdk.Authentication;
using Backbone.ConsumerApi.Sdk.Endpoints.Messages.Types.Requests;
using Backbone.ConsumerApi.Sdk.Endpoints.Messages.Types.Responses;
using Backbone.ConsumerApi.Tests.Integration.Configuration;
using Backbone.ConsumerApi.Tests.Integration.Extensions;
using Backbone.ConsumerApi.Tests.Integration.Helpers;
using Backbone.ConsumerApi.Tests.Integration.Support;
using Backbone.Crypto;
using Microsoft.Extensions.Options;

namespace Backbone.ConsumerApi.Tests.Integration.StepDefinitions;

[Binding]
[Scope(Feature = "POST Message")]
internal class MessagesStepDefinitions
{
private Client _client1 = null!;
private Client _client2 = null!;
private ApiResponse<SendMessageResponse>? _sendMessageResponse;
private readonly ClientCredentials _clientCredentials;
private readonly HttpClient _httpClient;

public MessagesStepDefinitions(HttpClientFactory factory, IOptions<HttpConfiguration> httpConfiguration)
{
_httpClient = factory.CreateClient();
_clientCredentials = new ClientCredentials(httpConfiguration.Value.ClientCredentials.ClientId, httpConfiguration.Value.ClientCredentials.ClientSecret);
}

[Given("Identities i1 and i2 with an established Relationship")]
public async Task GivenIdentitiesI1AndI2WithAnEstablishedRelationship()
{
_client1 = await Client.CreateForNewIdentity(_httpClient, _clientCredentials, Constants.DEVICE_PASSWORD);
_client2 = await Client.CreateForNewIdentity(_httpClient, _clientCredentials, Constants.DEVICE_PASSWORD);

await Utils.EstablishRelationshipBetween(_client1, _client2);
}

[Given("i2 is in status \"ToBeDeleted\"")]
public async Task GivenIdentityI2IsToBeDeleted()
{
var startDeletionProcessResponse = await _client2.Identities.StartDeletionProcess();
startDeletionProcessResponse.Should().BeASuccess();
}

[When("i1 sends a POST request to the /Messages endpoint with i2 as recipient")]
public async Task WhenAPostRequestIsSentToTheMessagesEndpoint()
{
var sendMessageRequest = new SendMessageRequest
{
Attachments = [],
Body = ConvertibleString.FromUtf8("Some Message").BytesRepresentation,
Recipients =
[
new SendMessageRequestRecipientInformation
{
Address = _client2.IdentityData!.Address,
EncryptedKey = ConvertibleString.FromUtf8("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").BytesRepresentation
}
]
};
_sendMessageResponse = await _client1.Messages.SendMessage(sendMessageRequest);
}

[Then(@"the response status code is (\d\d\d) \(.+\)")]
public void ThenTheResponseStatusCodeIs(int expectedStatusCode)
{
((int)_sendMessageResponse!.Status).Should().Be(expectedStatusCode);
}

[Then("the response contains a SendMessageResponse")]
public void ThenTheResponseContainsASendMessageResponse()
{
_sendMessageResponse!.Result.Should().NotBeNull();
_sendMessageResponse.Should().BeASuccess();
_sendMessageResponse.Should().ComplyWithSchema();
}

[Then(@"the response content contains an error with the error code ""([^""]*)""")]
public void ThenTheResponseContentIncludesAnErrorWithTheErrorCode(string errorCode)
{
_sendMessageResponse!.Error.Should().NotBeNull();
_sendMessageResponse.Error!.Code.Should().Be(errorCode);
}

[Then(@"the error contains a list of Identities to be deleted that includes i2")]
public void ThenTheErrorContainsAListOfIdentitiesToBeDeletedThatIncludesIdentityI2()
{
var data = _sendMessageResponse!.Error!.Data?.As<PeersToBeDeletedErrorData>();
data.Should().NotBeNull();
data!.PeersToBeDeleted.Contains(_client2.IdentityData!.Address).Should().BeTrue();
}
}

public class PeersToBeDeletedErrorData
{
public required List<string> PeersToBeDeleted { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public IdentitySummaryDTO(Identity identity)
Address = identity.Address.ToString();
PublicKey = identity.PublicKey;
CreatedAt = identity.CreatedAt;
Status = identity.Status;

Devices = identity.Devices.Select(d => new DeviceDTO
{
Expand All @@ -33,6 +34,7 @@ public IdentitySummaryDTO(Identity identity)
public string? ClientId { get; set; }
public byte[] PublicKey { get; set; }
public DateTime CreatedAt { get; set; }
public IdentityStatus Status { get; set; }

public IEnumerable<DeviceDTO> Devices { get; set; }
public int NumberOfDevices { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System.Linq.Expressions;
using Backbone.Modules.Devices.Application.DTOs;
using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository;
using Backbone.Modules.Devices.Domain.Entities.Identities;
using MediatR;

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

public class Handler : IRequestHandler<ListIdentitiesQuery, ListIdentitiesResponse>
{
private readonly IIdentitiesRepository _identitiesRepository;
Expand All @@ -14,9 +17,12 @@ public Handler(IIdentitiesRepository repository)

public async Task<ListIdentitiesResponse> Handle(ListIdentitiesQuery request, CancellationToken cancellationToken)
{
var dbPaginationResult = await _identitiesRepository.FindAll(request.PaginationFilter, cancellationToken);
var identityDtos = dbPaginationResult.ItemsOnPage.Select(el => new IdentitySummaryDTO(el)).ToList();
Expression<Func<Identity, bool>> filter = i => (request.Addresses == null || request.Addresses.Contains(i.Address)) &&
(request.Status == null || i.Status == request.Status);

var identities = await _identitiesRepository.Find(filter, cancellationToken);
var identityDtos = identities.Select(i => new IdentitySummaryDTO(i)).ToList();

return new ListIdentitiesResponse(identityDtos, request.PaginationFilter, dbPaginationResult.TotalNumberOfItems);
return new ListIdentitiesResponse(identityDtos);
}
}
Loading

0 comments on commit de940a3

Please sign in to comment.