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: Password-protected Tokens #909

Merged
merged 25 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b4dc74d
feat: implement password protected tokens
NikolaVetnic Oct 14, 2024
d6487c7
Merge branch 'main' into abl-18-password-protected-tokens
mergify[bot] Oct 14, 2024
7b82a82
chore: remove unused methods
NikolaVetnic Oct 14, 2024
008b3e7
chore: remove empty line
NikolaVetnic Oct 14, 2024
c667515
Merge branch 'abl-18-password-protected-tokens' of github.com:nmshd/b…
NikolaVetnic Oct 14, 2024
b33084d
test: add tests for anonymous user fetching tokens
NikolaVetnic Oct 15, 2024
f642ee4
Merge branch 'main' into abl-18-password-protected-tokens
mergify[bot] Oct 15, 2024
8aad121
fix: add check for existing allocations to relationship template CanB…
tnotheis Oct 15, 2024
733b609
chore: add explaining comments to password checks in Token.cs
tnotheis Oct 15, 2024
470418e
refactor: don't use queryable extensions; instead use expressions in …
tnotheis Oct 15, 2024
3728499
refactor: remove redundant "when not null" check from validator
tnotheis Oct 15, 2024
9f86cd2
refactor: rename things
tnotheis Oct 15, 2024
b7de38b
fix: re-add check for "password is not null" to validators
tnotheis Oct 15, 2024
c6a8f2f
Merge branch 'main' into abl-18-password-protected-tokens
NikolaVetnic Oct 15, 2024
7cf0f69
test: add can be collected with password tests
NikolaVetnic Oct 15, 2024
6c593d2
Merge branch 'abl-18-password-protected-tokens' of github.com:nmshd/b…
NikolaVetnic Oct 15, 2024
e9d0476
test: check for status code in get multiple templates feature file
tnotheis Oct 15, 2024
b73431a
test: check for status code in get multiple tokens feature file
tnotheis Oct 15, 2024
bb6b411
fix: don't throw ArgumentNullException in ReplaceExpressionVisitor
tnotheis Oct 15, 2024
926a852
test: fix naming
tnotheis Oct 15, 2024
bebfae1
fix: use correct error message in validator tests
tnotheis Oct 15, 2024
a130ec4
fix: use correct error message in CreateToken validator tests
tnotheis Oct 15, 2024
4abc7b6
test: add tests for anonymous user
NikolaVetnic Oct 15, 2024
285f116
Merge branch 'abl-18-password-protected-tokens' of github.com:nmshd/b…
NikolaVetnic Oct 15, 2024
638832f
Merge branch 'main' into abl-18-password-protected-tokens
mergify[bot] Oct 16, 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
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ User requests Relationship Templates
| rt12 | - |
| rt13 | password |
| rt14 | wordpass |
Then the response contains Relationship Template(s) <retreivedTemplates>
Then the response status code is 200 (OK)
And the response contains Relationship Template(s) <retreivedTemplates>

Examples:
| activeIdentity | retreivedTemplates |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,46 @@ Feature: GET /Tokens

User requests multiple Tokens

Scenario Outline: Requesting a list of Tokens in a variety of scenarios
Given Identities i1, i2, i3 and i4
And the following Tokens
| tokenName | tokenOwner | 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 /Tokens endpoint with the following payloads
| tokenName | 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 Token(s) <retreivedTokens>
Scenario Outline: Requesting a list of Tokens in a variety of scenarios
Given Identities i1, i2, i3 and i4
And the following Tokens
| tokenName | tokenOwner | 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 /Tokens endpoint with the following payloads
| tokenName | passwordOnGet |
| rt1 | - |
| rt2 | - |
| rt3 | password |
| rt4 | password |
| rt5 | password |
| rt6 | - |
| rt7 | password |
| rt8 | - |
| rt9 | - |
| rt10 | - |
| rt11 | - |
| rt12 | - |
| rt13 | password |
| rt14 | wordpass |
Then the response status code is 200 (OK)
And the response contains Token(s) <retreivedTokens>

Examples:
| activeIdentity | retreivedTokens |
| 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 |
Examples:
| activeIdentity | retreivedTokens |
| 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 @@ -14,7 +14,8 @@ public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> exp
var rightVisitor = new ReplaceExpressionVisitor(expression2.Parameters[0], parameter);
var right = rightVisitor.Visit(expression2.Body);

return Expression.Lambda<Func<T, bool>>(Expression.OrElse(left, right), parameter);
// CAUTION: the null suppression operator is used here without being sure if it's safe; so if there's a NullReferenceException, this is the first place to check
return Expression.Lambda<Func<T, bool>>(Expression.OrElse(left!, right!), parameter);
}

public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2)
Expand All @@ -27,7 +28,8 @@ public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> ex
var rightVisitor = new ReplaceExpressionVisitor(expression2.Parameters[0], parameter);
var right = rightVisitor.Visit(expression2.Body);

return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(left, right), parameter);
// CAUTION: the null suppression operator is used here without being sure if it's safe; so if there's a NullReferenceException, this is the first place to check
return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(left!, right!), parameter);
}

private class ReplaceExpressionVisitor : ExpressionVisitor
Expand All @@ -41,10 +43,8 @@ public ReplaceExpressionVisitor(Expression oldValue, Expression newValue)
_newValue = newValue;
}

public override Expression Visit(Expression? node)
public override Expression? Visit(Expression? node)
{
ArgumentNullException.ThrowIfNull(node);

return node == _oldValue ? _newValue : base.Visit(node);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,6 @@ public void Fails_when_Password_is_too_long()

// Assert
validationResult.ShouldHaveValidationErrorForItem(nameof(CreateRelationshipTemplateCommand.Password), "error.platform.validation.invalidPropertyValue",
"'Password' must be between 0 and 200 bytes long. You entered 250 bytes.");
"'Password' must be between 1 and 200 bytes long. You entered 250 bytes.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ public Validator()
.ValidId<CreateRelationshipTemplateCommand, IdentityAddress>()
.When(c => c.ForIdentity != null);

RuleFor(c => c.Password).NumberOfBytes(0, RelationshipTemplate.MAX_PASSWORD_LENGTH);
RuleFor(c => c.Password).NumberOfBytes(1, RelationshipTemplate.MAX_PASSWORD_LENGTH).When(t => t.Password != null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ public Validator()
{
RuleFor(x => x.Id).ValidId<GetRelationshipTemplateQuery, RelationshipTemplateId>();

RuleFor(x => x.Password).NumberOfBytes(1, RelationshipTemplate.MAX_PASSWORD_LENGTH);
RuleFor(x => x.Password).NumberOfBytes(1, RelationshipTemplate.MAX_PASSWORD_LENGTH).When(t => t.Password != null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Xunit;

namespace Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates;

public class RelationshipTemplateCanBeCollectedBy : AbstractTestsBase
{
private const string I1 = "did:e:prod.enmeshed.eu:dids:70cf4f3e6edf6bca33d35f";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates;

public class RelationshipTemplateCanBeCollectedWithPasswordTests : AbstractTestsBase
public class RelationshipTemplateCanBeCollectedUsingPasswordExpressionTests : AbstractTestsBase
{
[Fact]
public void Can_collect_without_a_password_when_no_password_is_defined()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ public Validator()
.ValidId<CreateTokenCommand, IdentityAddress>()
.When(t => t.ForIdentity != null);

RuleFor(c => c.Password).NumberOfBytes(0, Token.MAX_PASSWORD_LENGTH);
RuleFor(c => c.Password).NumberOfBytes(1, Token.MAX_PASSWORD_LENGTH).When(c => c.Password != null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ public class Validator : AbstractValidator<GetTokenQuery>
public Validator()
{
RuleFor(x => x.Id).ValidId<GetTokenQuery, TokenId>();
RuleFor(x => x.Password).NumberOfBytes(1, Token.MAX_PASSWORD_LENGTH);
RuleFor(x => x.Password).NumberOfBytes(1, Token.MAX_PASSWORD_LENGTH).When(t => t.Password != null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,6 @@ public void Fails_when_Password_is_too_long()

// Assert
validationResult.ShouldHaveValidationErrorForItem(nameof(CreateTokenCommand.Password), "error.platform.validation.invalidPropertyValue",
"'Password' must be between 0 and 200 bytes long. You entered 250 bytes.");
"'Password' must be between 1 and 200 bytes long. You entered 250 bytes.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
namespace Backbone.Modules.Tokens.Domain.Tests.TestHelpers;
public class TestData
{
public static Token CreateToken(IdentityAddress createdBy, IdentityAddress? forIdentity)
public static Token CreateToken(IdentityAddress createdBy, IdentityAddress? forIdentity, byte[]? password = null)
{
return new Token(createdBy, DeviceId.Parse("DVC1"), [], DateTime.Now.AddDays(1), forIdentity);
return new Token(createdBy, DeviceId.Parse("DVC1"), [], DateTime.Now.AddDays(1), forIdentity, password);
}
}
tnotheis marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using Backbone.Modules.Tokens.Domain.Tests.TestHelpers;
using Backbone.UnitTestTools.Data;
using FluentAssertions;
using Xunit;

namespace Backbone.Modules.Tokens.Domain.Tests.Tests;

public class TokenCanBeCollectedUsingPasswordTests
{
[Fact]
public void Can_collect_without_a_password_when_no_password_is_defined()
{
// Arrange
var creator = TestDataGenerator.CreateRandomIdentityAddress();
var collector = TestDataGenerator.CreateRandomIdentityAddress();

var template = TestData.CreateToken(creator, null);

// Act
var result = template.CanBeCollectedUsingPassword(collector, null);

// Assert
result.Should().BeTrue();
}

[Fact]
public void Can_collect_with_a_password_when_no_password_is_defined()
{
// Arrange
var creator = TestDataGenerator.CreateRandomIdentityAddress();
var collector = TestDataGenerator.CreateRandomIdentityAddress();

var template = TestData.CreateToken(creator, null);

// Act
var result = template.CanBeCollectedUsingPassword(collector, [1]);

// Assert
result.Should().BeTrue();
}

[Fact]
public void Can_collect_with_correct_password()
{
// Arrange
var creator = TestDataGenerator.CreateRandomIdentityAddress();
var collector = TestDataGenerator.CreateRandomIdentityAddress();

var template = TestData.CreateToken(creator, null, password: [1]);

// Act
var result = template.CanBeCollectedUsingPassword(collector, [1]);

// Assert
result.Should().BeTrue();
}

[Fact]
public void Cannot_collect_with_incorrect_password()
{
// Arrange
var creator = TestDataGenerator.CreateRandomIdentityAddress();
var collector = TestDataGenerator.CreateRandomIdentityAddress();

var template = TestData.CreateToken(creator, null, password: [1]);

// Act
var result = template.CanBeCollectedUsingPassword(collector, [2]);

// Assert
result.Should().BeFalse();
}

[Fact]
public void Can_collect_as_owner_without_a_password()
{
// Arrange
var creator = TestDataGenerator.CreateRandomIdentityAddress();

var template = TestData.CreateToken(creator, null, password: [1]);

// Act
var result = template.CanBeCollectedUsingPassword(creator, null);

// Assert
result.Should().BeTrue();
}
}