fix(acl): require the live scripted-alarm owner to match the generation's deployed owner (#797)
Review of 3d1b86b4: OpcUaPublishActor installs a new authority generation
before applying its address space, and an apply that throws keeps the old one.
A scripted alarm moved from equipment A to B therefore stayed commandable with
A's grant under B's generation (briefly before the rebuild, or indefinitely
after a failed apply).
The generation now carries ScriptedAlarmOwners (ScriptedAlarmId -> EquipmentId,
decoded by ParseDataPlaneAuthority from the artifact's ScriptedAlarms rows, the
same rows the composition materialises from). AuthorizeScriptedCondition denies
unless the node manager's live binding names the same owner as the generation:
a moved alarm is denied under the old owner's, the new owner's and a Cluster
grant until the address space re-binds it; an alarm the generation no longer
deploys is denied. Install/apply order unchanged.
Tests: window (a) generation moved / binding stale => both owners and Cluster
denied, then rebuild under the new owner => new owner allowed, old denied;
window (b) apply failed after install => denied across clock and further
generations, then a successful rebuild restores the new owner only; undeployed
alarm denied; artifact decode of the owner map.
This commit is contained in:
+13
-2
@@ -244,7 +244,9 @@ public sealed class ScriptedAlarmOwnerScopeWireTests
|
||||
oldGrant.StatusCode.ShouldBe(StatusCodes.BadUserAccessDenied);
|
||||
routed.Count.ShouldBe(1);
|
||||
|
||||
Reapply(state, "rev-fan", [ServerEventRig.Key()], OwnerGrant(Fan), Observer());
|
||||
// The old grant was refused above with the generation STILL deploying the alarm under Pump (binding ≠
|
||||
// generation); now the deploy that moved it lands, granting the new owner.
|
||||
Reapply(state, "rev-fan", Deployed(alarmOwner: Fan), [ServerEventRig.Key()], OwnerGrant(Fan), Observer());
|
||||
await ConditionMethodEventTests.CallAsync(
|
||||
session, Uns(session, Alarm), "Acknowledge", events.LatestEventId(item, "recreated"), ct);
|
||||
|
||||
@@ -306,7 +308,16 @@ public sealed class ScriptedAlarmOwnerScopeWireTests
|
||||
}
|
||||
|
||||
private static void Reapply(DataPlaneAuthorityState state, string revision, ApiKey[] keys, params NodeAcl[] grants) =>
|
||||
state.Apply(ServerEventRig.Cluster, revision, keys, grants, Resolver().Resolve(grants), OwnerPaths());
|
||||
Reapply(state, revision, Deployed(), keys, grants);
|
||||
|
||||
private static void Reapply(
|
||||
DataPlaneAuthorityState state, string revision, IReadOnlyDictionary<string, string> deployedOwners,
|
||||
ApiKey[] keys, params NodeAcl[] grants) =>
|
||||
state.Apply(ServerEventRig.Cluster, revision, keys, grants, Resolver().Resolve(grants), OwnerPaths(), deployedOwners);
|
||||
|
||||
/// <summary>The generation's deployed scripted-alarm owners; <paramref name="alarmOwner"/> moves the Pump alarm.</summary>
|
||||
private static Dictionary<string, string> Deployed(string alarmOwner = Pump) =>
|
||||
new() { [Alarm] = alarmOwner, [FanAlarm] = Fan };
|
||||
|
||||
/// <summary>The key's ONLY alarm authority: an Equipment grant on <paramref name="equipmentId"/>.</summary>
|
||||
private static NodeAcl OwnerGrant(string equipmentId) => new()
|
||||
|
||||
Reference in New Issue
Block a user