fix(admin): resolve Medium code-review finding (Admin-008)

Add @ReleasedBy parameter to sp_ReleaseExternalIdReservation via a new EF
migration so the operator principal (not the shared SQL account) is recorded
in ExternalIdReservation.ReleasedBy and ConfigAuditLog.Principal.
ReservationService.ReleaseAsync gains a releasedBy parameter; Reservations.razor
resolves the signed-in user from AuthenticationState and passes it through.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-22 07:29:54 -04:00
parent 71f91aa57c
commit 328ab1e614
4 changed files with 152 additions and 6 deletions

View File

@@ -0,0 +1,120 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
{
/// <summary>
/// Admin-008: adds <c>@ReleasedBy</c> parameter to
/// <c>dbo.sp_ReleaseExternalIdReservation</c> so the operator principal name (the LDAP
/// sign-in) is recorded in <c>ExternalIdReservation.ReleasedBy</c> and the
/// <c>ConfigAuditLog.Principal</c> column.
///
/// Prior to this migration the proc used <c>SUSER_SNAME()</c> for both columns, which
/// recorded the shared SQL service account rather than the Admin-UI operator who performed
/// the release — making the audit trail useless for attribution. The stored proc now
/// accepts <c>@ReleasedBy nvarchar(128)</c> and uses it for both columns; validation
/// rejects a null/empty value the same way <c>@ReleaseReason</c> is validated.
/// </summary>
/// <inheritdoc />
public partial class AddReleasedByToReleaseExternalIdReservation : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(Procs.ReleaseExternalIdReservationV2);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(Procs.ReleaseExternalIdReservationV1);
}
private static class Procs
{
/// <summary>V2 — accepts <c>@ReleasedBy</c> for proper operator attribution.</summary>
public const string ReleaseExternalIdReservationV2 = @"
CREATE OR ALTER PROCEDURE dbo.sp_ReleaseExternalIdReservation
@Kind nvarchar(16),
@Value nvarchar(64),
@ReleaseReason nvarchar(512),
@ReleasedBy nvarchar(128)
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
IF @ReleaseReason IS NULL OR LEN(@ReleaseReason) = 0
BEGIN
RAISERROR('ReleaseReason is required', 16, 1);
RETURN;
END
IF @ReleasedBy IS NULL OR LEN(@ReleasedBy) = 0
BEGIN
RAISERROR('ReleasedBy is required', 16, 1);
RETURN;
END
UPDATE dbo.ExternalIdReservation
SET ReleasedAt = SYSUTCDATETIME(),
ReleasedBy = @ReleasedBy,
ReleaseReason = @ReleaseReason
WHERE Kind = @Kind AND Value = @Value AND ReleasedAt IS NULL;
IF @@ROWCOUNT = 0
BEGIN
RAISERROR('No active reservation found for (%s, %s)', 16, 1, @Kind, @Value);
RETURN;
END
-- Escape all caller-supplied values via STRING_ESCAPE so quotes/backslashes cannot break the
-- JSON document or inject additional structure into the audit record.
INSERT dbo.ConfigAuditLog (Principal, EventType, DetailsJson)
VALUES (@ReleasedBy, 'ExternalIdReleased',
CONCAT('{""kind"":""', STRING_ESCAPE(@Kind, 'json'),
'"",""value"":""', STRING_ESCAPE(@Value, 'json'), '""}'));
END
";
/// <summary>V1 — original proc (uses SUSER_SNAME() for attribution). Restored on Down().</summary>
public const string ReleaseExternalIdReservationV1 = @"
CREATE OR ALTER PROCEDURE dbo.sp_ReleaseExternalIdReservation
@Kind nvarchar(16),
@Value nvarchar(64),
@ReleaseReason nvarchar(512)
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
IF @ReleaseReason IS NULL OR LEN(@ReleaseReason) = 0
BEGIN
RAISERROR('ReleaseReason is required', 16, 1);
RETURN;
END
UPDATE dbo.ExternalIdReservation
SET ReleasedAt = SYSUTCDATETIME(),
ReleasedBy = SUSER_SNAME(),
ReleaseReason = @ReleaseReason
WHERE Kind = @Kind AND Value = @Value AND ReleasedAt IS NULL;
IF @@ROWCOUNT = 0
BEGIN
RAISERROR('No active reservation found for (%s, %s)', 16, 1, @Kind, @Value);
RETURN;
END
-- Escape both caller-supplied values via STRING_ESCAPE so quotes/backslashes cannot break the
-- JSON document or inject additional structure into the audit record.
INSERT dbo.ConfigAuditLog (Principal, EventType, DetailsJson)
VALUES (SUSER_SNAME(), 'ExternalIdReleased',
CONCAT('{""kind"":""', STRING_ESCAPE(@Kind, 'json'),
'"",""value"":""', STRING_ESCAPE(@Value, 'json'), '""}'));
END
";
}
}
}