Skip to content

Commit

Permalink
Consumer API: Password-protected RelationshipTemplates (#900)
Browse files Browse the repository at this point in the history
* feat: support password protected relationship templates, wip

* test: remove redundant scenarios

* test: move step definitions to correct places and only use SINGLE_THING_OR_DEFAULT where it's fitting

* test: rename SINGLE_THING_OR_DEFAULT to OPTIONAL_SINGLE_THING to better reflect its purpose

* test: revert change to ThenTheResponseStatusCodeIs step definition

* test: use GetBytes extension method to convert string to bytes

* feat: when establishing a relationship, make sure the template is allocated by the active identity

* chore: fix a nullability issue

* refactor: expose RelationshipTemplate.CanBeCollectedWithPassword as simple method

* test: use Convert.FromBase64String again

* refactor: renaming and simplifcations

* fix: change implementation of CanBeCollectedWithPassword2 to not compare arrays with equals operator

* test: fix assertion

* chore: rename scenario

* test: remove redundant test case

* test: state explicitly which relations templates are expected

* test: tidy up

* feat: update relationship template validators

* chore: update method name

* chore: rename method

* chore: clean up message

* chore: remove redundant method

* feat: update sdk to allow for password protected templates

* fix: add endpoint expected by transport tests

* fix: add temporary endpoint to satisfy transport tests

* feat: address pr change requests

* fix: address issues causing tests to fail

* chore: fix formatting

* feat: catch and process json parsing exception

* fix: revert manually edited snapshots

* feat: add migration limiting the password length

* test: limit the scope of resharper warning disabling

* test: improve property names

* refactor: rename RelationshipTemplateQuery to RelationshipTemplateQueryItem in SDK

* refactor: don't set error message and code when using NumberOfBytes extension method

* fix: allow camelCase in GetAll query string

� Conflicts:
�	Modules/Relationships/src/Relationships.ConsumerApi/Controllers/RelationshipTemplatesController.cs

* refactor: add curly braces for better readability

* refactor: use generic application error in case the input cannot be parsed

* fix: fix merge error

* chore: delete redundant methods added by merge

* chore: fix formatting

* feat: revert to state before adding the password field

* feat: add password with required length in a single migration

* chore: remove comment

* refactor: add migration "the correct way"

* refactor: use new MAX_PASSWORD_LENGTH constant in ListRelationshipTemplatesQuery Validator

* feat: validate password max length in GetRelationshipTemplateQuery

* refactor: remove database configuration from db context

* chore: fix formatting

---------

Co-authored-by: Timo Notheisen <[email protected]>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Oct 11, 2024
1 parent 26be069 commit 8c93751
Show file tree
Hide file tree
Showing 37 changed files with 1,035 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Backbone.AdminApi.Infrastructure.DTOs;
using Backbone.BuildingBlocks.API;
using Backbone.BuildingBlocks.API.Mvc.ExceptionFilters;
using Backbone.BuildingBlocks.Application.Abstractions.Exceptions;
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext;
using Backbone.Modules.Devices.Application.Devices.Commands.RegisterDevice;
using Backbone.Modules.Devices.Application.Devices.DTOs;
Expand Down Expand Up @@ -55,7 +56,7 @@ public static IServiceCollection AddCustomAspNetCore(this IServiceCollection ser
: $"'{nameOfPropertyWithError}': {firstErrorMessage}";
context.HttpContext.Response.ContentType = "application/json";
var responsePayload = new HttpResponseEnvelopeError(
HttpError.ForProduction("error.platform.inputCannotBeParsed", formattedMessage,
HttpError.ForProduction(GenericApplicationErrors.Validation.InputCannotBeParsed().Code, formattedMessage,
"")); // TODO: add docs
return new BadRequestObjectResult(responsePayload);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Text.Json.Serialization;
using Backbone.BuildingBlocks.API;
using Backbone.BuildingBlocks.API.Mvc.ExceptionFilters;
using Backbone.BuildingBlocks.Application.Abstractions.Exceptions;
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext;
using Backbone.ConsumerApi.Configuration;
using Backbone.Infrastructure.UserContext;
Expand Down Expand Up @@ -46,7 +47,7 @@ public static IServiceCollection AddCustomAspNetCore(this IServiceCollection ser
: $"'{nameOfPropertyWithError}': {firstErrorMessage}";
context.HttpContext.Response.ContentType = "application/json";
var responsePayload = new HttpResponseEnvelopeError(
HttpError.ForProduction("error.platform.inputCannotBeParsed", formattedMessage,
HttpError.ForProduction(GenericApplicationErrors.Validation.InputCannotBeParsed().Code, formattedMessage,
"")); // TODO: add docs
return new BadRequestObjectResult(responsePayload);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
@Integration
Feature: GET /RelationshipTemplates

User requests Relationship Templates

Scenario Outline: Requesting a list of Relationship Templates in a variety of scenarios
Given Identities i1, i2, i3 and i4
And the following Relationship Templates
| templateName | templateOwner | forIdentity | password |
| rt1 | i1 | - | - |
| rt2 | i2 | - | - |
| rt3 | i1 | - | - |
| rt4 | i2 | - | - |
| rt5 | i1 | - | password |
| rt6 | i1 | - | password |
| rt7 | i2 | - | password |
| rt8 | i2 | - | password |
| rt9 | i1 | i1 | - |
| rt10 | i2 | i3 | - |
| rt11 | i2 | i2 | - |
| rt12 | i2 | i3 | - |
| rt13 | i2 | i3 | password |
| rt14 | i2 | i3 | password |
When <activeIdentity> sends a GET request to the /RelationshipTemplate endpoint with the following payloads
| templateName | passwordOnGet |
| rt1 | - |
| rt2 | - |
| rt3 | password |
| rt4 | password |
| rt5 | password |
| rt6 | - |
| rt7 | password |
| rt8 | - |
| rt9 | - |
| rt10 | - |
| rt11 | - |
| rt12 | - |
| rt13 | password |
| rt14 | wordpass |
Then the response contains Relationship Template(s) <retreivedTemplates>

Examples:
| activeIdentity | retreivedTemplates |
| i1 | rt1, rt2, rt3, rt4, rt5, rt6, rt7, rt9 |
| i2 | rt1, rt2, rt3, rt4, rt5, rt7, rt8, rt10, rt11, rt12, rt13, rt14 |
| i3 | rt1, rt2, rt3, rt4, rt5, rt7, rt10, rt12, rt13 |
| i4 | rt1, rt2, rt3, rt4, rt5, rt7 |
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,14 @@ User creates a Relationship Template
When i sends a POST request to the /RelationshipTemplates endpoint
Then the response status code is 201 (Created)
And the response contains a RelationshipMetadata

Scenario: Creating a relationship template with a password
Given Identity i
When i sends a POST request to the /RelationshipTemplates endpoint with the password "my-password"
Then the response status code is 201 (Created)
And the response contains a RelationshipMetadata

Scenario: Create a personalized Relationship Template with a password
Given Identities i1 and i2
When i1 sends a POST request to the /RelationshipTemplate endpoint with password "my-password" and forIdentity i2
Then the response status code is 201 (Created)
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,26 @@ Feature: GET /RelationshipTemplates/{id}

User requests a Relationship Template

Scenario: Requesting a Relationship Template with ForIdentity created for me
Given Identities i1 and i2
And Relationship Template rt created by i1 where ForIdentity is the address of i2
When i2 sends a GET request to the /RelationshipTemplates/{id} endpoint with rt.Id
Then the response status code is 200 (Ok)
And the response contains a rt
Scenario Outline: Requesting a Relationship Template in a variety of scenarios
Given Identities <givenIdentities>
And Relationship Template rt created by <templateOwner> with password "<password>" and forIdentity <forIdentity>
When <activeIdentity> sends a GET request to the /RelationshipTemplate/rt.Id endpoint with password "<passwordOnGet>"
Then the response status code is <responseStatusCode>

Scenario: Requesting a Relationship Template with ForIdentity created by me
Given Identities i1 and i2
And Relationship Template rt created by i1 where ForIdentity is the address of i2
When i1 sends a GET request to the /RelationshipTemplates/{id} endpoint with rt.Id
Then the response status code is 200 (Ok)
And the response contains a rt

Scenario: Requesting a Relationship Template with ForIdentity created for someone else
Given Identities i1, i2 and i3
And Relationship Template rt created by i1 where ForIdentity is the address of i2
When i3 sends a GET request to the /RelationshipTemplates/{id} endpoint with rt.Id
Then the response status code is 404 (Not Found)
Examples:
| givenIdentities | templateOwner | forIdentity | password | activeIdentity | passwordOnGet | responseStatusCode | description |
| i | i | - | - | i | - | 200 (OK) | owner tries to get |
| i1 and i2 | i1 | - | - | i2 | - | 200 (OK) | non-owner tries to get |
| i | i | - | - | i | password | 200 (OK) | owner passes password even though none is set |
| i1 and i2 | i1 | - | - | i2 | password | 200 (OK) | non-owner identity passes password even though none is set |
| i | i | - | password | i | password | 200 (OK) | owner passes correct password |
| i | i | - | password | i | - | 200 (OK) | owner doesn't pass password, even though one is set |
| i1 and i2 | i1 | - | password | i2 | password | 200 (OK) | non-owner identity passes correct password |
| i1 and i2 | i1 | - | password | i2 | - | 404 (Not Found) | non-owner identity passes no password even though one is set |
| i | i | i | - | i | - | 200 (OK) | owner is forIdentity and tries to get |
| i1 and i2 | i1 | i2 | - | i1 | - | 200 (OK) | non-owner is forIdentity, creator tries to get |
| i1 and i2 | i1 | i1 | - | i2 | - | 404 (Not Found) | owner is forIdentity and non-owner tries to get |
| i1 and i2 | i1 | i2 | - | i2 | - | 200 (OK) | non-owner is forIdentity and tries to get |
| i1 and i2 | i1 | i2 | password | i2 | password | 200 (OK) | non-owner is forIdentity and tries to get with correct password |
| i1 and i2 | i1 | i2 | password | i2 | wordpass | 404 (Not Found) | non-owner is forIdentity and tries to get with incorrect password |
| i1, i2 and i3 | i1 | i2 | password | i3 | password | 404 (Not Found) | non-owner is forIdentity, and thirdParty tries to get |
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
public static class RegexFor
{
public const string SINGLE_THING = "([a-zA-Z0-9]+)";
public const string OPTIONAL_SINGLE_THING = "([a-zA-Z0-9-]+)";
public const string LIST_OF_THINGS = "([a-zA-Z0-9, ]+)";
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
using Backbone.ConsumerApi.Sdk.Endpoints.RelationshipTemplates.Types.Requests;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types;
using Backbone.ConsumerApi.Sdk.Endpoints.RelationshipTemplates.Types.Requests;
using Backbone.ConsumerApi.Sdk.Endpoints.RelationshipTemplates.Types.Responses;
using Backbone.ConsumerApi.Tests.Integration.Contexts;
using Backbone.ConsumerApi.Tests.Integration.Helpers;
using TechTalk.SpecFlow.Assist;

namespace Backbone.ConsumerApi.Tests.Integration.StepDefinitions;

Expand All @@ -13,6 +18,8 @@ internal class RelationshipTemplatesStepDefinitions
private readonly RelationshipTemplatesContext _relationshipTemplatesContext;
private readonly ClientPool _clientPool;

private ApiResponse<ListRelationshipTemplatesResponse>? _listRelationshipTemplatesResponse;

public RelationshipTemplatesStepDefinitions(ResponseContext responseContext, RelationshipTemplatesContext relationshipTemplatesContext, ClientPool clientPool)
{
_responseContext = responseContext;
Expand All @@ -24,18 +31,37 @@ public RelationshipTemplatesStepDefinitions(ResponseContext responseContext, Rel

#region Given

[Given($"Relationship Template {RegexFor.SINGLE_THING} created by {RegexFor.SINGLE_THING} where ForIdentity is the address of {RegexFor.SINGLE_THING}")]
public async Task GivenRelationshipTemplateCreatedByIWhereForIdentityIsTheAddressOfI(string relationshipTemplateName, string identityName, string forIdentityName)
[Given($@"Relationship Template {RegexFor.SINGLE_THING} created by {RegexFor.SINGLE_THING} with password ""([^""]*)"" and forIdentity {RegexFor.OPTIONAL_SINGLE_THING}")]
public async Task GivenRelationshipTemplateCreatedByTokenOwnerWithPasswordAndForIdentity(string relationshipTemplateName, string identityName, string passwordString, string forIdentityName)
{
var client = _clientPool.FirstForIdentityName(identityName);
var forClient = _clientPool.FirstForIdentityName(forIdentityName);
var forClient = forIdentityName != "-" ? _clientPool.FirstForIdentityName(forIdentityName).IdentityData!.Address : null;
var password = passwordString.Trim() != "-" ? Convert.FromBase64String(passwordString.Trim()) : null;

var response = await client.RelationshipTemplates
.CreateTemplate(new CreateRelationshipTemplateRequest { Content = TestData.SOME_BYTES, ForIdentity = forClient.IdentityData!.Address });
.CreateTemplate(new CreateRelationshipTemplateRequest { Content = TestData.SOME_BYTES, ForIdentity = forClient, Password = password });

_relationshipTemplatesContext.CreateRelationshipTemplatesResponses[relationshipTemplateName] = response.Result!;
}

[Given(@"the following Relationship Templates")]
public async Task GivenRelationshipTemplatesWithTheFollowingProperties(Table table)
{
var relationshipTemplatePropertiesSet = table.CreateSet<RelationshipTemplateProperties>();

foreach (var relationshipTemplateProperties in relationshipTemplatePropertiesSet)
{
var client = _clientPool.FirstForIdentityName(relationshipTemplateProperties.TemplateOwner);
var forClient = relationshipTemplateProperties.ForIdentity != "-" ? _clientPool.FirstForIdentityName(relationshipTemplateProperties.ForIdentity).IdentityData!.Address : null;
var password = relationshipTemplateProperties.Password.Trim() != "-" ? Convert.FromBase64String(relationshipTemplateProperties.Password.Trim()) : null;

var response = await client.RelationshipTemplates
.CreateTemplate(new CreateRelationshipTemplateRequest { Content = TestData.SOME_BYTES, ForIdentity = forClient, Password = password });

_relationshipTemplatesContext.CreateRelationshipTemplatesResponses[relationshipTemplateProperties.TemplateName] = response.Result!;
}
}

#endregion

#region When
Expand All @@ -45,19 +71,89 @@ public async Task WhenIdentitySendsAPostRequestToTheRelationshipTemplatesEndpoin
{
var client = _clientPool.FirstForIdentityName(identityName);

_responseContext.WhenResponse = await client.RelationshipTemplates.CreateTemplate(new CreateRelationshipTemplateRequest { Content = TestData.SOME_BYTES });
_responseContext.WhenResponse = await client.RelationshipTemplates.CreateTemplate(
new CreateRelationshipTemplateRequest { Content = TestData.SOME_BYTES });
}

[When($@"{RegexFor.SINGLE_THING} sends a POST request to the /RelationshipTemplates endpoint with the password ""(.*)""")]
public async Task WhenIdentitySendsAPostRequestToTheRelationshipTemplatesEndpointWithThePassword(string identityName, string passwordString)
{
var client = _clientPool.FirstForIdentityName(identityName);
var password = Encoding.UTF8.GetBytes(passwordString);

_responseContext.WhenResponse = await client.RelationshipTemplates.CreateTemplate(
new CreateRelationshipTemplateRequest { Content = TestData.SOME_BYTES, Password = password });
}

[When($@"{RegexFor.SINGLE_THING} sends a POST request to the /RelationshipTemplate endpoint with password ""(.*)"" and forIdentity {RegexFor.SINGLE_THING}")]
public async Task WhenIdentitySendsAPostRequestToTheRelationshipTemplatesEndpointWithPasswordAndForIdentity(string identityName, string passwordString, string forIdentityName)
{
var client = _clientPool.FirstForIdentityName(identityName);
var forClient = _clientPool.FirstForIdentityName(forIdentityName);
var password = Encoding.UTF8.GetBytes(passwordString);

_responseContext.WhenResponse = await client.RelationshipTemplates.CreateTemplate(
new CreateRelationshipTemplateRequest { Content = TestData.SOME_BYTES, ForIdentity = forClient.IdentityData!.Address, Password = password });
}

[When($"{RegexFor.SINGLE_THING} sends a GET request to the /RelationshipTemplates/{{id}} endpoint with {RegexFor.SINGLE_THING}.Id")]
public async Task WhenISendsAGetRequestToTheRelationshipTemplatesIdEndpointWithId(string identityName, string relationshipTemplateName)
[When($@"{RegexFor.SINGLE_THING} sends a GET request to the /RelationshipTemplate/{RegexFor.SINGLE_THING}.Id endpoint with password ""([^""]*)""")]
public async Task WhenIdentitySendsAGetRequestToTheRelationshipTemplatesIdEndpointWithPassword(string identityName, string relationshipTemplateName, string password)
{
var client = _clientPool.FirstForIdentityName(identityName);

var relationshipTemplateId = _relationshipTemplatesContext.CreateRelationshipTemplatesResponses[relationshipTemplateName].Id;

_responseContext.WhenResponse = await client.RelationshipTemplates.GetTemplate(relationshipTemplateId);
_responseContext.WhenResponse = password != "-"
? await client.RelationshipTemplates.GetTemplate(relationshipTemplateId, Convert.FromBase64String(password.Trim()))
: await client.RelationshipTemplates.GetTemplate(relationshipTemplateId);
}

[When($@"{RegexFor.SINGLE_THING} sends a GET request to the /RelationshipTemplate endpoint with the following payloads")]
public async Task WhenISendsAGETRequestToTheRelationshipTemplateEndpointWithTheFollowingPayloads(string identityName, Table table)
{
var client = _clientPool.FirstForIdentityName(identityName);

var getRequestPayloadSet = table.CreateSet<GetRequestPayload>();

var queryItems = getRequestPayloadSet.Select(payload =>
{
var relationshipTemplateId = _relationshipTemplatesContext.CreateRelationshipTemplatesResponses[payload.TemplateName].Id;
var password = payload.PasswordOnGet == "-" ? null : Convert.FromBase64String(payload.PasswordOnGet.Trim());

return new RelationshipTemplateQueryItem { Id = relationshipTemplateId, Password = password };
}).ToList();

_responseContext.WhenResponse = _listRelationshipTemplatesResponse = await client.RelationshipTemplates.ListTemplates(queryItems);
}

#endregion

#region Then

[Then($@"the response contains Relationship Template\(s\) {RegexFor.LIST_OF_THINGS}")]
public void ThenTheResponseContainsRelationshipTemplates(string relationshipTemplateNames)
{
var relationshipTemplates = relationshipTemplateNames.Split(',').Select(item => _relationshipTemplatesContext.CreateRelationshipTemplatesResponses[item.Trim()]).ToList();
_listRelationshipTemplatesResponse!.Result!.Should().BeEquivalentTo(relationshipTemplates, options => options.WithStrictOrdering());
}

#endregion
}

// ReSharper disable once ClassNeverInstantiated.Local
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
file class RelationshipTemplateProperties
{
public required string TemplateName { get; set; }
public required string TemplateOwner { get; set; }
public required string ForIdentity { get; set; }
public required string Password { get; set; }
}

// ReSharper disable once ClassNeverInstantiated.Local
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
file class GetRequestPayload
{
public required string TemplateName { get; set; }
public required string PasswordOnGet { get; set; }
}
Loading

0 comments on commit 8c93751

Please sign in to comment.