Skip to content

Commit

Permalink
Fixed export type issues (#9)
Browse files Browse the repository at this point in the history
* Fixed export type issues

* Test coverage

* Test coverage
  • Loading branch information
mtmk authored Aug 22, 2024
1 parent a99c1a0 commit a498767
Show file tree
Hide file tree
Showing 12 changed files with 251 additions and 13 deletions.
4 changes: 2 additions & 2 deletions NATS.Jwt.Tests/Models/NatsAccountClaimsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public void SerializeDeserialize_FullNatsAccountClaims_ShouldSucceed()
Subject = "import.>",
Account = "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII",
LocalSubject = "local.import.>",
Type = 1,
Type = NatsExportType.Service,
Share = true,
AllowTrace = true,
},
Expand All @@ -75,7 +75,7 @@ public void SerializeDeserialize_FullNatsAccountClaims_ShouldSucceed()
{
Name = "TestExport",
Subject = "export.>",
Type = 1,
Type = NatsExportType.Service,
TokenReq = true,
Revocations =
new Dictionary<string, long>
Expand Down
40 changes: 38 additions & 2 deletions NATS.Jwt.Tests/Models/NatsExportTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public void TestNatsExportSerializationDeserialization()
{
Name = "TestExport",
Subject = "test.subject",
Type = 1,
Type = NatsExportType.Service,
TokenReq = true,
Revocations = new() { { "key1", 123456789L }, { "key2", 987654321L }, },
ResponseType = "Stream",
Expand All @@ -32,7 +32,7 @@ public void TestNatsExportSerializationDeserialization()

string json = JsonSerializer.Serialize(natsExport);

string expectedJson = "{\"name\":\"TestExport\",\"subject\":\"test.subject\",\"type\":1,\"token_req\":true,\"revocations\":{\"key1\":123456789,\"key2\":987654321},\"response_type\":\"Stream\",\"response_threshold\":\"00:00:05\",\"service_latency\":{\"sampling\":50,\"results\":\"results.subject\"},\"account_token_position\":2,\"advertise\":true,\"allow_trace\":true,\"description\":\"Test Description\",\"info_url\":\"https://example.com/info\"}";
string expectedJson = "{\"name\":\"TestExport\",\"subject\":\"test.subject\",\"type\":\"service\",\"token_req\":true,\"revocations\":{\"key1\":123456789,\"key2\":987654321},\"response_type\":\"Stream\",\"response_threshold\":\"00:00:05\",\"service_latency\":{\"sampling\":50,\"results\":\"results.subject\"},\"account_token_position\":2,\"advertise\":true,\"allow_trace\":true,\"description\":\"Test Description\",\"info_url\":\"https://example.com/info\"}";

Assert.Equal(expectedJson, json);

Expand All @@ -54,4 +54,40 @@ public void TestNatsExportSerializationDeserialization()
Assert.Equal(natsExport.Description, deserializedNatsExport.Description);
Assert.Equal(natsExport.InfoUrl, deserializedNatsExport.InfoUrl);
}

[Theory]
[InlineData(NatsExportType.Unknown, "unknown")]
[InlineData(NatsExportType.Stream, "stream")]
[InlineData(NatsExportType.Service, "service")]
public void TestExportTypeSerializationDeserialization(NatsExportType type, string jsonString)
{
var export = new NatsExport { Type = type };

string json = JsonSerializer.Serialize(export);

string expectedJson = type == NatsExportType.Unknown ? "{}" : $"{{\"type\":\"{jsonString}\"}}";

Assert.Equal(expectedJson, json);

var deserializedNatsExportType = JsonSerializer.Deserialize<NatsExport>(json);

Assert.Equal(type, deserializedNatsExportType.Type);
}

[Fact]
public void TestExportTypeExceptionsInSerializationDeserialization()
{
var export = new NatsExport { Type = (NatsExportType)42 };
Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(export));

string json = "{\"type\":\"not-a-valid-value\"}";
Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<NatsExport>(json));

string json2 = "{\"type\":\"unknown\"}";
var deserializedNatsExportType = JsonSerializer.Deserialize<NatsExport>(json2);
Assert.Equal(NatsExportType.Unknown, deserializedNatsExportType.Type);

string json3 = "{\"type\":1}";
Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<NatsExport>(json3));
}
}
4 changes: 2 additions & 2 deletions NATS.Jwt.Tests/Models/NatsImportTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ public void TestNatsImportSerializationDeserialization()
Account = "ACC123",
Token = "TOKEN456",
LocalSubject = "local.subject",
Type = 1,
Type = NatsExportType.Service,
Share = true,
AllowTrace = true,
To = "to.subject",
};

string json = JsonSerializer.Serialize(natsImport);

string expectedJson = "{\"name\":\"TestImport\",\"subject\":\"test.subject\",\"account\":\"ACC123\",\"token\":\"TOKEN456\",\"to\":\"to.subject\",\"local_subject\":\"local.subject\",\"type\":1,\"share\":true,\"allow_trace\":true}";
string expectedJson = "{\"name\":\"TestImport\",\"subject\":\"test.subject\",\"account\":\"ACC123\",\"token\":\"TOKEN456\",\"to\":\"to.subject\",\"local_subject\":\"local.subject\",\"type\":\"service\",\"share\":true,\"allow_trace\":true}";

Assert.Equal(expectedJson, json);

Expand Down
69 changes: 68 additions & 1 deletion NATS.Jwt.Tests/NatsJwtTests.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using NATS.Jwt.Models;
using NATS.NKeys;
using Xunit;
using Xunit.Abstractions;

namespace NATS.Jwt.Tests;

public class NatsJwtTests
public class NatsJwtTests(ITestOutputHelper output)
{
private readonly NatsJwt _natsJwt = new();

Expand Down Expand Up @@ -269,4 +271,69 @@ public void TestNewAccountClaims()
Assert.Equal(subject, claims.Subject);
Assert.NotNull(claims.Account);
}

[Fact]
public void TestMultipleExports()
{
var jwtUtils = new NatsJwt();

var operatorSigningKey = KeyPair.CreatePair(PrefixByte.Operator);
var systemAccountKeyPair = KeyPair.CreatePair(PrefixByte.Account);

// Create System Account
var systemAccountClaims = jwtUtils.NewAccountClaims(systemAccountKeyPair.GetPublicKey());
systemAccountClaims.Name = "SYS";
systemAccountClaims.Account.Exports =
[
new()
{
Name = "account-monitoring-services",
Subject = "$SYS.REQ.ACCOUNT.*.*",
AccountTokenPosition = 4,
Type = NatsExportType.Service,
ResponseType = "Stream",
Description = "Request account specific monitoring services for: SUBSZ, CONNZ, LEAFZ, JSZ and INFO",
InfoUrl = "https://docs.nats.io/nats-server/configuration/sys_accounts",
},
new()
{
Name = "account-monitoring-streams",
Subject = "$SYS.ACCOUNT.*.>",
AccountTokenPosition = 3,
Type = NatsExportType.Service,
Description = "Account specific monitoring stream",
InfoUrl = "https://docs.nats.io/nats-server/configuration/sys_accounts",
},
];
systemAccountClaims.Account.Imports =
[
new NatsImport
{
Name = "account-monitoring",
Subject = "$SYS.ACCOUNT.*.*",
Account = systemAccountKeyPair.GetPublicKey(),
Type = NatsExportType.Service,
LocalSubject = "account-monitoring",
},
new NatsImport
{
Name = "account-monitoring2",
Subject = "$SYS.ACCOUNT.*.>",
Account = systemAccountKeyPair.GetPublicKey(),
Type = NatsExportType.Service,
LocalSubject = "account-monitoring2",
},
];

var jwt = jwtUtils.EncodeAccountClaims(systemAccountClaims, operatorSigningKey);
var payload = EncodingUtils.FromBase64UrlEncoded(jwt.Split('.')[1]);
var json = JsonSerializer.Deserialize<JsonNode>(payload);

// Verify the exports are sorted by name
Assert.Equal("account-monitoring-streams", json["nats"]["exports"][0]["name"].GetValue<string>());
Assert.Equal("account-monitoring-services", json["nats"]["exports"][1]["name"].GetValue<string>());

string jsonStr = JsonSerializer.Serialize(json, new JsonSerializerOptions { WriteIndented = true });
output.WriteLine(jsonStr);
}
}
18 changes: 18 additions & 0 deletions NATS.Jwt/Internal/NatsExportComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) The NATS Authors.
// Licensed under the Apache License, Version 2.0.

using System;
using System.Collections.Generic;
using NATS.Jwt.Models;

namespace NATS.Jwt.Internal;

/// <inheritdoc />
internal class NatsExportComparer : IComparer<NatsExport>
{
/// <inheritdoc />
public int Compare(NatsExport? x, NatsExport? y)
{
return string.Compare(x?.Subject, y?.Subject, StringComparison.Ordinal);
}
}
18 changes: 18 additions & 0 deletions NATS.Jwt/Internal/NatsImportComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) The NATS Authors.
// Licensed under the Apache License, Version 2.0.

using System;
using System.Collections.Generic;
using NATS.Jwt.Models;

namespace NATS.Jwt.Internal;

/// <inheritdoc />
internal class NatsImportComparer : IComparer<NatsImport>
{
/// <inheritdoc />
public int Compare(NatsImport? x, NatsImport? y)
{
return string.Compare(x?.Subject, y?.Subject, StringComparison.Ordinal);
}
}
62 changes: 62 additions & 0 deletions NATS.Jwt/Internal/NatsJsonStringEnumConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) The NATS Authors.
// Licensed under the Apache License, Version 2.0.

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using NATS.Jwt.Models;

namespace NATS.Jwt.Internal;

/// <inheritdoc />
internal class NatsJsonStringEnumConverter<TEnum> : JsonConverter<TEnum>
where TEnum : struct, Enum
{
/// <inheritdoc />
public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.String)
{
throw new InvalidOperationException();
}

var stringValue = reader.GetString();

if (typeToConvert == typeof(NatsExportType))
{
switch (stringValue)
{
case "unknown":
return (TEnum)(object)NatsExportType.Unknown;
case "stream":
return (TEnum)(object)NatsExportType.Stream;
case "service":
return (TEnum)(object)NatsExportType.Service;
}
}

throw new InvalidOperationException($"Reading unknown enum type {typeToConvert.Name} or value {stringValue}");
}

/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
{
if (value is NatsExportType consumerConfigDeliverPolicy)
{
switch (consumerConfigDeliverPolicy)
{
case NatsExportType.Unknown:
writer.WriteStringValue("unknown");
return;
case NatsExportType.Stream:
writer.WriteStringValue("stream");
return;
case NatsExportType.Service:
writer.WriteStringValue("service");
return;
}
}

throw new InvalidOperationException($"Writing unknown enum value {value.GetType().Name}.{value}");
}
}
4 changes: 3 additions & 1 deletion NATS.Jwt/Models/NatsExport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using NATS.Jwt.Internal;

namespace NATS.Jwt.Models;

Expand Down Expand Up @@ -31,7 +32,8 @@ public record NatsExport
/// </summary>
[JsonPropertyName("type")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public int Type { get; set; }
[JsonConverter(typeof(NatsJsonStringEnumConverter<NatsExportType>))]
public NatsExportType Type { get; set; }

/// <summary>
/// Gets or sets a value indicating whether a token is required.
Expand Down
25 changes: 25 additions & 0 deletions NATS.Jwt/Models/NatsExportType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) The NATS Authors.
// Licensed under the Apache License, Version 2.0.

namespace NATS.Jwt.Models;

/// <summary>
/// Defines the type of export.
/// </summary>
public enum NatsExportType
{
/// <summary>
/// Unknown is used if we don't know the type.
/// </summary>
Unknown = 0,

/// <summary>
/// Stream defines the type field value for a stream "stream".
/// </summary>
Stream = 1,

/// <summary>
/// Service defines the type field value for a service "service".
/// </summary>
Service = 2,
}
4 changes: 3 additions & 1 deletion NATS.Jwt/Models/NatsImport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Text.Json.Serialization;
using NATS.Jwt.Internal;

namespace NATS.Jwt.Models;

Expand Down Expand Up @@ -70,7 +71,8 @@ public record NatsImport
/// </summary>
[JsonPropertyName("type")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public int Type { get; set; }
[JsonConverter(typeof(NatsJsonStringEnumConverter<NatsExportType>))]
public NatsExportType Type { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the import is shared.
Expand Down
8 changes: 6 additions & 2 deletions NATS.Jwt/NatsJwt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ public class NatsJwt
/// </summary>
public const string AlgorithmNkey = "ed25519-nkey";

private static readonly NatsExportComparer ExportComparer = new();

private static readonly NatsImportComparer ImportComparer = new();

/// <summary>
/// Formats the user configuration.
/// </summary>
Expand Down Expand Up @@ -263,8 +267,8 @@ public string EncodeUserClaims(NatsUserClaims userClaims, KeyPair keyPair, DateT
public string EncodeAccountClaims(NatsAccountClaims accountClaims, KeyPair keyPair, DateTimeOffset? issuedAt = null)
{
SetVersion(accountClaims.Account, AccountClaim);
accountClaims.Account.Imports?.Sort();
accountClaims.Account.Exports?.Sort();
accountClaims.Account.Imports?.Sort(ImportComparer);
accountClaims.Account.Exports?.Sort(ExportComparer);
return DoEncode(NatsJwtHeader, keyPair, accountClaims, JsonContext.Default.NatsAccountClaims, issuedAt);
}

Expand Down
8 changes: 6 additions & 2 deletions NATS.Jwt/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,12 @@ NATS.Jwt.Models.NatsExport.Subject.get -> string!
NATS.Jwt.Models.NatsExport.Subject.set -> void
NATS.Jwt.Models.NatsExport.TokenReq.get -> bool
NATS.Jwt.Models.NatsExport.TokenReq.set -> void
NATS.Jwt.Models.NatsExport.Type.get -> int
NATS.Jwt.Models.NatsExport.Type.get -> NATS.Jwt.Models.NatsExportType
NATS.Jwt.Models.NatsExport.Type.set -> void
NATS.Jwt.Models.NatsExportType
NATS.Jwt.Models.NatsExportType.Service = 2 -> NATS.Jwt.Models.NatsExportType
NATS.Jwt.Models.NatsExportType.Stream = 1 -> NATS.Jwt.Models.NatsExportType
NATS.Jwt.Models.NatsExportType.Unknown = 0 -> NATS.Jwt.Models.NatsExportType
NATS.Jwt.Models.NatsExternalAuthorization
NATS.Jwt.Models.NatsExternalAuthorization.AllowedAccounts.get -> System.Collections.Generic.List<string!>!
NATS.Jwt.Models.NatsExternalAuthorization.AllowedAccounts.set -> void
Expand Down Expand Up @@ -227,7 +231,7 @@ NATS.Jwt.Models.NatsImport.To.get -> string!
NATS.Jwt.Models.NatsImport.To.set -> void
NATS.Jwt.Models.NatsImport.Token.get -> string!
NATS.Jwt.Models.NatsImport.Token.set -> void
NATS.Jwt.Models.NatsImport.Type.get -> int
NATS.Jwt.Models.NatsImport.Type.get -> NATS.Jwt.Models.NatsExportType
NATS.Jwt.Models.NatsImport.Type.set -> void
NATS.Jwt.Models.NatsMsgTrace
NATS.Jwt.Models.NatsMsgTrace.Destination.get -> string!
Expand Down

0 comments on commit a498767

Please sign in to comment.