fix: preserve SDK role identity and continuation authority lifecycle
This commit is contained in:
@@ -87,7 +87,9 @@ internal sealed record HistoryContinuationState(
|
||||
long EmptyFromTicks = 0,
|
||||
long EmptyToTicks = 0,
|
||||
HistoryEndBound? PendingEndBound = null,
|
||||
bool WithinTieCluster = false)
|
||||
bool WithinTieCluster = false,
|
||||
string? BoundNodeKey = null,
|
||||
long AddressSpaceEpoch = -1)
|
||||
{
|
||||
/// <summary>The walk-back hint this cursor carries.</summary>
|
||||
public ReverseWalkHint WalkHint => new(SegmentWidthTicks, EmptyFromTicks, EmptyToTicks);
|
||||
|
||||
@@ -202,6 +202,8 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task StartAsync(StandardServer server, CancellationToken cancellationToken)
|
||||
{
|
||||
if (server is not OtOpcUaSdkServer sdkServer)
|
||||
throw new ArgumentException("The server must support application authentication before its listeners open.", nameof(server));
|
||||
_server = server;
|
||||
// 1.5.378 requires an ITelemetryContext on the ApplicationInstance ctor (the parameterless ctor
|
||||
// is obsolete). DefaultTelemetry.Create wires the SDK's internal logging; an empty builder keeps
|
||||
@@ -213,27 +215,32 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
|
||||
ConfigSectionName = "OtOpcUa",
|
||||
};
|
||||
|
||||
_ = await BuildConfigurationAsync(cancellationToken);
|
||||
await EnsureApplicationCertificateAsync(cancellationToken).ConfigureAwait(false);
|
||||
await _application.StartAsync(server).ConfigureAwait(false);
|
||||
|
||||
AttachUserAuthenticator();
|
||||
PopulateServerArray();
|
||||
try
|
||||
{
|
||||
_ = await BuildConfigurationAsync(cancellationToken);
|
||||
await EnsureApplicationCertificateAsync(cancellationToken).ConfigureAwait(false);
|
||||
AttachUserAuthenticator(sdkServer);
|
||||
await _application.StartAsync(server, cancellationToken).ConfigureAwait(false);
|
||||
PopulateServerArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
await DisposeAsync().ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
|
||||
_logger.LogInformation("OPC UA server started on opc.tcp://{Host}:{Port}",
|
||||
_options.PublicHostname, _options.OpcUaPort);
|
||||
}
|
||||
|
||||
private void AttachUserAuthenticator()
|
||||
private void AttachUserAuthenticator(OtOpcUaSdkServer server)
|
||||
{
|
||||
var registry = _server?.CurrentInstance?.IdentityRegistry
|
||||
?? throw new InvalidOperationException("OPC UA identity registry unavailable after start.");
|
||||
foreach (var type in new[] { UserTokenType.Anonymous, UserTokenType.UserName })
|
||||
{
|
||||
var adapter = new ApplicationTokenAuthenticator(this, type);
|
||||
registry.Register(adapter);
|
||||
_identityAuthenticators.Add(adapter);
|
||||
}
|
||||
server.ConfigureIdentityAuthenticators(_identityAuthenticators);
|
||||
}
|
||||
|
||||
private sealed class ApplicationTokenAuthenticator(OpcUaApplicationHost host, UserTokenType tokenType)
|
||||
@@ -634,22 +641,14 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_server?.CurrentInstance?.IdentityRegistry is { } registry)
|
||||
foreach (var authenticator in _identityAuthenticators) registry.Unregister(authenticator);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "OpcUaApplicationHost: detaching identity authenticators threw");
|
||||
}
|
||||
_identityAuthenticators.Clear();
|
||||
|
||||
// Authenticators remain installed until the SDK closes every listener and disposes its registry.
|
||||
if (_application is { } application)
|
||||
{
|
||||
_application = null;
|
||||
try { await application.DisposeAsync().ConfigureAwait(false); }
|
||||
catch (Exception ex) { _logger.LogWarning(ex, "OpcUaApplicationHost: disposal threw"); }
|
||||
}
|
||||
_identityAuthenticators.Clear();
|
||||
_server = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -698,6 +698,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
private volatile SemaphoreSlim? _historyReadBatchLimiter;
|
||||
private readonly object _historyReadBatchLimiterLock = new();
|
||||
|
||||
private long _historyContinuationEpoch;
|
||||
private volatile IHistoryContinuationStore _historyContinuationStore = new SessionHistoryContinuationStore();
|
||||
|
||||
/// <summary>
|
||||
@@ -2315,6 +2316,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
var refuseArrayOnScalar = _arrayOnScalarRefused.ContainsKey(MapKey(node.NodeId));
|
||||
var range = EvaluateEquipmentWriteRange(indexRange);
|
||||
if (range is not null) return range;
|
||||
if (value.TypeInfo.ValueRank > 1) return StatusCodes.BadTypeMismatch;
|
||||
var structure = EvaluateEquipmentWriteStructure(SdkValueBoundary.ToClr(value), node, refuseArrayOnScalar, out var coerced);
|
||||
if (structure is not null) return structure;
|
||||
value = SdkValueBoundary.FromClr(coerced, node is BaseDataVariableState { DataType: var dt, ValueRank: ValueRanks.OneDimension } && dt == DataTypeIds.Byte);
|
||||
@@ -3688,6 +3690,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
{
|
||||
lock (Lock)
|
||||
{
|
||||
Interlocked.Increment(ref _historyContinuationEpoch);
|
||||
// Review 01 L2 — every monitored item on a node this rebuild discards is told and forgotten BEFORE
|
||||
// the nodes go, so a later create on the same NodeId binds to the re-created node (see
|
||||
// TombstoneMonitoredNode). The shared root survives the rebuild and keeps its items.
|
||||
@@ -3812,6 +3815,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// caller (AddressSpaceApplier) falls back to a full rebuild (resync). Mirrors the per-node
|
||||
// teardown inside RebuildAddressSpace, scoped to this one id.
|
||||
if (!_variables.TryRemove(key, out var variable)) return false;
|
||||
// Conservatively invalidate every outstanding history cursor on topology removal.
|
||||
Interlocked.Increment(ref _historyContinuationEpoch);
|
||||
UnindexChild(key);
|
||||
TombstoneMonitoredNode(variable.NodeId, UtcNow);
|
||||
// 2026-08-15 review I3: sever the INVERSE half of every cross-tree Organizes edge this node holds
|
||||
@@ -3880,6 +3885,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// lingering notifier folder is harmless (RemoveEquipmentSubtree demotes it when the whole
|
||||
// equipment goes).
|
||||
if (!_alarmConditions.TryRemove(key, out var condition)) return false;
|
||||
// Conservatively invalidate every outstanding history cursor on topology removal.
|
||||
Interlocked.Increment(ref _historyContinuationEpoch);
|
||||
UnindexChild(key);
|
||||
TombstoneMonitoredSubtree(condition, UtcNow);
|
||||
// unwire this condition's extra equipment-folder notifiers bidirectionally BEFORE
|
||||
@@ -3908,6 +3915,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
{
|
||||
// Unknown equipment id ⇒ map drift ⇒ false (caller rebuilds).
|
||||
if (!_folders.ContainsKey(equipKey)) return false;
|
||||
// Conservatively invalidate every outstanding history cursor on topology removal.
|
||||
Interlocked.Increment(ref _historyContinuationEpoch);
|
||||
|
||||
// Review 01 P2 — walk the parent→children index the materialise calls maintain instead of
|
||||
// prefix-scanning every map (variables, conditions, folders, notifier folders, event sources and
|
||||
@@ -4868,9 +4877,9 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
if (e is AuditEventState && !IsAuditDeliverableTo(item)) continue;
|
||||
if (scopes is not null && !IsConditionVisibleTo(item, scopes)) continue;
|
||||
if (ServiceResult.IsBad(ValidateEventRolePermissions(item, e))) continue;
|
||||
if (reportingSession is not null
|
||||
if (reportingSession is { IsNull: false } reportingId
|
||||
&& item.Session?.Id is { } own
|
||||
&& !own.Equals(reportingSession))
|
||||
&& !own.Equals(reportingId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -5825,6 +5834,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
IList<ServiceResult> errors,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var historyEpoch = Volatile.Read(ref _historyContinuationEpoch);
|
||||
var inboundCp = nodesToRead[handle.Index].ContinuationPoint;
|
||||
var resumed = inboundCp is { Length: > 0 };
|
||||
// One snapshot of the (hot-swappable) source for the whole read: the capability check and every call must agree.
|
||||
@@ -5844,7 +5854,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// for THIS node only.
|
||||
var state = _historyContinuationStore.TryTake(session, inboundCp.ToArray());
|
||||
// A point is bound to the node that issued it: another node's source name is a miss too.
|
||||
if (state is null || state.Kind != HistoryCursorKind.Events || state.Tagname != (sourceName ?? string.Empty))
|
||||
if (state is null || !HistoryContinuationMatches(state, handle.NodeId, historyEpoch) || state.Kind != HistoryCursorKind.Events || state.Tagname != (sourceName ?? string.Empty))
|
||||
{
|
||||
errors[handle.Index] = StatusCodes.BadContinuationPointInvalid;
|
||||
results[handle.Index] = new SdkHistoryReadResult { StatusCode = StatusCodes.BadContinuationPointInvalid };
|
||||
@@ -6019,7 +6029,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// boundary skip) is exactly as without a filter: a page may come back with fewer events — even none — and a
|
||||
// continuation point, never skipping or repeating one.
|
||||
var historyEvent = ProjectEvents(events, selectClauses, where, filterContext);
|
||||
var outboundCp = pendingCursor is null ? null : _historyContinuationStore.Save(session, pendingCursor);
|
||||
var outboundCp = pendingCursor is null ? null : SaveHistoryContinuation(session, handle.NodeId, historyEpoch, pendingCursor);
|
||||
results[handle.Index] = new SdkHistoryReadResult
|
||||
{
|
||||
// No events ⇒ GoodNoData (the notifier is historized, the window just held no events — or none matched).
|
||||
@@ -6888,6 +6898,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
CancellationToken ct,
|
||||
bool returnBounds = false)
|
||||
{
|
||||
var historyEpoch = Volatile.Read(ref _historyContinuationEpoch);
|
||||
var inboundCp = nodesToRead[handle.Index].ContinuationPoint;
|
||||
|
||||
try
|
||||
@@ -6904,7 +6915,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// Resume read: take the stored cursor. A miss (unknown / evicted / malformed point) ⇒
|
||||
// BadContinuationPointInvalid for THIS node.
|
||||
var state = _historyContinuationStore.TryTake(session, inboundCp.ToArray());
|
||||
if (state is null || state.Kind != HistoryCursorKind.Raw
|
||||
if (state is null || !HistoryContinuationMatches(state, handle.NodeId, historyEpoch) || state.Kind != HistoryCursorKind.Raw
|
||||
|| !TryGetHistorizedTagname(handle.NodeId, out var boundTag) || boundTag != state.Tagname)
|
||||
{
|
||||
errors[handle.Index] = StatusCodes.BadContinuationPointInvalid;
|
||||
@@ -6971,7 +6982,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
return null;
|
||||
}
|
||||
|
||||
return _historyContinuationStore.Save(session, new HistoryContinuationState(
|
||||
return SaveHistoryContinuation(session, handle.NodeId, historyEpoch, new HistoryContinuationState(
|
||||
tagname, previous, endUtc, 0, numValuesPerNode, SegmentWidthTicks: walkHint.WidthTicks,
|
||||
EmptyFromTicks: walkHint.EmptyFromTicks, EmptyToTicks: walkHint.EmptyToTicks,
|
||||
PendingEndBound: HistoryBounds.Carry(endBound!, page)! with { DataExhausted = true }));
|
||||
@@ -7037,7 +7048,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
if (startBound is not null && numValuesPerNode == 1 && anything)
|
||||
{
|
||||
// The page is the start bound alone; the data starts at StartTime on the next page.
|
||||
var boundOnlyCp = _historyContinuationStore.Save(session, new HistoryContinuationState(
|
||||
var boundOnlyCp = SaveHistoryContinuation(session, handle.NodeId, historyEpoch, new HistoryContinuationState(
|
||||
tagname, startUtc, endUtc, 0, numValuesPerNode, SegmentWidthTicks: walkHint.WidthTicks,
|
||||
EmptyFromTicks: walkHint.EmptyFromTicks, EmptyToTicks: walkHint.EmptyToTicks,
|
||||
PendingEndBound: HistoryBounds.Carry(pending, [startBound])));
|
||||
@@ -7224,7 +7235,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
EmptyFromTicks: walkHint.EmptyFromTicks, EmptyToTicks: walkHint.EmptyToTicks,
|
||||
PendingEndBound: HistoryBounds.Carry(endBound, slice),
|
||||
WithinTieCluster: resumeAt == startUtc);
|
||||
clusterCp = _historyContinuationStore.Save(session, clusterState);
|
||||
clusterCp = SaveHistoryContinuation(session, handle.NodeId, historyEpoch, clusterState);
|
||||
}
|
||||
else if (endBound is not null)
|
||||
{
|
||||
@@ -7279,7 +7290,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
{
|
||||
// Save may return null (no session on this request) ⇒ degrade to single-shot for this node.
|
||||
// Built AFTER historyData so a failure projecting samples can never orphan a stored cursor.
|
||||
outboundCp = _historyContinuationStore.Save(session, nextState);
|
||||
outboundCp = SaveHistoryContinuation(session, handle.NodeId, historyEpoch, nextState);
|
||||
}
|
||||
else if (endBound is not null)
|
||||
{
|
||||
@@ -7366,6 +7377,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
uint numValuesPerNode,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var historyEpoch = Volatile.Read(ref _historyContinuationEpoch);
|
||||
var inboundCp = nodesToRead[handle.Index].ContinuationPoint;
|
||||
try
|
||||
{
|
||||
@@ -7381,7 +7393,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
if (inboundCp is { Length: > 0 })
|
||||
{
|
||||
var state = _historyContinuationStore.TryTake(session, inboundCp.ToArray());
|
||||
if (state is null || state.Kind != HistoryCursorKind.Modified || state.Tagname != tagname)
|
||||
if (state is null || !HistoryContinuationMatches(state, handle.NodeId, historyEpoch) || state.Kind != HistoryCursorKind.Modified || state.Tagname != tagname)
|
||||
{
|
||||
errors[handle.Index] = StatusCodes.BadContinuationPointInvalid;
|
||||
results[handle.Index] = new SdkHistoryReadResult { StatusCode = StatusCodes.BadContinuationPointInvalid };
|
||||
@@ -7411,7 +7423,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
if (page.HasMore && numValuesPerNode > 0 && page.Values.Count > 0)
|
||||
{
|
||||
var last = page.Values[^1];
|
||||
outboundCp = _historyContinuationStore.Save(session, new HistoryContinuationState(
|
||||
outboundCp = SaveHistoryContinuation(session, handle.NodeId, historyEpoch, new HistoryContinuationState(
|
||||
tagname, last.SourceTimeUtc, endUtc, 0, numValuesPerNode,
|
||||
HistoryCursorKind.Modified, last.ModifiedId, startUtc));
|
||||
}
|
||||
@@ -7489,6 +7501,18 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
}
|
||||
}
|
||||
|
||||
private byte[]? SaveHistoryContinuation(ISession? session, NodeId nodeId, long epoch, HistoryContinuationState state)
|
||||
{
|
||||
if (epoch != Volatile.Read(ref _historyContinuationEpoch)) return null;
|
||||
return _historyContinuationStore.Save(session, state with
|
||||
{
|
||||
BoundNodeKey = nodeId.ToString(), AddressSpaceEpoch = epoch,
|
||||
});
|
||||
}
|
||||
|
||||
private static bool HistoryContinuationMatches(HistoryContinuationState state, NodeId nodeId, long epoch) =>
|
||||
state.BoundNodeKey == nodeId.ToString() && state.AddressSpaceEpoch == epoch;
|
||||
|
||||
/// <summary>Transport failures are retryable; unsupported operations retain their distinct status.</summary>
|
||||
private static StatusCode HistoryReadFailureStatus(Exception error)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Server;
|
||||
using Opc.Ua.Identity;
|
||||
using Opc.Ua.Server.Hosting;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Authorization;
|
||||
@@ -25,6 +27,21 @@ public sealed class OtOpcUaSdkServer : StandardServer
|
||||
public OtOpcUaSdkServer(ILogger<OtOpcUaNodeManager>? nodeManagerLogger = null)
|
||||
: base(DefaultTelemetry.Create(_ => { })) => _nodeManagerLogger = nodeManagerLogger;
|
||||
|
||||
internal void ConfigureIdentityAuthenticators(IEnumerable<IUserTokenAuthenticator> authenticators) =>
|
||||
AddPreStartupTask(new IdentityStartupTask(authenticators.ToArray()));
|
||||
|
||||
private sealed class IdentityStartupTask(IReadOnlyList<IUserTokenAuthenticator> authenticators) : IServerPreStartupTask
|
||||
{
|
||||
public ValueTask OnServerStartingAsync(IServerContext server, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (server is not IServerInternal internalServer)
|
||||
throw new InvalidOperationException("Application identity registry unavailable during server startup.");
|
||||
foreach (var authenticator in authenticators) internalServer.IdentityRegistry.Register(authenticator);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly AsyncLocal<System.Net.IPAddress?> _activationPeer = new();
|
||||
|
||||
/// <summary>The transport-observed peer of the current activation request, when available.</summary>
|
||||
@@ -40,16 +57,23 @@ public sealed class OtOpcUaSdkServer : StandardServer
|
||||
ExtensionObject userIdentityToken,
|
||||
SignatureData? userTokenSignature,
|
||||
RequestLifetime requestLifetime)
|
||||
{
|
||||
return await WithActivationPeerAddressAsync(secureChannelContext.PeerAddress,
|
||||
() => base.ActivateSessionAsync(secureChannelContext, requestHeader, clientSignature,
|
||||
clientSoftwareCertificates, localeIds, userIdentityToken, userTokenSignature, requestLifetime))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal async ValueTask<ActivateSessionResponse> WithActivationPeerAddressAsync(
|
||||
System.Net.IPAddress? peer, Func<ValueTask<ActivateSessionResponse>> activation)
|
||||
{
|
||||
// AuthenticationContext does not carry the request peer in 2.0.1. Keep the public
|
||||
// transport evidence in this activation's async flow, including reactivation on a new channel.
|
||||
var previous = _activationPeer.Value;
|
||||
_activationPeer.Value = secureChannelContext.PeerAddress;
|
||||
_activationPeer.Value = peer;
|
||||
try
|
||||
{
|
||||
return await base.ActivateSessionAsync(secureChannelContext, requestHeader, clientSignature,
|
||||
clientSoftwareCertificates, localeIds, userIdentityToken, userTokenSignature, requestLifetime)
|
||||
.ConfigureAwait(false);
|
||||
return await activation().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -267,6 +291,7 @@ public sealed class OtOpcUaSdkServer : StandardServer
|
||||
defaults.PercentDataGood = ServerAggregateConfiguration.PercentDataGood;
|
||||
defaults.UseSlopedExtrapolation = ServerAggregateConfiguration.UseSlopedExtrapolation;
|
||||
manager.SetDefaultConfiguration(defaults);
|
||||
await Compatibility.Aggregates.CompatibilityAggregateFactory.RegisterAsync(manager, cancellationToken).ConfigureAwait(false);
|
||||
_otOpcUaNodeManager?.SyncAdvertisedAggregates();
|
||||
return manager;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Server;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Security;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="UserIdentity"/> that additionally carries the LDAP-resolved roles
|
||||
/// (<see cref="OpcUaUserAuthResult.Roles"/>) for the authenticated session. The SDK stores the
|
||||
/// instance we assign to <c>ImpersonateEventArgs.Identity</c> by reference onto
|
||||
/// <c>Session.Identity</c>/<c>Session.EffectiveIdentity</c> (no wrapping or copying), so a method
|
||||
/// (<see cref="OpcUaUserAuthResult.Roles"/>) for the authenticated session. The SDK may augment identities with mandatory and dynamically mapped roles. This subtype
|
||||
/// preserves the application identity when those roles are added, so a method
|
||||
/// handler can later recover the roles via
|
||||
/// <c>(context as ISessionOperationContext)?.UserIdentity as RoleCarryingUserIdentity</c> — the
|
||||
/// seam the inbound alarm ack/shelve <c>AlarmAck</c> gate (T18) reads. This type only carries the
|
||||
/// roles; it makes no auth decision.
|
||||
/// </summary>
|
||||
public sealed class RoleCarryingUserIdentity : UserIdentity
|
||||
public sealed class RoleCarryingUserIdentity : RoleBasedIdentity
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance, building the base identity from the same UserName token the
|
||||
@@ -35,7 +35,14 @@ public sealed class RoleCarryingUserIdentity : UserIdentity
|
||||
/// <param name="apiKeyId">The public id of the key that authenticated, or null for anonymous.</param>
|
||||
public RoleCarryingUserIdentity(
|
||||
UserIdentityToken token, IReadOnlyList<string> roles, string principal, string? apiKeyId)
|
||||
: base(token)
|
||||
: this(new UserIdentity(token), roles, principal, apiKeyId, [], new NamespaceTable())
|
||||
{
|
||||
}
|
||||
|
||||
private RoleCarryingUserIdentity(
|
||||
IUserIdentity identity, IReadOnlyList<string> roles, string principal, string? apiKeyId,
|
||||
IEnumerable<Role> sdkRoles, NamespaceTable namespaces)
|
||||
: base(identity, sdkRoles, namespaces)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(roles);
|
||||
// Freeze to a defensive copy so a caller mutating the source list can never retroactively
|
||||
@@ -46,8 +53,13 @@ public sealed class RoleCarryingUserIdentity : UserIdentity
|
||||
ApiKeyId = apiKeyId;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override RoleBasedIdentity WithAdditionalRoles(IEnumerable<Role> additionalRoles, NamespaceTable namespaces) =>
|
||||
new RoleCarryingUserIdentity(InnerIdentity, Roles, Principal, ApiKeyId,
|
||||
base.Roles.Concat(additionalRoles), namespaces);
|
||||
|
||||
/// <summary>The roles the authenticator granted this session, used by downstream permission gates.</summary>
|
||||
public IReadOnlyList<string> Roles { get; }
|
||||
public new IReadOnlyList<string> Roles { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The ACL principal this session authenticates as — the name <c>NodeAcl</c> rows key on, and what
|
||||
|
||||
Reference in New Issue
Block a user