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

Consumer API: Prevent messages to identities to be deleted #685

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f47fad4
feat: add find all method with possible identity address collection f…
daniel-almeida-konkconsulting Jun 5, 2024
c6988f0
test: unit test find all identities handler
daniel-almeida-konkconsulting Jun 5, 2024
70c444b
feat: ensure message is only sent if none of the recipients has an id…
daniel-almeida-konkconsulting Jun 5, 2024
c9206d2
refactor: add status filter to list identities query to prevent pulli…
daniel-almeida-konkconsulting Jun 6, 2024
ce9e65d
test: exclude consumer api projects from arch unit tests
daniel-almeida-konkconsulting Jun 6, 2024
b561681
test: add integration tests for messages controller (send messages)
daniel-almeida-konkconsulting Jun 6, 2024
6d7c96d
Merge branch 'main' into nmshdb-131-prevent-messages-to-identities-to…
mergify[bot] Jun 6, 2024
22d5adb
refactor: use expression for identitiy filtering
daniel-almeida-konkconsulting Jun 7, 2024
2d32f15
Merge branch 'main' into nmshdb-131-prevent-messages-to-identities-to…
daniel-almeida-konkconsulting Jun 7, 2024
6c6a112
fix: arch unit tests
daniel-almeida-konkconsulting Jun 7, 2024
0bbf77a
Merge branch 'main' into nmshdb-131-prevent-messages-to-identities-to…
mergify[bot] Jun 10, 2024
1aef86e
Merge branch 'main' into nmshdb-131-prevent-messages-to-identities-to…
mergify[bot] Jun 10, 2024
6547a01
test: try to make deserialization of data reusable
tnotheis Jun 10, 2024
321ebc7
test: make PeersToBeDeleted property required and rename class
tnotheis Jun 10, 2024
cd5d140
chore: formatting
tnotheis Jun 10, 2024
0704f87
Merge branch 'main' into nmshdb-131-prevent-messages-to-identities-to…
daniel-almeida-konkconsulting Jun 11, 2024
d428f0b
fix: update npm packages with vulnerabilties
daniel-almeida-konkconsulting Jun 11, 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
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");
daniel-almeida-konkconsulting marked this conversation as resolved.
Show resolved Hide resolved

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);
daniel-almeida-konkconsulting marked this conversation as resolved.
Show resolved Hide resolved
}

[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