feat(batch19): implement activation, issuer, and external auth account methods
This commit is contained in:
@@ -3182,6 +3182,321 @@ public sealed class Account : INatsAccount
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Re-evaluates import validity when an activation token expiration timer fires.
|
||||||
|
/// Mirrors Go <c>(a *Account) activationExpired(...)</c>.
|
||||||
|
/// </summary>
|
||||||
|
internal void ActivationExpired(Account exportAccount, string subject, object? kind)
|
||||||
|
{
|
||||||
|
var normalizedKind = NormalizeExportKind(kind);
|
||||||
|
if (string.Equals(normalizedKind, "stream", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
StreamActivationExpired(exportAccount, subject);
|
||||||
|
}
|
||||||
|
else if (string.Equals(normalizedKind, "service", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
ServiceActivationExpired(exportAccount, subject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates an import activation claim/token.
|
||||||
|
/// Mirrors Go <c>(a *Account) checkActivation(...)</c>.
|
||||||
|
/// </summary>
|
||||||
|
internal bool CheckActivation(Account importAccount, object? claim, ExportAuth? exportAuth, bool expirationTimer)
|
||||||
|
{
|
||||||
|
if (claim == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!TryReadStringMember(claim, "Token", out var token) || string.IsNullOrWhiteSpace(token))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!TryDecodeJwtPayload(token, out var activationPayload))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!IsIssuerClaimTrusted(activationPayload))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (TryReadLongMember(activationPayload, "exp", out var expires) && expires > 0)
|
||||||
|
{
|
||||||
|
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
if (expires <= now)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (expirationTimer)
|
||||||
|
{
|
||||||
|
var delay = TimeSpan.FromSeconds(expires - now);
|
||||||
|
string importSubject = ReadActivationImportSubject(activationPayload);
|
||||||
|
object? claimType = TryReadMember(claim, "Type", out var typeValue) ? typeValue : null;
|
||||||
|
|
||||||
|
_ = new Timer(
|
||||||
|
_ => importAccount.ActivationExpired(this, importSubject, claimType),
|
||||||
|
null,
|
||||||
|
delay,
|
||||||
|
Timeout.InfiniteTimeSpan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportAuth == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
string subject = TryReadStringMember(activationPayload, "sub", out var sub) ? sub : string.Empty;
|
||||||
|
long issuedAt = TryReadLongMember(activationPayload, "iat", out var iat) ? iat : 0;
|
||||||
|
return !IsRevoked(exportAuth.ActivationsRevoked, subject, issuedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true when activation issuer details are trusted for this account.
|
||||||
|
/// Mirrors Go <c>(a *Account) isIssuerClaimTrusted(...)</c>.
|
||||||
|
/// </summary>
|
||||||
|
internal bool IsIssuerClaimTrusted(object? claims)
|
||||||
|
{
|
||||||
|
if (claims == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
string issuerAccount =
|
||||||
|
TryReadStringMember(claims, "IssuerAccount", out var ia) ? ia :
|
||||||
|
TryReadStringMember(claims, "issuer_account", out var iaAlt) ? iaAlt :
|
||||||
|
string.Empty;
|
||||||
|
|
||||||
|
// If issuer-account is omitted, issuer defaults to the account itself.
|
||||||
|
if (string.IsNullOrEmpty(issuerAccount))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!string.Equals(Name, issuerAccount, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
if (Server is NatsServer server)
|
||||||
|
{
|
||||||
|
string importSubject = ReadActivationImportSubject(claims);
|
||||||
|
string importType = TryReadStringMember(claims, "import_type", out var it) ? it : string.Empty;
|
||||||
|
server.Errorf(
|
||||||
|
"Invalid issuer account {0} in activation claim (subject: {1} - type: {2}) for account {3}",
|
||||||
|
issuerAccount,
|
||||||
|
importSubject,
|
||||||
|
importType,
|
||||||
|
Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string issuer =
|
||||||
|
TryReadStringMember(claims, "Issuer", out var issuerValue) ? issuerValue :
|
||||||
|
TryReadStringMember(claims, "iss", out var issValue) ? issValue :
|
||||||
|
string.Empty;
|
||||||
|
|
||||||
|
_mu.EnterReadLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
(_, var ok) = HasIssuerNoLock(issuer);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mu.ExitReadLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether another account is approved to import this service export.
|
||||||
|
/// Mirrors Go <c>(a *Account) checkServiceImportAuthorized(...)</c>.
|
||||||
|
/// </summary>
|
||||||
|
internal bool CheckServiceImportAuthorized(Account account, string subject, object? importClaim)
|
||||||
|
{
|
||||||
|
_mu.EnterReadLock();
|
||||||
|
try { return CheckServiceImportAuthorizedNoLock(account, subject, importClaim); }
|
||||||
|
finally { _mu.ExitReadLock(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lock-free helper for service import authorization checks.
|
||||||
|
/// Mirrors Go <c>(a *Account) checkServiceImportAuthorizedNoLock(...)</c>.
|
||||||
|
/// </summary>
|
||||||
|
internal bool CheckServiceImportAuthorizedNoLock(Account account, string subject, object? importClaim)
|
||||||
|
{
|
||||||
|
if (Exports.Services == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return CheckServiceExportApproved(account, subject, importClaim);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns whether bearer tokens should be rejected for this account.
|
||||||
|
/// Mirrors Go <c>(a *Account) failBearer() bool</c>.
|
||||||
|
/// </summary>
|
||||||
|
internal bool FailBearer()
|
||||||
|
{
|
||||||
|
_mu.EnterReadLock();
|
||||||
|
try { return DisallowBearer; }
|
||||||
|
finally { _mu.ExitReadLock(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates expiration state/timer from claim data.
|
||||||
|
/// Mirrors Go <c>(a *Account) checkExpiration(...)</c>.
|
||||||
|
/// </summary>
|
||||||
|
internal void CheckExpiration(object? claimsData)
|
||||||
|
{
|
||||||
|
long expires =
|
||||||
|
claimsData != null && TryReadLongMember(claimsData, "Expires", out var exp) ? exp :
|
||||||
|
claimsData != null && TryReadLongMember(claimsData, "exp", out var expUnix) ? expUnix :
|
||||||
|
0;
|
||||||
|
|
||||||
|
_mu.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ClearExpirationTimer();
|
||||||
|
|
||||||
|
if (expires == 0)
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _expired, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
if (expires <= now)
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _expired, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetExpirationTimer(TimeSpan.FromSeconds(expires - now));
|
||||||
|
Interlocked.Exchange(ref _expired, 0);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mu.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns signer scope for issuer, if present.
|
||||||
|
/// Mirrors Go <c>(a *Account) hasIssuer(...)</c>.
|
||||||
|
/// </summary>
|
||||||
|
internal (object? Scope, bool Ok) HasIssuer(string issuer)
|
||||||
|
{
|
||||||
|
_mu.EnterReadLock();
|
||||||
|
try { return HasIssuerNoLock(issuer); }
|
||||||
|
finally { _mu.ExitReadLock(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lock-free signer lookup.
|
||||||
|
/// Mirrors Go <c>(a *Account) hasIssuerNoLock(...)</c>.
|
||||||
|
/// </summary>
|
||||||
|
internal (object? Scope, bool Ok) HasIssuerNoLock(string issuer)
|
||||||
|
{
|
||||||
|
if (SigningKeys == null || string.IsNullOrEmpty(issuer))
|
||||||
|
return (null, false);
|
||||||
|
|
||||||
|
return SigningKeys.TryGetValue(issuer, out var scope)
|
||||||
|
? (scope, true)
|
||||||
|
: (null, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the leaf-node loop-detection subject.
|
||||||
|
/// Mirrors Go <c>(a *Account) getLDSubject() string</c>.
|
||||||
|
/// </summary>
|
||||||
|
internal string GetLDSubject()
|
||||||
|
{
|
||||||
|
_mu.EnterReadLock();
|
||||||
|
try { return LoopDetectionSubject; }
|
||||||
|
finally { _mu.ExitReadLock(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns account label used in trace output.
|
||||||
|
/// Mirrors Go <c>(a *Account) traceLabel() string</c>.
|
||||||
|
/// </summary>
|
||||||
|
internal string TraceLabel()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(NameTag))
|
||||||
|
return Name;
|
||||||
|
return $"{Name}/{NameTag}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true when external auth is configured.
|
||||||
|
/// Mirrors Go <c>(a *Account) hasExternalAuth() bool</c>.
|
||||||
|
/// </summary>
|
||||||
|
internal bool HasExternalAuth()
|
||||||
|
{
|
||||||
|
_mu.EnterReadLock();
|
||||||
|
try { return ExternalAuth != null; }
|
||||||
|
finally { _mu.ExitReadLock(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true when <paramref name="userId"/> is configured as an external-auth user.
|
||||||
|
/// Mirrors Go <c>(a *Account) isExternalAuthUser(userID string) bool</c>.
|
||||||
|
/// </summary>
|
||||||
|
internal bool IsExternalAuthUser(string userId)
|
||||||
|
{
|
||||||
|
_mu.EnterReadLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var authUser in ReadStringListMember(ExternalAuth, "AuthUsers", "auth_users"))
|
||||||
|
{
|
||||||
|
if (string.Equals(userId, authUser, StringComparison.Ordinal))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mu.ExitReadLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns configured external-auth xkey, or empty when unset.
|
||||||
|
/// Mirrors Go <c>(a *Account) externalAuthXKey() string</c>.
|
||||||
|
/// </summary>
|
||||||
|
internal string ExternalAuthXKey()
|
||||||
|
{
|
||||||
|
_mu.EnterReadLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (TryReadStringMember(ExternalAuth, "XKey", out var xkey) && !string.IsNullOrEmpty(xkey))
|
||||||
|
return xkey;
|
||||||
|
if (TryReadStringMember(ExternalAuth, "xkey", out var xkeyAlt) && !string.IsNullOrEmpty(xkeyAlt))
|
||||||
|
return xkeyAlt;
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mu.ExitReadLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns whether external auth allows account switching to <paramref name="account"/>.
|
||||||
|
/// Mirrors Go <c>(a *Account) isAllowedAcount(acc string) bool</c>.
|
||||||
|
/// </summary>
|
||||||
|
internal bool IsAllowedAcount(string account)
|
||||||
|
{
|
||||||
|
_mu.EnterReadLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var allowed = ReadStringListMember(ExternalAuth, "AllowedAccounts", "allowed_accounts");
|
||||||
|
if (allowed.Count == 1 && string.Equals(allowed[0], "*", StringComparison.Ordinal))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
foreach (var candidate in allowed)
|
||||||
|
{
|
||||||
|
if (string.Equals(candidate, account, StringComparison.Ordinal))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mu.ExitReadLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Export checks
|
// Export checks
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -3877,6 +4192,230 @@ public sealed class Account : INatsAccount
|
|||||||
return DateTime.UnixEpoch.AddTicks(unixNanos / 100L);
|
return DateTime.UnixEpoch.AddTicks(unixNanos / 100L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryDecodeJwtPayload(string token, out JsonElement payload)
|
||||||
|
{
|
||||||
|
payload = default;
|
||||||
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var parts = token.Split('.');
|
||||||
|
if (parts.Length < 2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
string base64 = parts[1]
|
||||||
|
.Replace("-", "+", StringComparison.Ordinal)
|
||||||
|
.Replace("_", "/", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
int mod = base64.Length % 4;
|
||||||
|
if (mod > 0)
|
||||||
|
base64 = base64.PadRight(base64.Length + (4 - mod), '=');
|
||||||
|
|
||||||
|
byte[] bytes;
|
||||||
|
try { bytes = Convert.FromBase64String(base64); }
|
||||||
|
catch { return false; }
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(bytes);
|
||||||
|
payload = doc.RootElement.Clone();
|
||||||
|
return payload.ValueKind == JsonValueKind.Object;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadMember(object source, string name, out object? value)
|
||||||
|
{
|
||||||
|
value = null;
|
||||||
|
if (source == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (source is JsonElement element)
|
||||||
|
{
|
||||||
|
if (element.ValueKind != JsonValueKind.Object)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
foreach (var property in element.EnumerateObject())
|
||||||
|
{
|
||||||
|
if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
value = property.Value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source is IDictionary<string, object?> dictionary &&
|
||||||
|
dictionary.TryGetValue(name, out var dictionaryValue))
|
||||||
|
{
|
||||||
|
value = dictionaryValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source is IDictionary<string, string> stringDictionary &&
|
||||||
|
stringDictionary.TryGetValue(name, out var stringDictionaryValue))
|
||||||
|
{
|
||||||
|
value = stringDictionaryValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var propertyInfo = source
|
||||||
|
.GetType()
|
||||||
|
.GetProperty(name, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.IgnoreCase);
|
||||||
|
if (propertyInfo == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
value = propertyInfo.GetValue(source);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadStringMember(object? source, string name, out string value)
|
||||||
|
{
|
||||||
|
value = string.Empty;
|
||||||
|
if (source == null || !TryReadMember(source, name, out var member))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (member is JsonElement element)
|
||||||
|
{
|
||||||
|
if (element.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
value = element.GetString() ?? string.Empty;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.ValueKind == JsonValueKind.Number)
|
||||||
|
{
|
||||||
|
value = element.ToString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = member?.ToString() ?? string.Empty;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadLongMember(object source, string name, out long value)
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
if (!TryReadMember(source, name, out var member))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (member is JsonElement element)
|
||||||
|
{
|
||||||
|
if (element.ValueKind == JsonValueKind.Number)
|
||||||
|
return element.TryGetInt64(out value);
|
||||||
|
|
||||||
|
if (element.ValueKind == JsonValueKind.String)
|
||||||
|
return long.TryParse(element.GetString(), out value);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (member)
|
||||||
|
{
|
||||||
|
case byte b:
|
||||||
|
value = b;
|
||||||
|
return true;
|
||||||
|
case sbyte sb:
|
||||||
|
value = sb;
|
||||||
|
return true;
|
||||||
|
case short s:
|
||||||
|
value = s;
|
||||||
|
return true;
|
||||||
|
case ushort us:
|
||||||
|
value = us;
|
||||||
|
return true;
|
||||||
|
case int i:
|
||||||
|
value = i;
|
||||||
|
return true;
|
||||||
|
case uint ui:
|
||||||
|
value = ui;
|
||||||
|
return true;
|
||||||
|
case long l:
|
||||||
|
value = l;
|
||||||
|
return true;
|
||||||
|
case ulong ul when ul <= long.MaxValue:
|
||||||
|
value = (long)ul;
|
||||||
|
return true;
|
||||||
|
case string str:
|
||||||
|
return long.TryParse(str, out value);
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> ReadStringListMember(object? source, params string[] names)
|
||||||
|
{
|
||||||
|
if (source == null)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
foreach (var name in names)
|
||||||
|
{
|
||||||
|
if (!TryReadMember(source, name, out var member) || member == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (member is IEnumerable<string> enumerableStrings)
|
||||||
|
return [.. enumerableStrings];
|
||||||
|
|
||||||
|
if (member is JsonElement element && element.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
var results = new List<string>();
|
||||||
|
foreach (var item in element.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (item.ValueKind == JsonValueKind.String)
|
||||||
|
results.Add(item.GetString() ?? string.Empty);
|
||||||
|
else
|
||||||
|
results.Add(item.ToString());
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (member is IEnumerable<object?> objectEnumerable)
|
||||||
|
{
|
||||||
|
var results = new List<string>();
|
||||||
|
foreach (var item in objectEnumerable)
|
||||||
|
results.Add(item?.ToString() ?? string.Empty);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeExportKind(object? kind)
|
||||||
|
{
|
||||||
|
if (kind is JsonElement element)
|
||||||
|
return element.ToString().Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
return kind?.ToString()?.Trim().ToLowerInvariant() ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadActivationImportSubject(object claimOrPayload)
|
||||||
|
{
|
||||||
|
if (TryReadStringMember(claimOrPayload, "ImportSubject", out var importSubject) && !string.IsNullOrEmpty(importSubject))
|
||||||
|
return importSubject;
|
||||||
|
if (TryReadStringMember(claimOrPayload, "import_subject", out var importSubjectSnake) && !string.IsNullOrEmpty(importSubjectSnake))
|
||||||
|
return importSubjectSnake;
|
||||||
|
|
||||||
|
if (claimOrPayload is JsonElement element &&
|
||||||
|
element.ValueKind == JsonValueKind.Object &&
|
||||||
|
element.TryGetProperty("nats", out var natsObj) &&
|
||||||
|
natsObj.ValueKind == JsonValueKind.Object &&
|
||||||
|
natsObj.TryGetProperty("import_subject", out var natsImportSubject) &&
|
||||||
|
natsImportSubject.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return natsImportSubject.GetString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tokenises a subject string into an array, using the same split logic
|
/// Tokenises a subject string into an array, using the same split logic
|
||||||
/// as <c>btsep</c>-based tokenisation in the Go source.
|
/// as <c>btsep</c>-based tokenisation in the Go source.
|
||||||
@@ -3923,9 +4462,8 @@ public sealed class Account : INatsAccount
|
|||||||
/// Checks whether <paramref name="account"/> is authorised to use
|
/// Checks whether <paramref name="account"/> is authorised to use
|
||||||
/// <paramref name="ea"/> (either via explicit approval or token requirement).
|
/// <paramref name="ea"/> (either via explicit approval or token requirement).
|
||||||
/// Mirrors Go <c>(a *Account) checkAuth(...) bool</c>.
|
/// Mirrors Go <c>(a *Account) checkAuth(...) bool</c>.
|
||||||
/// TODO: session 11 — full JWT activation check.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static bool CheckAuth(
|
private bool CheckAuth(
|
||||||
ExportAuth ea,
|
ExportAuth ea,
|
||||||
Account account,
|
Account account,
|
||||||
object? imClaim,
|
object? imClaim,
|
||||||
@@ -3936,8 +4474,7 @@ public sealed class Account : INatsAccount
|
|||||||
|
|
||||||
if (ea.TokenRequired)
|
if (ea.TokenRequired)
|
||||||
{
|
{
|
||||||
// TODO: session 11 — validate activation token in imClaim.
|
return CheckActivation(account, imClaim, ea, expirationTimer: true);
|
||||||
return imClaim != null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// No approved list and no token required → public export.
|
// No approved list and no token required → public export.
|
||||||
|
|||||||
BIN
porting.db
BIN
porting.db
Binary file not shown.
Reference in New Issue
Block a user