Extend PermissionLruCache with SetSub/TryGetSub (internal key prefix "S:")
alongside existing PUB API ("P:" prefix, backward-compatible). Add Invalidate()
and Generation property for generation-based cache invalidation. Add
GenerationId/IncrementGeneration to Account for account-level change signalling.
10 new tests in SubPermissionCacheTests cover all paths.
153 lines
4.8 KiB
C#
153 lines
4.8 KiB
C#
namespace NATS.Server.Auth;
|
|
|
|
/// <summary>
|
|
/// Fixed-capacity LRU cache for permission results, supporting both PUB and SUB entries
|
|
/// with generation-based invalidation.
|
|
/// Lock-protected (per-client, low contention).
|
|
/// Reference: Go client.go maxPermCacheSize=128.
|
|
/// </summary>
|
|
public sealed class PermissionLruCache
|
|
{
|
|
private readonly int _capacity;
|
|
private readonly Dictionary<string, LinkedListNode<(string Key, bool Value)>> _map;
|
|
private readonly LinkedList<(string Key, bool Value)> _list = new();
|
|
private readonly object _lock = new();
|
|
|
|
// Generation tracking: _generation is the authoritative counter (bumped by Invalidate),
|
|
// _cacheGeneration is what the cache was last synced to. A mismatch on access triggers a clear.
|
|
private long _generation;
|
|
private long _cacheGeneration;
|
|
|
|
public PermissionLruCache(int capacity = 128)
|
|
{
|
|
_capacity = capacity;
|
|
_map = new Dictionary<string, LinkedListNode<(string Key, bool Value)>>(capacity, StringComparer.Ordinal);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The current generation counter. Incremented on each <see cref="Invalidate"/> call.
|
|
/// </summary>
|
|
public long Generation => Interlocked.Read(ref _generation);
|
|
|
|
/// <summary>
|
|
/// Bumps the generation counter so that the next TryGet or TryGetSub call detects
|
|
/// the staleness and clears all cached entries.
|
|
/// </summary>
|
|
public void Invalidate() => Interlocked.Increment(ref _generation);
|
|
|
|
// Ensures the cache is cleared if Invalidate was called since the last access.
|
|
// Must be called inside _lock.
|
|
private void EnsureFresh()
|
|
{
|
|
var gen = Interlocked.Read(ref _generation);
|
|
if (gen != _cacheGeneration)
|
|
{
|
|
_map.Clear();
|
|
_list.Clear();
|
|
_cacheGeneration = gen;
|
|
}
|
|
}
|
|
|
|
// ── PUB API (backward-compatible) ────────────────────────────────────────
|
|
|
|
/// <summary>Looks up a PUB permission for <paramref name="key"/>.</summary>
|
|
public bool TryGet(string key, out bool value)
|
|
{
|
|
var internalKey = "P:" + key;
|
|
lock (_lock)
|
|
{
|
|
EnsureFresh();
|
|
if (_map.TryGetValue(internalKey, out var node))
|
|
{
|
|
value = node.Value.Value;
|
|
_list.Remove(node);
|
|
_list.AddFirst(node);
|
|
return true;
|
|
}
|
|
|
|
value = default;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>Stores a PUB permission for <paramref name="key"/>.</summary>
|
|
public void Set(string key, bool value)
|
|
{
|
|
var internalKey = "P:" + key;
|
|
lock (_lock)
|
|
{
|
|
EnsureFresh();
|
|
SetInternal(internalKey, value);
|
|
}
|
|
}
|
|
|
|
// ── SUB API ───────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Looks up a SUB permission for <paramref name="subject"/>.</summary>
|
|
public bool TryGetSub(string subject, out bool value)
|
|
{
|
|
var internalKey = "S:" + subject;
|
|
lock (_lock)
|
|
{
|
|
EnsureFresh();
|
|
if (_map.TryGetValue(internalKey, out var node))
|
|
{
|
|
value = node.Value.Value;
|
|
_list.Remove(node);
|
|
_list.AddFirst(node);
|
|
return true;
|
|
}
|
|
|
|
value = default;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>Stores a SUB permission for <paramref name="subject"/>.</summary>
|
|
public void SetSub(string subject, bool allowed)
|
|
{
|
|
var internalKey = "S:" + subject;
|
|
lock (_lock)
|
|
{
|
|
EnsureFresh();
|
|
SetInternal(internalKey, allowed);
|
|
}
|
|
}
|
|
|
|
// ── Shared ────────────────────────────────────────────────────────────────
|
|
|
|
public int Count
|
|
{
|
|
get
|
|
{
|
|
lock (_lock)
|
|
{
|
|
return _map.Count;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Must be called inside _lock.
|
|
private void SetInternal(string internalKey, bool value)
|
|
{
|
|
if (_map.TryGetValue(internalKey, out var existing))
|
|
{
|
|
_list.Remove(existing);
|
|
existing.Value = (internalKey, value);
|
|
_list.AddFirst(existing);
|
|
return;
|
|
}
|
|
|
|
if (_map.Count >= _capacity)
|
|
{
|
|
var last = _list.Last!;
|
|
_map.Remove(last.Value.Key);
|
|
_list.RemoveLast();
|
|
}
|
|
|
|
var node = new LinkedListNode<(string Key, bool Value)>((internalKey, value));
|
|
_list.AddFirst(node);
|
|
_map[internalKey] = node;
|
|
}
|
|
}
|