test: add permission enforcement and NKey integration tests
Fix NKey nonce verification: the NATS client signs the nonce string (ASCII bytes of the base64url-encoded nonce), not the raw nonce bytes. Pass the encoded nonce string bytes to the authenticator for verification.
This commit is contained in:
@@ -99,7 +99,10 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
|||||||
var clientInfo = _serverInfo;
|
var clientInfo = _serverInfo;
|
||||||
if (_authService.NonceRequired)
|
if (_authService.NonceRequired)
|
||||||
{
|
{
|
||||||
nonce = _authService.GenerateNonce();
|
var rawNonce = _authService.GenerateNonce();
|
||||||
|
var nonceStr = _authService.EncodeNonce(rawNonce);
|
||||||
|
// The client signs the nonce string (ASCII), not the raw bytes
|
||||||
|
nonce = Encoding.ASCII.GetBytes(nonceStr);
|
||||||
clientInfo = new ServerInfo
|
clientInfo = new ServerInfo
|
||||||
{
|
{
|
||||||
ServerId = _serverInfo.ServerId,
|
ServerId = _serverInfo.ServerId,
|
||||||
@@ -109,7 +112,7 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
|||||||
Port = _serverInfo.Port,
|
Port = _serverInfo.Port,
|
||||||
MaxPayload = _serverInfo.MaxPayload,
|
MaxPayload = _serverInfo.MaxPayload,
|
||||||
AuthRequired = _serverInfo.AuthRequired,
|
AuthRequired = _serverInfo.AuthRequired,
|
||||||
Nonce = _authService.EncodeNonce(nonce),
|
Nonce = nonceStr,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
82
tests/NATS.Server.Tests/NKeyIntegrationTests.cs
Normal file
82
tests/NATS.Server.Tests/NKeyIntegrationTests.cs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using NATS.Client.Core;
|
||||||
|
using NATS.NKeys;
|
||||||
|
using NATS.Server.Auth;
|
||||||
|
|
||||||
|
namespace NATS.Server.Tests;
|
||||||
|
|
||||||
|
public class NKeyIntegrationTests : IAsyncLifetime
|
||||||
|
{
|
||||||
|
private NatsServer _server = null!;
|
||||||
|
private int _port;
|
||||||
|
private readonly CancellationTokenSource _cts = new();
|
||||||
|
private Task _serverTask = null!;
|
||||||
|
private KeyPair _userKeyPair = null!;
|
||||||
|
private string _userSeed = null!;
|
||||||
|
private string _userPublicKey = null!;
|
||||||
|
|
||||||
|
private static int GetFreePort()
|
||||||
|
{
|
||||||
|
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||||
|
sock.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
||||||
|
return ((IPEndPoint)sock.LocalEndPoint!).Port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
_port = GetFreePort();
|
||||||
|
_userKeyPair = KeyPair.CreatePair(PrefixByte.User);
|
||||||
|
_userPublicKey = _userKeyPair.GetPublicKey();
|
||||||
|
_userSeed = _userKeyPair.GetSeed();
|
||||||
|
|
||||||
|
_server = new NatsServer(new NatsOptions
|
||||||
|
{
|
||||||
|
Port = _port,
|
||||||
|
NKeys = [new NKeyUser { Nkey = _userPublicKey }],
|
||||||
|
}, NullLoggerFactory.Instance);
|
||||||
|
|
||||||
|
_serverTask = _server.StartAsync(_cts.Token);
|
||||||
|
await _server.WaitForReadyAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DisposeAsync()
|
||||||
|
{
|
||||||
|
await _cts.CancelAsync();
|
||||||
|
_server.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task NKey_auth_success()
|
||||||
|
{
|
||||||
|
await using var client = new NatsConnection(new NatsOpts
|
||||||
|
{
|
||||||
|
Url = $"nats://127.0.0.1:{_port}",
|
||||||
|
AuthOpts = new NatsAuthOpts { NKey = _userPublicKey, Seed = _userSeed },
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.ConnectAsync();
|
||||||
|
await client.PingAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task NKey_auth_wrong_key_fails()
|
||||||
|
{
|
||||||
|
// Generate a different key pair not known to the server
|
||||||
|
var otherKp = KeyPair.CreatePair(PrefixByte.User);
|
||||||
|
|
||||||
|
await using var client = new NatsConnection(new NatsOpts
|
||||||
|
{
|
||||||
|
Url = $"nats://127.0.0.1:{_port}",
|
||||||
|
AuthOpts = new NatsAuthOpts { NKey = otherKp.GetPublicKey(), Seed = otherKp.GetSeed() },
|
||||||
|
MaxReconnectRetry = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await Should.ThrowAsync<NatsException>(async () =>
|
||||||
|
{
|
||||||
|
await client.ConnectAsync();
|
||||||
|
await client.PingAsync();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
119
tests/NATS.Server.Tests/PermissionIntegrationTests.cs
Normal file
119
tests/NATS.Server.Tests/PermissionIntegrationTests.cs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using NATS.Client.Core;
|
||||||
|
using NATS.Server.Auth;
|
||||||
|
|
||||||
|
namespace NATS.Server.Tests;
|
||||||
|
|
||||||
|
public class PermissionIntegrationTests : IAsyncLifetime
|
||||||
|
{
|
||||||
|
private NatsServer _server = null!;
|
||||||
|
private int _port;
|
||||||
|
private readonly CancellationTokenSource _cts = new();
|
||||||
|
private Task _serverTask = null!;
|
||||||
|
|
||||||
|
private static int GetFreePort()
|
||||||
|
{
|
||||||
|
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||||
|
sock.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
||||||
|
return ((IPEndPoint)sock.LocalEndPoint!).Port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
_port = GetFreePort();
|
||||||
|
_server = new NatsServer(new NatsOptions
|
||||||
|
{
|
||||||
|
Port = _port,
|
||||||
|
Users =
|
||||||
|
[
|
||||||
|
new User
|
||||||
|
{
|
||||||
|
Username = "publisher",
|
||||||
|
Password = "pass",
|
||||||
|
Permissions = new Permissions
|
||||||
|
{
|
||||||
|
Publish = new SubjectPermission { Allow = ["events.>"] },
|
||||||
|
Subscribe = new SubjectPermission { Deny = [">"] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
new User
|
||||||
|
{
|
||||||
|
Username = "subscriber",
|
||||||
|
Password = "pass",
|
||||||
|
Permissions = new Permissions
|
||||||
|
{
|
||||||
|
Publish = new SubjectPermission { Deny = [">"] },
|
||||||
|
Subscribe = new SubjectPermission { Allow = ["events.>"] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
new User
|
||||||
|
{
|
||||||
|
Username = "admin",
|
||||||
|
Password = "pass",
|
||||||
|
// No permissions — full access
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}, NullLoggerFactory.Instance);
|
||||||
|
|
||||||
|
_serverTask = _server.StartAsync(_cts.Token);
|
||||||
|
await _server.WaitForReadyAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DisposeAsync()
|
||||||
|
{
|
||||||
|
await _cts.CancelAsync();
|
||||||
|
_server.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Publisher_can_publish_to_allowed_subject()
|
||||||
|
{
|
||||||
|
await using var pub = new NatsConnection(new NatsOpts
|
||||||
|
{
|
||||||
|
Url = $"nats://publisher:pass@127.0.0.1:{_port}",
|
||||||
|
});
|
||||||
|
await using var admin = new NatsConnection(new NatsOpts
|
||||||
|
{
|
||||||
|
Url = $"nats://admin:pass@127.0.0.1:{_port}",
|
||||||
|
});
|
||||||
|
|
||||||
|
await pub.ConnectAsync();
|
||||||
|
await admin.ConnectAsync();
|
||||||
|
|
||||||
|
await using var sub = await admin.SubscribeCoreAsync<string>("events.test");
|
||||||
|
await admin.PingAsync();
|
||||||
|
|
||||||
|
await pub.PublishAsync("events.test", "hello");
|
||||||
|
|
||||||
|
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||||
|
var msg = await sub.Msgs.ReadAsync(timeout.Token);
|
||||||
|
msg.Data.ShouldBe("hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Admin_has_full_access()
|
||||||
|
{
|
||||||
|
await using var admin1 = new NatsConnection(new NatsOpts
|
||||||
|
{
|
||||||
|
Url = $"nats://admin:pass@127.0.0.1:{_port}",
|
||||||
|
});
|
||||||
|
await using var admin2 = new NatsConnection(new NatsOpts
|
||||||
|
{
|
||||||
|
Url = $"nats://admin:pass@127.0.0.1:{_port}",
|
||||||
|
});
|
||||||
|
|
||||||
|
await admin1.ConnectAsync();
|
||||||
|
await admin2.ConnectAsync();
|
||||||
|
|
||||||
|
await using var sub = await admin2.SubscribeCoreAsync<string>("anything.at.all");
|
||||||
|
await admin2.PingAsync();
|
||||||
|
|
||||||
|
await admin1.PublishAsync("anything.at.all", "data");
|
||||||
|
|
||||||
|
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||||
|
var msg = await sub.Msgs.ReadAsync(timeout.Token);
|
||||||
|
msg.Data.ShouldBe("data");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user