ca0cf19a1f
A bulk command with N denied items wrote N audit events into the bounded, drop-on-full audit channel, so a low-privilege key could evict every other pending audit record. Bulk filters now accumulate a ConstraintDenialSummary (count, constraint names, <=20 sample targets) and write one aggregated constraint-denied event per command. The per-item constraint checks are synchronous, so IConstraintEnforcer's Check* methods now return ValueTask instead of allocating a Task per item.
117 lines
6.3 KiB
C#
117 lines
6.3 KiB
C#
using ZB.MOM.WW.MxGateway.Server.Security.Authentication;
|
|
using ZB.MOM.WW.MxGateway.Server.Sessions;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Server.Security.Authorization;
|
|
|
|
/// <summary>Enforces API-key read/write constraints and records constraint denials for audit.</summary>
|
|
/// <remarks>
|
|
/// The per-item checks return <see cref="ValueTask{TResult}"/> because every production check is
|
|
/// synchronous (an in-memory Galaxy hierarchy lookup) and runs once per item of a bulk command:
|
|
/// a <see cref="Task{TResult}"/> return allocated a task per item for a value that was already
|
|
/// known. An implementation that genuinely needs to await still can.
|
|
/// </remarks>
|
|
public interface IConstraintEnforcer
|
|
{
|
|
/// <summary>
|
|
/// Gets a value indicating whether any read constraint applies to an identity at all, so a
|
|
/// bulk caller can hoist the question out of its per-item loop.
|
|
/// </summary>
|
|
/// <param name="identity">The API key identity.</param>
|
|
/// <returns><see langword="true"/> when at least one read constraint applies; otherwise <see langword="false"/>.</returns>
|
|
/// <remarks>
|
|
/// Every per-item <see cref="CheckReadTagAsync"/> / <see cref="CheckReadHandleAsync"/> call for
|
|
/// an unconstrained identity allows the item, so skipping the loop removes work without
|
|
/// changing a decision. The default implementation answers <see langword="true"/> — an
|
|
/// implementation that does not model constraints (test doubles, allow-all enforcers) keeps
|
|
/// being consulted per item rather than being silently bypassed.
|
|
/// </remarks>
|
|
bool HasReadConstraints(ApiKeyIdentity? identity) => true;
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether any write constraint applies to an identity at all, the
|
|
/// write-side counterpart of <see cref="HasReadConstraints"/>.
|
|
/// </summary>
|
|
/// <param name="identity">The API key identity.</param>
|
|
/// <returns><see langword="true"/> when at least one write constraint applies; otherwise <see langword="false"/>.</returns>
|
|
/// <remarks>The same conservative default as <see cref="HasReadConstraints"/> applies.</remarks>
|
|
bool HasWriteConstraints(ApiKeyIdentity? identity) => true;
|
|
|
|
/// <summary>Checks whether a read constraint is satisfied for a tag address.</summary>
|
|
/// <param name="identity">The API key identity.</param>
|
|
/// <param name="tagAddress">Tag address to check.</param>
|
|
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
|
/// <returns>The constraint failure details if denied; otherwise null.</returns>
|
|
ValueTask<ConstraintFailure?> CheckReadTagAsync(
|
|
ApiKeyIdentity? identity,
|
|
string tagAddress,
|
|
CancellationToken cancellationToken);
|
|
|
|
/// <summary>Checks whether a read constraint is satisfied for an item handle.</summary>
|
|
/// <param name="identity">The API key identity.</param>
|
|
/// <param name="session">The gateway session.</param>
|
|
/// <param name="serverHandle">The MXAccess server handle.</param>
|
|
/// <param name="itemHandle">The MXAccess item handle.</param>
|
|
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
|
/// <returns>The constraint failure details if denied; otherwise null.</returns>
|
|
ValueTask<ConstraintFailure?> CheckReadHandleAsync(
|
|
ApiKeyIdentity? identity,
|
|
GatewaySession session,
|
|
int serverHandle,
|
|
int itemHandle,
|
|
CancellationToken cancellationToken);
|
|
|
|
/// <summary>Checks whether a write constraint is satisfied for an item handle.</summary>
|
|
/// <param name="identity">The API key identity.</param>
|
|
/// <param name="session">The gateway session.</param>
|
|
/// <param name="serverHandle">The MXAccess server handle.</param>
|
|
/// <param name="itemHandle">The MXAccess item handle.</param>
|
|
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
|
/// <returns>The constraint failure details if denied; otherwise null.</returns>
|
|
ValueTask<ConstraintFailure?> CheckWriteHandleAsync(
|
|
ApiKeyIdentity? identity,
|
|
GatewaySession session,
|
|
int serverHandle,
|
|
int itemHandle,
|
|
CancellationToken cancellationToken);
|
|
|
|
/// <summary>Records a constraint denial for audit and metrics.</summary>
|
|
/// <param name="identity">The API key identity.</param>
|
|
/// <param name="commandKind">The kind of command denied.</param>
|
|
/// <param name="target">The target of the denied command.</param>
|
|
/// <param name="failure">The constraint failure details.</param>
|
|
/// <param name="correlationId">
|
|
/// The per-request client correlation id, if any. Stored on the audit record's
|
|
/// <c>CorrelationId</c> when it parses as a GUID; otherwise left null.
|
|
/// </param>
|
|
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
Task RecordDenialAsync(
|
|
ApiKeyIdentity? identity,
|
|
string commandKind,
|
|
string target,
|
|
ConstraintFailure failure,
|
|
string? correlationId,
|
|
CancellationToken cancellationToken);
|
|
|
|
/// <summary>
|
|
/// Records every constraint denial of one bulk command as a single aggregated audit event.
|
|
/// </summary>
|
|
/// <param name="identity">The API key identity.</param>
|
|
/// <param name="commandKind">The kind of bulk command whose items were denied.</param>
|
|
/// <param name="summary">The accumulated denials: count, constraint names and a bounded target sample.</param>
|
|
/// <param name="correlationId">The per-request client correlation id, if any (same handling as <see cref="RecordDenialAsync"/>).</param>
|
|
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
/// <remarks>
|
|
/// Bulk callers use this instead of one <see cref="RecordDenialAsync"/> per item: the audit
|
|
/// channel is bounded and drops on full, so per-item events let one bulk command evict every
|
|
/// other pending audit record.
|
|
/// </remarks>
|
|
Task RecordBulkDenialAsync(
|
|
ApiKeyIdentity? identity,
|
|
string commandKind,
|
|
ConstraintDenialSummary summary,
|
|
string? correlationId,
|
|
CancellationToken cancellationToken);
|
|
}
|