summaryrefslogtreecommitdiff
path: root/src/Agent.Windows/Security/TaskAclManager.cs
blob: be0080d1cdd071952323bdbc646b82c3c727ba7a (plain)
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;
    }
}