1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
|
using System.Security.AccessControl;
using System.Security.Principal;
using Agent.Windows.Recovery;
namespace Agent.Windows.Security;
public sealed record AclSnapshot(string Path, byte[] SecurityDescriptor, Guid? JournalEntryId = null);
public sealed class TaskAclManager(SecurityIdentifier appContainerSid, ISecurityAuditSink audit, TaskResourceJournal? journal = null)
{
public AclSnapshot Grant(Guid taskId, string path, FileCapability capabilities)
{
string canonical = ResolvedPath.Resolve(path);
FileSystemSecurity security = ReadSecurity(canonical);
byte[] original = security.GetSecurityDescriptorBinaryForm();
Guid? journalEntry = journal?.Prepare("acl", canonical, new Dictionary<string, string>
{
["security_descriptor"] = Convert.ToBase64String(original)
});
bool directory = Directory.Exists(canonical);
FileSystemRights auditRights;
if (directory)
{
FileSystemRights directoryRights = ToDirectoryRights(capabilities);
FileSystemRights fileRights = ToFileRights(capabilities);
if (directoryRights == 0 || fileRights == 0) throw new ArgumentOutOfRangeException(nameof(capabilities));
security.AddAccessRule(new FileSystemAccessRule(appContainerSid, directoryRights,
InheritanceFlags.ContainerInherit, PropagationFlags.None, AccessControlType.Allow));
security.AddAccessRule(new FileSystemAccessRule(appContainerSid, fileRights,
InheritanceFlags.ObjectInherit, PropagationFlags.InheritOnly, AccessControlType.Allow));
auditRights = directoryRights | fileRights;
}
else
{
auditRights = ToFileRights(capabilities);
if (auditRights == 0) throw new ArgumentOutOfRangeException(nameof(capabilities));
security.AddAccessRule(new FileSystemAccessRule(appContainerSid, auditRights,
InheritanceFlags.None, PropagationFlags.None, AccessControlType.Allow));
}
try
{
WriteSecurity(canonical, security);
if (journalEntry is not null) journal!.MarkApplied(journalEntry.Value);
}
catch (Exception applyFailure)
{
try
{
byte[] current = ReadSecurity(canonical).GetSecurityDescriptorBinaryForm();
if (!current.AsSpan().SequenceEqual(original))
{
FileSystemSecurity rollback = Directory.Exists(canonical) ? new DirectorySecurity() : new FileSecurity();
rollback.SetSecurityDescriptorBinaryForm(original, AccessControlSections.Access);
WriteSecurity(canonical, rollback);
byte[] restored = ReadSecurity(canonical).GetSecurityDescriptorBinaryForm();
if (!restored.AsSpan().SequenceEqual(original)) throw new InvalidOperationException("ACL rollback verification failed.");
}
if (journalEntry is not null) journal!.MarkCleaned(journalEntry.Value);
}
catch (Exception rollbackFailure)
{
throw new AggregateException("ACL apply failed and immediate rollback did not reach a verified clean state.", applyFailure, rollbackFailure);
}
throw;
}
audit.Write(new(DateTimeOffset.UtcNow, taskId, "acl.apply", "applied", new Dictionary<string, string>
{
["path"] = canonical,
["sid"] = appContainerSid.Value,
["rights"] = auditRights.ToString(),
["scope"] = directory ? "root_inheritance" : "file"
}));
return new(canonical, original, journalEntry);
}
public void Restore(Guid taskId, AclSnapshot snapshot)
{
FileSystemSecurity security = Directory.Exists(snapshot.Path) ? new DirectorySecurity() : new FileSecurity();
security.SetSecurityDescriptorBinaryForm(snapshot.SecurityDescriptor, AccessControlSections.Access);
WriteSecurity(snapshot.Path, security);
if (snapshot.JournalEntryId is not null) journal!.MarkCleaned(snapshot.JournalEntryId.Value);
audit.Write(new(DateTimeOffset.UtcNow, taskId, "acl.restore", "restored", new Dictionary<string, string> { ["path"] = snapshot.Path }));
}
private static FileSystemSecurity ReadSecurity(string path) => Directory.Exists(path)
? FileSystemAclExtensions.GetAccessControl(new DirectoryInfo(path), AccessControlSections.Access)
: FileSystemAclExtensions.GetAccessControl(new FileInfo(path), AccessControlSections.Access);
private static void WriteSecurity(string path, FileSystemSecurity security)
{
if (security is DirectorySecurity directorySecurity) FileSystemAclExtensions.SetAccessControl(new DirectoryInfo(path), directorySecurity);
else FileSystemAclExtensions.SetAccessControl(new FileInfo(path), (FileSecurity)security);
}
private static FileSystemRights ToFileRights(FileCapability capabilities)
{
FileSystemRights rights = 0;
if ((capabilities & FileCapability.Read) != 0)
rights |= FileSystemRights.ReadData | FileSystemRights.ReadAttributes | FileSystemRights.ReadExtendedAttributes | FileSystemRights.ReadPermissions;
if ((capabilities & FileCapability.Write) != 0) rights |= FileSystemRights.Write;
if ((capabilities & FileCapability.Execute) != 0) rights |= FileSystemRights.ExecuteFile | FileSystemRights.ReadData | FileSystemRights.ReadAttributes;
if ((capabilities & FileCapability.Delete) != 0) rights |= FileSystemRights.Delete;
return rights;
}
private static FileSystemRights ToDirectoryRights(FileCapability capabilities)
{
FileSystemRights rights = FileSystemRights.Traverse | FileSystemRights.ReadAttributes | FileSystemRights.ReadPermissions;
if ((capabilities & FileCapability.Read) != 0) rights |= FileSystemRights.ListDirectory | FileSystemRights.ReadExtendedAttributes;
if ((capabilities & FileCapability.Write) != 0) rights |= FileSystemRights.CreateFiles | FileSystemRights.CreateDirectories |
FileSystemRights.WriteAttributes | FileSystemRights.WriteExtendedAttributes;
if ((capabilities & FileCapability.Delete) != 0) rights |= FileSystemRights.Delete | FileSystemRights.DeleteSubdirectoriesAndFiles;
return rights;
}
}
|