using Opc.Ua;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Authorization;
namespace ZB.MOM.WW.OtOpcUa.Server.Security;
///
/// Bridges the OPC UA stack's to the
/// evaluator. Resolves the session's
/// from whatever the identity claims + the stack's
/// session handle, then delegates to the evaluator and returns a single bool the
/// dispatch paths can use to short-circuit with BadUserAccessDenied.
///
///
/// This class is deliberately the single integration seam between the Server
/// project and the Core.Authorization evaluator. DriverNodeManager holds one
/// reference and calls on every Read / Write / HistoryRead /
/// Browse / Call / CreateMonitoredItems / etc. The evaluator itself stays pure — it
/// doesn't know about the OPC UA stack types.
///
/// Fail-open-during-transition: when the evaluator is configured with
/// StrictMode = false, missing cluster tries OR sessions without resolved
/// LDAP groups get true so existing deployments keep working while ACLs are
/// populated. Flip to strict via Authorization:StrictMode = true in production.
///
public sealed class AuthorizationGate
{
private readonly IPermissionEvaluator _evaluator;
private readonly bool _strictMode;
private readonly TimeProvider _timeProvider;
public AuthorizationGate(IPermissionEvaluator evaluator, bool strictMode = false, TimeProvider? timeProvider = null)
{
_evaluator = evaluator ?? throw new ArgumentNullException(nameof(evaluator));
_strictMode = strictMode;
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// True when strict authorization is enabled — no-grant = denied.
public bool StrictMode => _strictMode;
///
/// Authorize an OPC UA operation against the session identity + scope. Returns true to
/// allow the dispatch to continue; false to surface BadUserAccessDenied.
///
public bool IsAllowed(IUserIdentity? identity, OpcUaOperation operation, NodeScope scope)
{
// Anonymous / unknown identity — strict mode denies, lax mode allows so the fallback
// auth layers (WriteAuthzPolicy) still see the call.
if (identity is null) return !_strictMode;
var session = BuildSessionState(identity, scope.ClusterId);
if (session is null)
{
// Identity doesn't carry LDAP groups. In lax mode let the dispatch proceed so
// older deployments keep working; strict mode denies.
return !_strictMode;
}
var decision = _evaluator.Authorize(session, operation, scope);
if (decision.IsAllowed) return true;
return !_strictMode;
}
///
/// Materialize a from the session identity.
/// Returns null when the identity doesn't carry LDAP group metadata.
///
public UserAuthorizationState? BuildSessionState(IUserIdentity identity, string clusterId)
{
if (identity is not ILdapGroupsBearer bearer || bearer.LdapGroups.Count == 0)
return null;
var sessionId = identity.DisplayName ?? Guid.NewGuid().ToString("N");
return new UserAuthorizationState
{
SessionId = sessionId,
ClusterId = clusterId,
LdapGroups = bearer.LdapGroups,
MembershipResolvedUtc = _timeProvider.GetUtcNow().UtcDateTime,
AuthGenerationId = 0,
MembershipVersion = 0,
};
}
}