fix(security): enforce template locks on native alarm source overrides at flatten and management-command level
Add IsLocked to ResolvedNativeAlarmSource; FlatteningService skips overrides on a locked source (mirrors attribute/alarm lock rules); ManagementActor SetInstanceNativeAlarmSourceOverride flattens the instance and rejects an override on a locked source (and rejects a canonical name that does not resolve, preventing dangling overrides). Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -146,6 +146,12 @@ public sealed record ResolvedNativeAlarmSource
|
||||
public string? ConditionFilter { get; init; }
|
||||
/// <summary>Gets the source of this binding: "Template", "Inherited", "Composed", or "Override".</summary>
|
||||
public string Source { get; init; } = "Template";
|
||||
/// <summary>
|
||||
/// Gets whether this native alarm source is locked at the template level. A locked
|
||||
/// source cannot be repointed by an instance override — mirrors the attribute/alarm
|
||||
/// lock semantics.
|
||||
/// </summary>
|
||||
public bool IsLocked { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -885,6 +885,27 @@ public class ManagementActor : ReceiveActor
|
||||
await EnforceSiteScopeForInstance(sp, user, cmd.InstanceId);
|
||||
var repo = sp.GetRequiredService<ITemplateEngineRepository>();
|
||||
|
||||
// Enforce template-level locks (and reject dangling overrides) before persisting.
|
||||
// Flatten the instance (script compilation skipped — a lock check does not need it)
|
||||
// and look up the resolved source by canonical name: a locked source cannot be
|
||||
// overridden, and an unresolved name would create a dead override the flattener
|
||||
// silently drops.
|
||||
var pipeline = sp.GetRequiredService<IFlatteningPipeline>();
|
||||
var flattenResult = await pipeline.FlattenAndValidateAsync(
|
||||
cmd.InstanceId, default, validateScripts: false);
|
||||
if (flattenResult.IsFailure)
|
||||
throw new ManagementCommandException(
|
||||
$"Cannot set native alarm source override: the instance could not be flattened ({flattenResult.Error}).");
|
||||
|
||||
var resolvedSource = flattenResult.Value.Configuration.NativeAlarmSources
|
||||
.FirstOrDefault(s => string.Equals(s.CanonicalName, cmd.SourceCanonicalName, StringComparison.Ordinal));
|
||||
if (resolvedSource == null)
|
||||
throw new ManagementCommandException(
|
||||
$"Native alarm source '{cmd.SourceCanonicalName}' does not resolve for this instance and cannot be overridden.");
|
||||
if (resolvedSource.IsLocked)
|
||||
throw new ManagementCommandException(
|
||||
$"Native alarm source '{cmd.SourceCanonicalName}' is locked at the template level and cannot be overridden.");
|
||||
|
||||
var existing = await repo.GetNativeAlarmSourceOverrideAsync(cmd.InstanceId, cmd.SourceCanonicalName);
|
||||
if (existing == null)
|
||||
{
|
||||
|
||||
@@ -718,7 +718,8 @@ public class FlatteningService
|
||||
ConnectionName = binding.ConnectionName,
|
||||
SourceReference = binding.SourceReference,
|
||||
ConditionFilter = binding.ConditionFilter,
|
||||
Source = source
|
||||
Source = source,
|
||||
IsLocked = binding.IsLocked || lockedNames.Contains(binding.Name)
|
||||
};
|
||||
|
||||
if (binding.IsLocked)
|
||||
@@ -811,6 +812,9 @@ public class FlatteningService
|
||||
if (!sources.TryGetValue(ovr.SourceCanonicalName, out var existing))
|
||||
continue; // Cannot add new bindings via overrides.
|
||||
|
||||
if (existing.IsLocked)
|
||||
continue; // Locked at the template level — instance override is ignored.
|
||||
|
||||
sources[ovr.SourceCanonicalName] = existing with
|
||||
{
|
||||
ConnectionName = ovr.ConnectionNameOverride ?? existing.ConnectionName,
|
||||
|
||||
Reference in New Issue
Block a user