namespace KArtSell.Modules.ModelOperations.Domain; /// /// VS-02 DOMAIN: Security Master Synchronization Policy /// /// Handles: /// - Conflict resolution (last-write-wins) /// - Permission rule validation /// - Version management /// - Idempotency keys /// /// Pure logic, no I/O, testable, deterministic. /// public record SecurityRule( Guid RuleId, string ResourceName, string Action, int Version, DateTime EffectiveAt, DateTime? ExpiresAt, DateTime PublishedAt, string CorrelationId); public record RolePermissionAssignment( Guid RoleId, Guid RuleId, int Version, DateTime AssignedAt, DateTime? RemovedAt); public record SyncState( int LocalVersion, int RemoteVersion, List LocalRules, List RemoteRules, string IdempotencyKey, string CorrelationId); public record SyncResult( bool IsSuccess, int NewVersion, List AppliedRules, List Conflicts, string? ErrorMessage, string CorrelationId); public static class SecurityMasterPolicy { /// /// Determine sync action: accept, reject, or rollback /// /// Rules: /// 1. If localVersion >= remoteVersion: Already synced (idempotent) /// 2. If localVersion < remoteVersion: Accept all remote rules /// 3. Version conflict: Reject with 409 /// 4. Last-write-wins per rule (by PublishedAt timestamp) /// public static SyncResult ResolveSyncConflict(SyncState state) { if (state.LocalVersion > state.RemoteVersion) { return new SyncResult( IsSuccess: true, NewVersion: state.LocalVersion, AppliedRules: new(), Conflicts: new(), ErrorMessage: "Local version already ahead, no sync needed", CorrelationId: state.CorrelationId); } if (state.LocalVersion == state.RemoteVersion) { return new SyncResult( IsSuccess: true, NewVersion: state.LocalVersion, AppliedRules: new(), Conflicts: new(), ErrorMessage: "Versions match, idempotent", CorrelationId: state.CorrelationId); } var conflicts = new List(); var rulesToApply = new List(); foreach (var remoteRule in state.RemoteRules) { var localRule = state.LocalRules.FirstOrDefault(r => r.RuleId == remoteRule.RuleId); if (localRule == null) { rulesToApply.Add(remoteRule); continue; } if (localRule.PublishedAt < remoteRule.PublishedAt) { rulesToApply.Add(remoteRule); } else if (localRule.PublishedAt == remoteRule.PublishedAt && localRule.Version < remoteRule.Version) { rulesToApply.Add(remoteRule); conflicts.Add($"Version conflict on rule {remoteRule.RuleId}: local {localRule.Version}, remote {remoteRule.Version}"); } } return new SyncResult( IsSuccess: true, NewVersion: state.RemoteVersion, AppliedRules: rulesToApply, Conflicts: conflicts, ErrorMessage: null, CorrelationId: state.CorrelationId); } /// /// Validate rule before applying /// /// Checks: /// - Resource name not empty /// - Action in {read, write, execute} /// - EffectiveAt <= ExpiresAt (if set) /// - Timestamps in UTC /// public static (bool IsValid, List Errors) ValidateRule(SecurityRule rule) { var errors = new List(); if (string.IsNullOrWhiteSpace(rule.ResourceName)) errors.Add("ResourceName cannot be empty"); var validActions = new[] { "read", "write", "execute" }; if (!validActions.Contains(rule.Action.ToLowerInvariant())) errors.Add($"Action must be one of: {string.Join(", ", validActions)}"); if (rule.ExpiresAt.HasValue && rule.EffectiveAt > rule.ExpiresAt) errors.Add("EffectiveAt must be before or equal to ExpiresAt"); if (rule.PublishedAt.Kind != DateTimeKind.Utc) errors.Add("PublishedAt must be UTC"); return (errors.Count == 0, errors); } /// /// Check if rule is active at given time /// public static bool IsRuleActive(SecurityRule rule, DateTime? asOf = null) { var now = asOf ?? DateTime.UtcNow; if (now < rule.EffectiveAt) return false; if (rule.ExpiresAt.HasValue && now > rule.ExpiresAt) return false; return true; } /// /// Create idempotency key for sync operation /// Format: {fromVersion}:{correlationId} /// public static string CreateIdempotencyKey(int fromVersion, string correlationId) { return $"sync-{fromVersion}-{correlationId}"; } /// /// Detect rollback scenario: partial sync that failed mid-transaction /// /// If applied rules don't match version increment, rollback needed. /// public static bool RequiresRollback(int appliedRuleCount, int versionIncrement) { return appliedRuleCount == 0 && versionIncrement > 0; } }