Phase 6.2 Stream C wiring — AuthorizationBootstrap + OpcUaApplicationHost.SetAuthorization
Closes task #133 — the "authz gate is inert in production" blocker surfaced during task #123. Before this commit, every ACL check on the six dispatch surfaces (Read, Write, HistoryRead, Browse, CreateMonitoredItems, Call) short-circuited to allow because Program.cs constructed OpcUaApplicationHost without passing authzGate or scopeResolver. New pieces: - `AuthorizationOptions` — bound to `Node:Authorization` in appsettings.json. `Enabled` (default false) is the master switch; `StrictMode` (default false) controls the anonymous / no-LDAP-groups fallback behaviour. - `AuthorizationBootstrap` — singleton service that loads `NodeAcl` rows for the published generation, builds a `PermissionTrieCache` + `AuthorizationGate`, merges every registered driver's `EquipmentNamespaceContent` through `ScopePathIndexBuilder` into one full-path `NodeScopeResolver`. Returns `(null, null)` when disabled or when no generation is Published yet. - `DriverEquipmentContentRegistry.Snapshot()` — new method returning a defensive copy of the driver → content map so the bootstrap can iterate without holding the lock. - `OpcUaApplicationHost.SetAuthorization(gate, resolver)` — late-bind method matching the existing `SetPhase7Sources` pattern. Must run before `StartAsync`; rejects post-start rebinding with InvalidOperationException. - `OpcUaServerService.ExecuteAsync` calls `AuthorizationBootstrap.BuildAsync` after `PopulateEquipmentContentAsync` and before `applicationHost.StartAsync`, in the same window that `SetPhase7Sources` runs. Behaviour change - Default (Enabled=false): no behaviour change — the gate stays null, all six dispatch surfaces run unchanged. Safe for any existing deployment on upgrade. - Enabled=true with StrictMode=false: identities carrying LDAP groups are evaluated against the trie; anonymous / no-groups identities pass through (v1 legacy-client compatibility). - Enabled=true with StrictMode=true: everything evaluates. Anonymous or no-groups identities are denied. Follow-up not covered here: rebind the gate+resolver on generation refresh (the `GenerationRefreshHostedService` that shipped earlier in this session). Today the gate only reflects the bootstrap generation — operators publishing new ACL changes need a process restart to see them. Matches the current driver-hot-reload limitation and is tracked in the existing 6.3 follow-up bullet. Docs: v2-release-readiness.md Phase 6.2 Stream C.12 bullet flipped to Closed with operator-facing config pointer (`Node:Authorization:Enabled`). All 283/283 Server.Tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,7 +38,7 @@ Remaining Stream C surfaces (hardening, not release-blocking):
|
|||||||
- ~~CreateMonitoredItems + TransferSubscriptions gating with per-item `(AuthGenerationId, MembershipVersion)` stamp so revoked grants surface `BadUserAccessDenied` within one publish cycle (decision #153).~~ **Partial, 2026-04-24.** `DriverNodeManager.CreateMonitoredItems` override pre-gates each request and pre-populates `BadUserAccessDenied` into the errors slot for denied items (the base stack honours pre-set errors and skips those items). Decision #153's per-item `(AuthGenerationId, MembershipVersion)` stamp for detecting mid-subscription revocation is still to ship — needs subscription-layer plumbing. TransferSubscriptions not yet wired (same pattern).
|
- ~~CreateMonitoredItems + TransferSubscriptions gating with per-item `(AuthGenerationId, MembershipVersion)` stamp so revoked grants surface `BadUserAccessDenied` within one publish cycle (decision #153).~~ **Partial, 2026-04-24.** `DriverNodeManager.CreateMonitoredItems` override pre-gates each request and pre-populates `BadUserAccessDenied` into the errors slot for denied items (the base stack honours pre-set errors and skips those items). Decision #153's per-item `(AuthGenerationId, MembershipVersion)` stamp for detecting mid-subscription revocation is still to ship — needs subscription-layer plumbing. TransferSubscriptions not yet wired (same pattern).
|
||||||
- ~~Alarm Acknowledge / Confirm / Shelve gating.~~ **Partial, 2026-04-24.** Acknowledge + Confirm map to dedicated `OpcUaOperation.AlarmAcknowledge` / `AlarmConfirm` via `MapCallOperation`; Shelve falls through to generic `OpcUaOperation.Call` (needs per-instance method NodeId resolution to distinguish — follow-up).
|
- ~~Alarm Acknowledge / Confirm / Shelve gating.~~ **Partial, 2026-04-24.** Acknowledge + Confirm map to dedicated `OpcUaOperation.AlarmAcknowledge` / `AlarmConfirm` via `MapCallOperation`; Shelve falls through to generic `OpcUaOperation.Call` (needs per-instance method NodeId resolution to distinguish — follow-up).
|
||||||
- ~~Call (method invocation) gating.~~ **Closed 2026-04-24.** `DriverNodeManager.Call` override pre-gates each `CallMethodRequest` via `GateCallMethodRequests`. Denied calls return `BadUserAccessDenied` without running the method. Alarm methods map to alarm-specific operation kinds; everything else gates as generic `Call`.
|
- ~~Call (method invocation) gating.~~ **Closed 2026-04-24.** `DriverNodeManager.Call` override pre-gates each `CallMethodRequest` via `GateCallMethodRequests`. Denied calls return `BadUserAccessDenied` without running the method. Alarm methods map to alarm-specific operation kinds; everything else gates as generic `Call`.
|
||||||
- ~~Finer-grained scope resolution — current `NodeScopeResolver` returns a flat cluster-level scope. Joining against the live Configuration DB to populate UnsArea / UnsLine / Equipment path is tracked as Stream C.12.~~ **Partial, 2026-04-24.** `ScopePathIndexBuilder` + indexed-mode `NodeScopeResolver` exist and are unit-tested — index keys driver-side full-ref → full `Cluster → Namespace → UnsArea → UnsLine → Equipment → Tag` scope. **Critical follow-up (task #133):** Program.cs does not yet construct either the gate or the resolver — all six dispatch-layer gates (Read, Write, HistoryRead, Browse, CreateMonitoredItems, Call) are currently inert in production. Wiring is required before GA.
|
- ~~Finer-grained scope resolution — current `NodeScopeResolver` returns a flat cluster-level scope. Joining against the live Configuration DB to populate UnsArea / UnsLine / Equipment path is tracked as Stream C.12.~~ **Closed 2026-04-24.** `AuthorizationBootstrap` now loads `NodeAcl` rows for the current generation into a `PermissionTrieCache`, builds the gate, and merges every registered driver's `EquipmentNamespaceContent` into a full-path `NodeScopeResolver` index. `OpcUaServerService` calls the bootstrap after the equipment registry is populated, before `OpcUaApplicationHost.StartAsync`. Disabled by default — operators flip `Node:Authorization:Enabled=true` to enforce, `StrictMode=true` to reject anonymous/no-groups identities.
|
||||||
- 3-user integration matrix covering every operation × allow/deny.
|
- 3-user integration matrix covering every operation × allow/deny.
|
||||||
|
|
||||||
### ~~Config fallback — Phase 6.1 Stream D wiring~~ (task #136 — **CLOSED** 2026-04-19, PR #96)
|
### ~~Config fallback — Phase 6.1 Stream D wiring~~ (task #136 — **CLOSED** 2026-04-19, PR #96)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using ZB.MOM.WW.OtOpcUa.Server.Security;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.OtOpcUa.Server;
|
namespace ZB.MOM.WW.OtOpcUa.Server;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -20,4 +22,7 @@ public sealed class NodeOptions
|
|||||||
|
|
||||||
/// <summary>Path to the LiteDB local cache file.</summary>
|
/// <summary>Path to the LiteDB local cache file.</summary>
|
||||||
public string LocalCachePath { get; init; } = "config_cache.db";
|
public string LocalCachePath { get; init; } = "config_cache.db";
|
||||||
|
|
||||||
|
/// <summary>Phase 6.2 authorization pipeline config. Disabled by default.</summary>
|
||||||
|
public AuthorizationOptions Authorization { get; init; } = new();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,4 +44,17 @@ public sealed class DriverEquipmentContentRegistry
|
|||||||
{
|
{
|
||||||
get { lock (_lock) { return _content.Count; } }
|
get { lock (_lock) { return _content.Count; } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Snapshot the current driver → content map. Returns a copy so callers can iterate
|
||||||
|
/// without holding the lock. Used at authorization bootstrap to merge all namespaces
|
||||||
|
/// into a single <see cref="Security.NodeScopeResolver"/> path index.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyDictionary<string, EquipmentNamespaceContent> Snapshot()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, EquipmentNamespaceContent>(_content, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
|
|||||||
private readonly DriverHost _driverHost;
|
private readonly DriverHost _driverHost;
|
||||||
private readonly IUserAuthenticator _authenticator;
|
private readonly IUserAuthenticator _authenticator;
|
||||||
private readonly DriverResiliencePipelineBuilder _pipelineBuilder;
|
private readonly DriverResiliencePipelineBuilder _pipelineBuilder;
|
||||||
private readonly AuthorizationGate? _authzGate;
|
private AuthorizationGate? _authzGate;
|
||||||
private readonly NodeScopeResolver? _scopeResolver;
|
private NodeScopeResolver? _scopeResolver;
|
||||||
private readonly StaleConfigFlag? _staleConfigFlag;
|
private readonly StaleConfigFlag? _staleConfigFlag;
|
||||||
private readonly Func<string, ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverTier>? _tierLookup;
|
private readonly Func<string, ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverTier>? _tierLookup;
|
||||||
private readonly Func<string, string?>? _resilienceConfigLookup;
|
private readonly Func<string, string?>? _resilienceConfigLookup;
|
||||||
@@ -95,6 +95,23 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
|
|||||||
_scriptedAlarmReadable = scriptedAlarmReadable;
|
_scriptedAlarmReadable = scriptedAlarmReadable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Late-bind the Phase 6.2 authorization gate + node-scope resolver. Must be called
|
||||||
|
/// BEFORE <see cref="StartAsync"/> — once the OPC UA server starts the
|
||||||
|
/// <see cref="OtOpcUaServer"/> + per-namespace <see cref="DriverNodeManager"/>s
|
||||||
|
/// capture these fields and later rebinding has no effect on already-materialized
|
||||||
|
/// managers. Call with <c>null</c> for either parameter to leave the corresponding
|
||||||
|
/// pipeline inert.
|
||||||
|
/// </summary>
|
||||||
|
public void SetAuthorization(AuthorizationGate? gate, NodeScopeResolver? resolver)
|
||||||
|
{
|
||||||
|
if (_server is not null)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Authorization must be set before OpcUaApplicationHost.StartAsync; the OtOpcUaServer + DriverNodeManagers have already captured the previous values.");
|
||||||
|
_authzGate = gate;
|
||||||
|
_scopeResolver = resolver;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds the <see cref="ApplicationConfiguration"/>, validates/creates the application
|
/// Builds the <see cref="ApplicationConfiguration"/>, validates/creates the application
|
||||||
/// certificate, constructs + starts the <see cref="OtOpcUaServer"/>, then drives
|
/// certificate, constructs + starts the <see cref="OtOpcUaServer"/>, then drives
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
||||||
using ZB.MOM.WW.OtOpcUa.Server.OpcUa;
|
using ZB.MOM.WW.OtOpcUa.Server.OpcUa;
|
||||||
using ZB.MOM.WW.OtOpcUa.Server.Phase7;
|
using ZB.MOM.WW.OtOpcUa.Server.Phase7;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Server.Security;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.OtOpcUa.Server;
|
namespace ZB.MOM.WW.OtOpcUa.Server;
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ public sealed class OpcUaServerService(
|
|||||||
DriverEquipmentContentRegistry equipmentContentRegistry,
|
DriverEquipmentContentRegistry equipmentContentRegistry,
|
||||||
DriverInstanceBootstrapper driverBootstrapper,
|
DriverInstanceBootstrapper driverBootstrapper,
|
||||||
Phase7Composer phase7Composer,
|
Phase7Composer phase7Composer,
|
||||||
|
AuthorizationBootstrap authorizationBootstrap,
|
||||||
IServiceScopeFactory scopeFactory,
|
IServiceScopeFactory scopeFactory,
|
||||||
ILogger<OpcUaServerService> logger) : BackgroundService
|
ILogger<OpcUaServerService> logger) : BackgroundService
|
||||||
{
|
{
|
||||||
@@ -55,6 +57,15 @@ public sealed class OpcUaServerService(
|
|||||||
// No-op when the generation has no virtual tags or scripted alarms.
|
// No-op when the generation has no virtual tags or scripted alarms.
|
||||||
var phase7 = await phase7Composer.PrepareAsync(gen, stoppingToken);
|
var phase7 = await phase7Composer.PrepareAsync(gen, stoppingToken);
|
||||||
applicationHost.SetPhase7Sources(phase7.VirtualReadable, phase7.ScriptedAlarmReadable);
|
applicationHost.SetPhase7Sources(phase7.VirtualReadable, phase7.ScriptedAlarmReadable);
|
||||||
|
|
||||||
|
// Phase 6.2 Stream C wiring — build the AuthorizationGate + NodeScopeResolver
|
||||||
|
// from the published generation's NodeAcl rows and the populated equipment
|
||||||
|
// registry. No-op when Node:Authorization:Enabled=false. Must run before
|
||||||
|
// StartAsync: OtOpcUaServer + DriverNodeManager construction captures the
|
||||||
|
// field values on the application host.
|
||||||
|
var (authzGate, scopeResolver) = await authorizationBootstrap
|
||||||
|
.BuildAsync(gen, stoppingToken).ConfigureAwait(false);
|
||||||
|
applicationHost.SetAuthorization(authzGate, scopeResolver);
|
||||||
}
|
}
|
||||||
|
|
||||||
await applicationHost.StartAsync(stoppingToken);
|
await applicationHost.StartAsync(stoppingToken);
|
||||||
|
|||||||
@@ -123,6 +123,11 @@ builder.Services.AddSingleton<DriverInstanceBootstrapper>();
|
|||||||
// added to OpcUaApplicationHost's ctor seam.
|
// added to OpcUaApplicationHost's ctor seam.
|
||||||
builder.Services.AddSingleton<DriverEquipmentContentRegistry>();
|
builder.Services.AddSingleton<DriverEquipmentContentRegistry>();
|
||||||
builder.Services.AddScoped<EquipmentNamespaceContentLoader>();
|
builder.Services.AddScoped<EquipmentNamespaceContentLoader>();
|
||||||
|
// Phase 6.2 Stream C wiring — constructs AuthorizationGate + NodeScopeResolver from the
|
||||||
|
// published generation's NodeAcl rows + per-driver EquipmentNamespaceContent. Gated by
|
||||||
|
// NodeOptions.Authorization.Enabled (default false) so existing deployments don't flip
|
||||||
|
// to ACL enforcement accidentally on upgrade.
|
||||||
|
builder.Services.AddSingleton<AuthorizationBootstrap>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<OpcUaApplicationHost>(sp =>
|
builder.Services.AddSingleton<OpcUaApplicationHost>(sp =>
|
||||||
{
|
{
|
||||||
|
|||||||
115
src/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationBootstrap.cs
Normal file
115
src/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationBootstrap.cs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Core.Authorization;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Core.OpcUa;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Server.OpcUa;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Server.Security;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bootstraps the Phase 6.2 authorization pipeline for the running Server. Loads
|
||||||
|
/// <c>NodeAcl</c> rows for the current generation into a
|
||||||
|
/// <see cref="PermissionTrieCache"/>, constructs an <see cref="AuthorizationGate"/>,
|
||||||
|
/// and merges per-namespace <see cref="EquipmentNamespaceContent"/> into a single
|
||||||
|
/// full-path index for <see cref="NodeScopeResolver"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Called by <c>OpcUaServerService.ExecuteAsync</c> after the
|
||||||
|
/// <see cref="DriverEquipmentContentRegistry"/> has been populated but before
|
||||||
|
/// <c>OpcUaApplicationHost.StartAsync</c> runs — that's the window where the
|
||||||
|
/// config-DB state is known + the OPC UA server hasn't yet captured the gate
|
||||||
|
/// references.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="AuthorizationOptions.Enabled"/> gates the whole flow. When
|
||||||
|
/// <c>false</c> (default), <see cref="BuildAsync"/> returns <c>(null, null)</c>
|
||||||
|
/// and the dispatch layer short-circuits every ACL check — identical to
|
||||||
|
/// pre-Phase-6.2.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class AuthorizationBootstrap(
|
||||||
|
IDbContextFactory<OtOpcUaConfigDbContext> dbFactory,
|
||||||
|
DriverEquipmentContentRegistry equipmentContentRegistry,
|
||||||
|
NodeOptions nodeOptions,
|
||||||
|
ILogger<AuthorizationBootstrap> logger)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Build a gate + resolver pair for the supplied <paramref name="generationId"/>.
|
||||||
|
/// Returns <c>(null, null)</c> when authorization is disabled via
|
||||||
|
/// <see cref="AuthorizationOptions.Enabled"/> or when the generation couldn't be
|
||||||
|
/// fetched — in that case the dispatch layer runs without ACL enforcement (same
|
||||||
|
/// behaviour the Server had before Phase 6.2 Stream C landed).
|
||||||
|
/// </summary>
|
||||||
|
public async Task<(AuthorizationGate?, NodeScopeResolver?)> BuildAsync(
|
||||||
|
long? generationId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!nodeOptions.Authorization.Enabled)
|
||||||
|
{
|
||||||
|
logger.LogInformation(
|
||||||
|
"Authorization disabled (Node:Authorization:Enabled=false); all ACL gates remain inert");
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generationId is not long gen)
|
||||||
|
{
|
||||||
|
logger.LogWarning(
|
||||||
|
"Authorization enabled but no Published generation available — ACL enforcement skipped until next publish");
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var gate = await BuildGateAsync(gen, cancellationToken).ConfigureAwait(false);
|
||||||
|
var resolver = BuildResolver();
|
||||||
|
|
||||||
|
logger.LogInformation(
|
||||||
|
"Authorization pipeline bootstrapped — generation {Gen}, strictMode={Strict}",
|
||||||
|
gen, nodeOptions.Authorization.StrictMode);
|
||||||
|
|
||||||
|
return (gate, resolver);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load every <see cref="Configuration.Entities.NodeAcl"/> row for
|
||||||
|
/// <paramref name="generationId"/> scoped to this node's cluster, build a
|
||||||
|
/// <see cref="PermissionTrieCache"/>, construct an <see cref="AuthorizationGate"/>.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<AuthorizationGate> BuildGateAsync(long generationId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using var ctx = await dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
var rows = await ctx.NodeAcls
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(a => a.ClusterId == nodeOptions.ClusterId && a.GenerationId == generationId)
|
||||||
|
.ToListAsync(cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
var cache = new PermissionTrieCache();
|
||||||
|
cache.Install(PermissionTrieBuilder.Build(nodeOptions.ClusterId, generationId, rows));
|
||||||
|
var evaluator = new TriePermissionEvaluator(cache);
|
||||||
|
return new AuthorizationGate(evaluator, strictMode: nodeOptions.Authorization.StrictMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Merge each registered driver's <see cref="EquipmentNamespaceContent"/> into a single
|
||||||
|
/// full-path index. Tag rows that cross-reference missing Equipment / Line / Area are
|
||||||
|
/// silently skipped (the cluster-only fallback handles them). Duplicate TagConfig
|
||||||
|
/// across namespaces is a config error — <see cref="ScopePathIndexBuilder"/> throws
|
||||||
|
/// on collision; we let that bubble so bootstrap fails fast.
|
||||||
|
/// </summary>
|
||||||
|
private NodeScopeResolver BuildResolver()
|
||||||
|
{
|
||||||
|
var merged = new Dictionary<string, NodeScope>(StringComparer.Ordinal);
|
||||||
|
foreach (var kv in equipmentContentRegistry.Snapshot())
|
||||||
|
{
|
||||||
|
// Namespace id isn't carried on EquipmentNamespaceContent directly — driverId
|
||||||
|
// serves as the namespace-stable key for ACL scope resolution.
|
||||||
|
var perNamespace = ScopePathIndexBuilder.Build(nodeOptions.ClusterId, kv.Key, kv.Value);
|
||||||
|
foreach (var entry in perNamespace)
|
||||||
|
merged[entry.Key] = entry.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged.Count == 0
|
||||||
|
? new NodeScopeResolver(nodeOptions.ClusterId)
|
||||||
|
: new NodeScopeResolver(nodeOptions.ClusterId, merged);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
namespace ZB.MOM.WW.OtOpcUa.Server.Security;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration for the Phase 6.2 authorization pipeline. Bound from the
|
||||||
|
/// <c>Node:Authorization</c> section of <c>appsettings.json</c>. Defaults ship disabled
|
||||||
|
/// so upgrading from pre-Phase-6.2 doesn't accidentally start denying reads the day a
|
||||||
|
/// new build lands — operators opt in explicitly once their <c>NodeAcl</c> rows are
|
||||||
|
/// populated.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="Enabled"/> is the master switch. When <c>false</c> (default),
|
||||||
|
/// the OPC UA application host constructs with
|
||||||
|
/// <c>authzGate: null, scopeResolver: null</c>; all six dispatch-layer gates
|
||||||
|
/// (Read, Write, HistoryRead, Browse, CreateMonitoredItems, Call) short-circuit
|
||||||
|
/// to pass — identical behaviour to pre-Phase-6.2.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// When <c>true</c>, <see cref="StrictMode"/> picks between two failure modes:
|
||||||
|
/// <c>false</c> (default) grants anonymous / no-LDAP-groups identities a pass-
|
||||||
|
/// through so v1-style legacy clients keep working; <c>true</c> denies them.
|
||||||
|
/// Production deployments should flip to <c>StrictMode = true</c> once every
|
||||||
|
/// client has been validated against the new identity flow.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class AuthorizationOptions
|
||||||
|
{
|
||||||
|
/// <summary>Master switch. False = gate is inert; true = gate is wired into dispatch.</summary>
|
||||||
|
public bool Enabled { get; init; }
|
||||||
|
|
||||||
|
/// <summary>False = anonymous / no-groups identities pass; true = they're denied.</summary>
|
||||||
|
public bool StrictMode { get; init; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user