Skip to content

Commit

Permalink
Implement external properties/soft-deleting features
Browse files Browse the repository at this point in the history
  • Loading branch information
npavlyk82 committed Feb 12, 2025
1 parent 38c5855 commit aae8ab7
Show file tree
Hide file tree
Showing 24 changed files with 297 additions and 22 deletions.
4 changes: 2 additions & 2 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
"isRoot": true,
"tools": {
"dotnet-sonarscanner": {
"version": "4.10.0",
"version": "5.13.1",
"commands": [
"dotnet-sonarscanner"
]
}
}
}
}
6 changes: 3 additions & 3 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup .NET
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
3
Expand All @@ -22,8 +22,8 @@ jobs:
uses: swisslife-oss/actions/pull-request@main
with:
sonar_token: ${{ secrets.SONAR_TOKEN }}
sonar_project_key: 'SwissLife-OSS_HotChocolateExtensions'
sonar_project_name: 'hotchocolate-extensions'
sonar_project_key: 'SwissLife-OSS_Bewit'
sonar_project_name: 'bewit'
pr_number: ${{ github.event.pull_request.number }}
pr_source_branch: ${{ github.head_ref }}
pr_target_branch: ${{ github.base_ref }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup .NET
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
3
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,17 @@ ISchema schema = SchemaBuilder.New()
d.Name("Mutation");
d.Field("RequestAccessUrl")
.Type<NonNullType<StringType>>()
.Resolver(ctx => $"{mvcApiUrl}/api/file/123")
.Resolver(ctx =>
{
Dictionary<string, string> extraProperties = new()
{
["foo"] = "bar"
};

ctx.AddBewitTokenExtraProperties(extraProperties);

return $"{mvcApiUrl}/api/file/123";
})
.UseBewitUrlProtection();
}))
.Create();
Expand Down
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"sdk": {
"version": "8.0.402"
"version": "8.0.405"
}
}
3 changes: 2 additions & 1 deletion samples/Endpoint_SecuredUrl/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
var id = c.Request.RouteValues.GetValueOrDefault("id");

BewitToken<string> token =
await generator.GenerateBewitTokenAsync($"/download/{id}", default);
await generator.GenerateBewitTokenAsync($"/download/{id}",
new Dictionary<string, object>(), default);

string html = @$"<html><a href=""/download/{id}?bewit={token}"">download</a>
<br>{(string)token}</html>";
Expand Down
9 changes: 7 additions & 2 deletions samples/HotChocolate_SecuredArgument/Types/Mutation.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Bewit.Generation;
Expand Down Expand Up @@ -29,14 +30,18 @@ public async Task<string> InvalidateBewitTokens(
public async Task<string> CreateBewitToken(string value)
{
return (await _fooPayloadGenerator
.GenerateBewitTokenAsync(new FooPayload {Value = value}, default))
.GenerateBewitTokenAsync(
new FooPayload {Value = value},
new Dictionary<string, object>(),
default))
.ToString();
}

public async Task<string> CreateIdentifiableBewitToken(string identifier)
{
return (await _barPayloadGenerator
.GenerateIdentifiableBewitTokenAsync(new BarPayload(), identifier, default))
.GenerateIdentifiableBewitTokenAsync(
new BarPayload(), identifier, new Dictionary<string, object>(), default))
.ToString();
}
}
Expand Down
7 changes: 7 additions & 0 deletions src/Core/Token.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;

namespace Bewit
Expand All @@ -22,6 +23,12 @@ protected Token(string nonce, DateTime expirationDate)

public DateTime ExpirationDate { get; private set; }

[JsonIgnore]
public bool? IsDeleted { get; set; } = false;

[JsonIgnore]
public Dictionary<string, object> ExtraProperties { get; set; }

public static Token Create(string nonce, DateTime expirationDate)
{
if (string.IsNullOrWhiteSpace(nonce))
Expand Down
3 changes: 2 additions & 1 deletion src/Extensions.HotChocolate/Generation/BewitMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ public async Task InvokeAsync(
if (context.Result is TPayload result)
{
BewitToken<TPayload> bewit
= await tokenGenerator.GenerateBewitTokenAsync(result, context.RequestAborted);
= await tokenGenerator.GenerateBewitTokenAsync(
result, context.GetBewitTokenExtraProperties(), context.RequestAborted);

context.Result = (string)bewit;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.Linq;
using HotChocolate.Resolvers;

namespace Bewit.Extensions.HotChocolate.Generation;

public static class BewitTokenExtraPropertiesHelper

{
private const string ExtraPropertyPrefix = "BewitTokenExtraProperty:";

public static void AddBewitTokenExtraProperties(
this IResolverContext resolverContext, Dictionary<string, object> extraProperties)
{
if (extraProperties == null)
{
return;
}

resolverContext.ScopedContextData =
resolverContext.ScopedContextData.SetItems(
extraProperties.ToDictionary(
ctx => $"{ExtraPropertyPrefix}{ctx.Key}",
ctx => ctx.Value));
}

public static Dictionary<string, object> GetBewitTokenExtraProperties(this IMiddlewareContext context)
{
Dictionary<string, object> extraProperties = new Dictionary<string, object>();

foreach (var key in context.ScopedContextData.Keys)
{
if (!key.StartsWith(ExtraPropertyPrefix))
{
continue;
}

object extraPropertyValue = context.ScopedContextData.GetValueOrDefault(key);

if (extraPropertyValue != null)
{
extraProperties.Add(
key.Substring(ExtraPropertyPrefix.Length),
extraPropertyValue);
}
}

return extraProperties;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public async Task InvokeAsync(

BewitToken<string> bewit =
await tokenGenerator.GenerateBewitTokenAsync(
uri.PathAndQuery, context.RequestAborted);
uri.PathAndQuery, context.GetBewitTokenExtraProperties(), context.RequestAborted);

var parametersToAdd = new Dictionary<string, string>
{
Expand Down
8 changes: 7 additions & 1 deletion src/Generation/BewitTokenGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Threading;
Expand Down Expand Up @@ -43,19 +44,24 @@ public BewitTokenGenerator(

public Task<BewitToken<T>> GenerateBewitTokenAsync(
T payload,
CancellationToken cancellationToken)
Dictionary<string, object> extraProperties,
CancellationToken cancellationToken
)
{
var token = Token.Create(CreateNextToken(), CreateExpirationDate());
token.ExtraProperties = extraProperties;
return GenerateBewitTokenImplAsync(payload, token, cancellationToken);

}

public Task<BewitToken<T>> GenerateIdentifiableBewitTokenAsync(
T payload,
string identifier,
Dictionary<string, object> extraProperties,
CancellationToken cancellationToken)
{
var token = new IdentifiableToken(identifier, CreateNextToken(), CreateExpirationDate());
token.ExtraProperties = extraProperties;
return GenerateBewitTokenImplAsync(payload, token, cancellationToken);
}

Expand Down
2 changes: 2 additions & 0 deletions src/Generation/IBewitTokenGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

Expand All @@ -7,6 +8,7 @@ public interface IBewitTokenGenerator<T>
{
Task<BewitToken<T>> GenerateBewitTokenAsync(
T payload,
Dictionary<string, object> extraProperties,
CancellationToken cancellationToken);
}
}
2 changes: 2 additions & 0 deletions src/Generation/IIdentifiableBewitTokenGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

Expand All @@ -8,6 +9,7 @@ public interface IIdentifiableBewitTokenGenerator<T>
Task<BewitToken<T>> GenerateIdentifiableBewitTokenAsync(
T payload,
string identifier,
Dictionary<string, object> extraProperties,
CancellationToken cancellationToken);

Task InvalidateIdentifier(
Expand Down
3 changes: 3 additions & 0 deletions src/Storage.MongoDB/MongoNonceOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,8 @@ public class MongoNonceOptions
/// ReUse will keep it in the storage.
/// </summary>
public NonceUsage NonceUsage { get; set; } = NonceUsage.OneTime;


public int RecordExpireAfterDays { get; set; } = 365 * 2;
}
}
77 changes: 75 additions & 2 deletions src/Storage.MongoDB/MongoNonceRepository.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson.Serialization;
Expand Down Expand Up @@ -27,6 +29,8 @@ static MongoNonceRepository()
{
cm.MapIdMember(c => c.Nonce);
cm.MapField(c => c.ExpirationDate);
cm.MapField(c => c.IsDeleted);
cm.MapField(c => c.ExtraProperties);
cm.SetIgnoreExtraElements(true);
});
}
Expand All @@ -38,24 +42,46 @@ public MongoNonceRepository(IMongoDatabase database, MongoNonceOptions options)

_collection.Indexes.CreateOne(new CreateIndexModel<Token>(
Builders<Token>.IndexKeys.Ascending(nameof(IdentifiableToken.Identifier))));

_collection.Indexes.CreateOne(new CreateIndexModel<Token>(
Builders<Token>.IndexKeys.Ascending(nameof(Token.ExpirationDate)),
new CreateIndexOptions
{
ExpireAfter = TimeSpan.FromDays(options.RecordExpireAfterDays)
}));
}

public async ValueTask InsertOneAsync(
Token token, CancellationToken cancellationToken)
{
token.ExtraProperties = token.ExtraProperties ?? new Dictionary<string, object>();

IReadOnlySet<string> propertyNamesWithPrimitiveValueType =
GetPropertyNamesWithPrimitiveValueType(token.ExtraProperties);

SetJsonStringValue(token.ExtraProperties, propertyNamesWithPrimitiveValueType);

await _collection.InsertOneAsync(token, cancellationToken: cancellationToken);

CreateIndexes(propertyNamesWithPrimitiveValueType);
}

public async ValueTask<Token?> TakeOneAsync(
string token,
CancellationToken cancellationToken)
{
FilterDefinition<Token> findFilter = Builders<Token>.Filter.Eq(n => n.Nonce, token);
FilterDefinition<Token> findFilter =
Builders<Token>.Filter.Eq(n => n.Nonce, token) &
(Builders<Token>.Filter.Not(Builders<Token>.Filter.Exists(n => n.IsDeleted)) |
Builders<Token>.Filter.Eq(n => n.IsDeleted, false));

UpdateDefinition<Token> updateDefinition =
Builders<Token>.Update.Set(x => x.IsDeleted, true);

if (_options.NonceUsage == NonceUsage.OneTime)
{
return await _collection
.FindOneAndDeleteAsync(findFilter, cancellationToken: cancellationToken);
.FindOneAndUpdateAsync(findFilter, updateDefinition, cancellationToken: cancellationToken);
}

return await _collection
Expand All @@ -77,5 +103,52 @@ public static void Initialize()
{
//ensure static constructor is called
}

private void CreateIndexes(IEnumerable<string> extraPropertyNames)
{
var indexOptions = new CreateIndexOptions { Background = true };

foreach (string extraPropertyName in extraPropertyNames)
{
_collection.Indexes.CreateOne(new CreateIndexModel<Token>(
Builders<Token>.IndexKeys.Ascending(
$"{nameof(Token.ExtraProperties)}.{extraPropertyName}"), indexOptions));
}
}

private IReadOnlySet<string> GetPropertyNamesWithPrimitiveValueType(
Dictionary<string, object> extraProperties)
{
HashSet<string> names = new HashSet<string>();

foreach (KeyValuePair<string, object> keyValue in extraProperties)
{
if (keyValue.Value == null)
{
continue;
}

if (TypeChecker.IsPrimitiveType(keyValue.Value.GetType()))
{
names.Add(keyValue.Key);
}
}

return names;
}

private void SetJsonStringValue(
Dictionary<string, object> extraProperties, IReadOnlySet<string> namesToSkip)
{
foreach(string name in extraProperties.Keys)
{
if (namesToSkip.Contains(name))
{
continue;
}

extraProperties[name] = JsonSerializer.Serialize(extraProperties[name]);
}
}
}
}
Loading

0 comments on commit aae8ab7

Please sign in to comment.