File: Utils\SocketPermissionHelperTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj (Aspire.Hosting.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Net.Sockets;
using System.Security.AccessControl;
using System.Security.Principal;
using Aspire.Shared;
 
namespace Aspire.Hosting.Tests.Utils;
 
[Trait("Partition", "4")]
public sealed class SocketPermissionHelperTests
{
    private const UnixFileMode DirectoryMode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute;
    private const UnixFileMode SocketMode = UnixFileMode.UserRead | UnixFileMode.UserWrite;
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task Bind_ProtectsDirectoryAndSocket_AndAllowsOwnerConnection(bool existingDirectory)
    {
        var root = Directory.CreateTempSubdirectory();
        try
        {
            var directory = Path.Combine(root.FullName, "custom");
            if (existingDirectory)
            {
                SocketPermissionHelper.CreateDirectory(directory, repairExisting: false);
            }
 
            var socketPath = Path.Combine(directory, "s.sock");
            using var listener = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
            SocketPermissionHelper.Bind(listener, socketPath);
 
            AssertDirectoryPermissions(directory);
            if (OperatingSystem.IsWindows())
            {
                using var identity = WindowsIdentity.GetCurrent();
                var security = new FileInfo(socketPath).GetAccessControl();
                var rule = Assert.Single(security.GetAccessRules(true, true, typeof(SecurityIdentifier)).Cast<FileSystemAccessRule>());
                Assert.Equal(identity.User, rule.IdentityReference);
                Assert.Equal(AccessControlType.Allow, rule.AccessControlType);
                Assert.Equal(FileSystemRights.FullControl, rule.FileSystemRights);
            }
            else
            {
                Assert.Equal(SocketMode, File.GetUnixFileMode(socketPath));
            }
 
            listener.Listen();
            using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
            using var client = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
            await client.ConnectAsync(new UnixDomainSocketEndPoint(socketPath), timeout.Token);
            using var accepted = await listener.AcceptAsync(timeout.Token);
            Assert.True(accepted.Connected);
        }
        finally
        {
            root.Delete(recursive: true);
        }
    }
 
    [Fact]
    public void CreateDirectory_RepairsOwnedDirectoryBeforeAttemptingToBind()
    {
        var root = Directory.CreateTempSubdirectory();
        try
        {
            var directory = Path.Combine(root.FullName, ".aspire", "pty");
            MakePermissiveDirectory(directory);
            var socketPath = Path.Combine(directory, "occupied");
            File.WriteAllText(socketPath, "existing file");
 
            using var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
            SocketPermissionHelper.CreateDirectory(directory, repairExisting: true);
            Assert.Throws<SocketException>(() => SocketPermissionHelper.Bind(socket, socketPath));
 
            AssertDirectoryPermissions(directory);
            Assert.Null(socket.LocalEndPoint);
            Assert.Equal("existing file", File.ReadAllText(socketPath));
        }
        finally
        {
            root.Delete(recursive: true);
        }
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void CreateDirectory_ExistingOverrideKeepsPermissions(bool allowSystem)
    {
        var root = Directory.CreateTempSubdirectory();
        try
        {
            var directory = Path.Combine(root.FullName, "custom");
            SocketPermissionHelper.CreateDirectory(directory, repairExisting: false);
            if (OperatingSystem.IsWindows() && allowSystem)
            {
                var info = new DirectoryInfo(directory);
                var security = info.GetAccessControl();
                security.AddAccessRule(new FileSystemAccessRule(
                    new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null),
                    FileSystemRights.FullControl,
                    InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
                    PropagationFlags.None,
                    AccessControlType.Allow));
                info.SetAccessControl(security);
            }
            var originalPermissions = OperatingSystem.IsWindows()
                ? new DirectoryInfo(directory).GetAccessControl().GetSecurityDescriptorSddlForm(AccessControlSections.All)
                : File.GetUnixFileMode(directory).ToString();
 
            SocketPermissionHelper.CreateDirectory(directory, repairExisting: false);
 
            var actualPermissions = OperatingSystem.IsWindows()
                ? new DirectoryInfo(directory).GetAccessControl().GetSecurityDescriptorSddlForm(AccessControlSections.All)
                : File.GetUnixFileMode(directory).ToString();
            Assert.Equal(originalPermissions, actualPermissions);
        }
        finally
        {
            root.Delete(recursive: true);
        }
    }
 
    [Fact]
    public void CreateDirectory_ExistingWindowsOverrideRequiresInheritablePermissions()
    {
        if (!OperatingSystem.IsWindows())
        {
            return;
        }
 
        var root = Directory.CreateTempSubdirectory();
        try
        {
            var directory = SocketPermissionHelper.CreateDirectory(Path.Combine(root.FullName, "custom"), repairExisting: false);
            using var identity = WindowsIdentity.GetCurrent();
            var security = new DirectorySecurity();
            security.SetOwner(identity.User!);
            security.SetAccessRuleProtection(isProtected: true, preserveInheritance: false);
            security.AddAccessRule(new FileSystemAccessRule(
                identity.User!, FileSystemRights.FullControl, AccessControlType.Allow));
            directory.SetAccessControl(security);
            var originalPermissions = directory.GetAccessControl().GetSecurityDescriptorSddlForm(AccessControlSections.All);
 
            Assert.Throws<IOException>(() => SocketPermissionHelper.CreateDirectory(directory.FullName, repairExisting: false));
 
            Assert.Equal(originalPermissions, directory.GetAccessControl().GetSecurityDescriptorSddlForm(AccessControlSections.All));
        }
        finally
        {
            root.Delete(recursive: true);
        }
    }
 
    [Fact]
    public void Bind_DirectoryCreationFailure_DoesNotBind()
    {
        var root = Directory.CreateTempSubdirectory();
        try
        {
            Directory.CreateDirectory(Path.Combine(root.FullName, ".aspire"));
            var directory = Path.Combine(root.FullName, ".aspire", "pty");
            File.WriteAllText(directory, "not a directory");
 
            using var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
            Assert.ThrowsAny<IOException>(() => SocketPermissionHelper.Bind(socket, Path.Combine(directory, "s.sock")));
 
            Assert.Null(socket.LocalEndPoint);
            Assert.Equal("not a directory", File.ReadAllText(directory));
        }
        finally
        {
            root.Delete(recursive: true);
        }
    }
 
    [Fact]
    public void CreateDirectory_AllowsLinkedHomeAncestor()
    {
        if (OperatingSystem.IsWindows())
        {
            return;
        }
 
        var root = Directory.CreateTempSubdirectory();
        try
        {
            var target = Directory.CreateDirectory(Path.Combine(root.FullName, "target"));
            Directory.CreateDirectory(Path.Combine(target.FullName, "home"));
            var link = Path.Combine(root.FullName, "homes");
            Directory.CreateSymbolicLink(link, target.FullName);
            var profile = Path.Combine(link, "home");
            var path = Path.Combine(profile, ".aspire", "cli", "bch");
 
            var directory = SocketPermissionHelper.CreateDirectory(
                path, repairExisting: true, root.FullName, profile, root.FullName);
 
            Assert.Equal(path, directory.FullName);
            AssertDirectoryPermissions(Path.Combine(target.FullName, "home", ".aspire", "cli", "bch"));
        }
        finally
        {
            root.Delete(recursive: true);
        }
    }
 
    [Theory]
    [InlineData(false, false)]
    [InlineData(false, true)]
    [InlineData(true, false)]
    [InlineData(true, true)]
    public void CreateDirectory_AllowsLinkedTrustedBase(bool temporaryBase, bool repairExisting)
    {
        if (OperatingSystem.IsWindows())
        {
            return;
        }
 
        var root = Directory.CreateTempSubdirectory();
        try
        {
            var target = Directory.CreateDirectory(Path.Combine(root.FullName, "target"));
            var baseMode = File.GetUnixFileMode(target.FullName);
            var link = Path.Combine(root.FullName, "base");
            Directory.CreateSymbolicLink(link, target.FullName);
            var path = Path.Combine(link, ".aspire", "trmnl");
            var targetDirectory = Path.Combine(target.FullName, ".aspire", "trmnl");
            if (repairExisting)
            {
                MakePermissiveDirectory(targetDirectory);
            }
 
            var directory = SocketPermissionHelper.CreateDirectory(
                path, repairExisting, root.FullName,
                temporaryBase ? root.FullName : link,
                temporaryBase ? link : root.FullName);
 
            Assert.Equal(path, directory.FullName);
            AssertDirectoryPermissions(targetDirectory);
            Assert.Equal(baseMode, File.GetUnixFileMode(target.FullName));
 
            SocketPermissionHelper.CreateDirectory(
                path, repairExisting: false, root.FullName,
                temporaryBase ? root.FullName : link,
                temporaryBase ? link : root.FullName);
            AssertDirectoryPermissions(targetDirectory);
        }
        finally
        {
            root.Delete(recursive: true);
        }
    }
 
    [Theory]
    [InlineData(".aspire", false)]
    [InlineData(".aspire", true)]
    [InlineData(".aspire/cli", false)]
    [InlineData(".aspire/cli", true)]
    [InlineData(".aspire/cli/bch", false)]
    [InlineData(".aspire/cli/bch", true)]
    public void CreateDirectory_RejectsLinksBelowLinkedHome(string linkedSuffix, bool repairExisting)
    {
        if (OperatingSystem.IsWindows())
        {
            return;
        }
 
        var root = Directory.CreateTempSubdirectory();
        try
        {
            var home = Directory.CreateDirectory(Path.Combine(root.FullName, "home"));
            var profile = Path.Combine(root.FullName, "profile");
            Directory.CreateSymbolicLink(profile, home.FullName);
            var target = Path.Combine(root.FullName, "target");
            MakePermissiveDirectory(target);
            var originalMode = File.GetUnixFileMode(target);
            var link = Path.Combine(profile, linkedSuffix);
            Directory.CreateDirectory(Path.GetDirectoryName(link)!);
            Directory.CreateSymbolicLink(link, target);
 
            Assert.Throws<IOException>(() => SocketPermissionHelper.CreateDirectory(
                Path.Combine(profile, ".aspire", "cli", "bch"), repairExisting,
                root.FullName, profile, root.FullName));
 
            Assert.Equal(originalMode, File.GetUnixFileMode(target));
            Assert.Empty(Directory.EnumerateFileSystemEntries(target));
        }
        finally
        {
            root.Delete(recursive: true);
        }
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void CreateDirectory_RejectsInvalidLinkedHome(bool fileTarget)
    {
        if (OperatingSystem.IsWindows())
        {
            return;
        }
 
        var root = Directory.CreateTempSubdirectory();
        try
        {
            var target = Path.Combine(root.FullName, "target");
            if (fileTarget)
            {
                File.WriteAllText(target, "not a directory");
            }
            var profile = Path.Combine(root.FullName, "profile");
            Directory.CreateSymbolicLink(profile, target);
 
            Assert.ThrowsAny<IOException>(() => SocketPermissionHelper.CreateDirectory(
                Path.Combine(profile, ".aspire", "trmnl"), repairExisting: true,
                root.FullName, profile, root.FullName));
 
            Assert.False(Directory.Exists(target));
            if (fileTarget)
            {
                Assert.Equal("not a directory", File.ReadAllText(target));
            }
        }
        finally
        {
            root.Delete(recursive: true);
        }
    }
 
    [Fact]
    public void CreateDirectory_RejectsSymbolicLinkWithoutChangingTarget()
    {
        if (OperatingSystem.IsWindows())
        {
            // Unprivileged symlink creation is not available on every Windows test agent.
            return;
        }
 
        var root = Directory.CreateTempSubdirectory();
        try
        {
            var target = Path.Combine(root.FullName, "target");
            MakePermissiveDirectory(target);
            var originalMode = File.GetUnixFileMode(target);
            Directory.CreateDirectory(Path.Combine(root.FullName, ".aspire"));
            var link = Path.Combine(root.FullName, ".aspire", "pty");
            Directory.CreateSymbolicLink(link, target);
 
            Assert.Throws<IOException>(() => SocketPermissionHelper.CreateDirectory(link, repairExisting: false));
 
            Assert.Equal(originalMode, File.GetUnixFileMode(target));
        }
        finally
        {
            root.Delete(recursive: true);
        }
    }
 
    [Fact]
    public void CreateDirectory_RejectsSharedRoots()
    {
        Assert.Throws<IOException>(() => SocketPermissionHelper.CreateDirectory(Path.GetTempPath(), repairExisting: false));
        Assert.Throws<IOException>(() => SocketPermissionHelper.CreateDirectory(Path.GetPathRoot(Path.GetTempPath())!, repairExisting: false));
    }
 
    [Theory]
    [InlineData(".")]
    [InlineData("..")]
    [InlineData("home")]
    [InlineData("shared")]
    [InlineData(".aspire")]
    [InlineData(".aspire/cli")]
    [InlineData(".aspire/pty/..")]
    [InlineData(".aspire/pty")]
    [InlineData("aspire-dcp-test")]
    public void Bind_RejectsPermissiveOverrideWithoutChangingPermissions(string relativePath)
    {
        var root = Directory.CreateTempSubdirectory();
        try
        {
            var directory = Path.GetFullPath(Path.Combine(root.FullName, "base", relativePath));
            MakePermissiveDirectory(directory);
            var originalPermissions = OperatingSystem.IsWindows()
                ? new DirectoryInfo(directory).GetAccessControl().GetSecurityDescriptorSddlForm(AccessControlSections.All)
                : File.GetUnixFileMode(directory).ToString();
 
            var socketPath = Path.Combine(directory, "s.sock");
            using var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
            Assert.Throws<IOException>(() => SocketPermissionHelper.Bind(socket, socketPath));
            Assert.Null(socket.LocalEndPoint);
            Assert.False(File.Exists(socketPath));
 
            var actualPermissions = OperatingSystem.IsWindows()
                ? new DirectoryInfo(directory).GetAccessControl().GetSecurityDescriptorSddlForm(AccessControlSections.All)
                : File.GetUnixFileMode(directory).ToString();
            Assert.Equal(originalPermissions, actualPermissions);
        }
        finally
        {
            root.Delete(recursive: true);
        }
    }
 
    [Theory]
    [InlineData("working")]
    [InlineData("profile")]
    [InlineData("temp")]
    public void CreateDirectory_RejectsSuppliedEnvironmentDirectoryWithoutChangingPermissions(string environmentDirectory)
    {
        var root = Directory.CreateTempSubdirectory();
        try
        {
            // Use valid permissions so rejection exercises the environment checks.
            var directory = Path.Combine(root.FullName, ".aspire", "pty");
            SocketPermissionHelper.CreateDirectory(directory, repairExisting: false);
            var originalPermissions = OperatingSystem.IsWindows()
                ? new DirectoryInfo(directory).GetAccessControl().GetSecurityDescriptorSddlForm(AccessControlSections.All)
                : File.GetUnixFileMode(directory).ToString();
            var currentDirectory = environmentDirectory == "working" ? directory : root.FullName;
            var userProfileDirectory = environmentDirectory == "profile" ? directory : root.FullName;
            var tempDirectory = environmentDirectory == "temp" ? directory : root.FullName;
 
            Assert.Throws<IOException>(() => SocketPermissionHelper.CreateDirectory(
                environmentDirectory == "working" ? "." : directory, repairExisting: false,
                currentDirectory, userProfileDirectory, tempDirectory));
 
            var actualPermissions = OperatingSystem.IsWindows()
                ? new DirectoryInfo(directory).GetAccessControl().GetSecurityDescriptorSddlForm(AccessControlSections.All)
                : File.GetUnixFileMode(directory).ToString();
            Assert.Equal(originalPermissions, actualPermissions);
        }
        finally
        {
            root.Delete(recursive: true);
        }
    }
 
    [Fact]
    public void CreateDirectory_ResolvesRelativePathAgainstSuppliedWorkingDirectory()
    {
        var root = Directory.CreateTempSubdirectory();
        try
        {
            var directory = SocketPermissionHelper.CreateDirectory(
                "custom", repairExisting: false, root.FullName, root.FullName, root.FullName);
 
            Assert.Equal(Path.Combine(root.FullName, "custom"), directory.FullName);
            AssertDirectoryPermissions(directory.FullName);
        }
        finally
        {
            root.Delete(recursive: true);
        }
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void CreateDirectory_OverrideNameDoesNotGrantPermissionRepair(bool dcpLayout)
    {
        var root = Directory.CreateTempSubdirectory();
        try
        {
            var directory = Path.Combine(root.FullName, dcpLayout ? "aspire-dcp-test" : ".aspire/pty");
            MakePermissiveDirectory(directory);
            var originalPermissions = OperatingSystem.IsWindows()
                ? new DirectoryInfo(directory).GetAccessControl().GetSecurityDescriptorSddlForm(AccessControlSections.All)
                : File.GetUnixFileMode(directory).ToString();
 
            Assert.Throws<IOException>(() => SocketPermissionHelper.CreateDirectory(
                directory, repairExisting: false, root.FullName, root.FullName, root.FullName));
 
            var actualPermissions = OperatingSystem.IsWindows()
                ? new DirectoryInfo(directory).GetAccessControl().GetSecurityDescriptorSddlForm(AccessControlSections.All)
                : File.GetUnixFileMode(directory).ToString();
            Assert.Equal(originalPermissions, actualPermissions);
        }
        finally
        {
            root.Delete(recursive: true);
        }
    }
 
    [Fact]
    public void CreateDirectory_RejectsLinkedAspireParentWithoutChangingTarget()
    {
        if (OperatingSystem.IsWindows())
        {
            return;
        }
 
        var root = Directory.CreateTempSubdirectory();
        try
        {
            var target = Directory.CreateDirectory(Path.Combine(root.FullName, "target"));
            var directory = Path.Combine(target.FullName, "pty");
            MakePermissiveDirectory(directory);
            var originalMode = File.GetUnixFileMode(directory);
            Directory.CreateSymbolicLink(Path.Combine(root.FullName, ".aspire"), target.FullName);
 
            Assert.Throws<IOException>(() => SocketPermissionHelper.CreateDirectory(
                Path.Combine(root.FullName, ".aspire", "pty"), repairExisting: false));
 
            Assert.Equal(originalMode, File.GetUnixFileMode(directory));
        }
        finally
        {
            root.Delete(recursive: true);
        }
    }
 
    [Fact]
    public void CreateDirectory_RejectsSharedStickyDirectory()
    {
        if (OperatingSystem.IsWindows())
        {
            return;
        }
 
        var root = Directory.CreateTempSubdirectory();
        try
        {
            var directory = Directory.CreateDirectory(Path.Combine(root.FullName, ".aspire", "pty")).FullName;
            var originalMode = DirectoryMode | UnixFileMode.StickyBit | UnixFileMode.OtherRead |
                UnixFileMode.OtherWrite | UnixFileMode.OtherExecute;
            File.SetUnixFileMode(directory, originalMode);
 
            Assert.Throws<IOException>(() => SocketPermissionHelper.CreateDirectory(directory, repairExisting: true));
 
            Assert.Equal(originalMode, File.GetUnixFileMode(directory));
        }
        finally
        {
            root.Delete(recursive: true);
        }
    }
 
    private static void MakePermissiveDirectory(string path)
    {
        var directory = Directory.CreateDirectory(path);
        if (OperatingSystem.IsWindows())
        {
            var security = directory.GetAccessControl();
            security.AddAccessRule(new FileSystemAccessRule(
                new SecurityIdentifier(WellKnownSidType.WorldSid, null),
                FileSystemRights.FullControl,
                InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
                PropagationFlags.None,
                AccessControlType.Allow));
            directory.SetAccessControl(security);
        }
        else
        {
            File.SetUnixFileMode(path, DirectoryMode |
                UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute |
                UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute);
        }
    }
 
    private static void AssertDirectoryPermissions(string path)
    {
        if (OperatingSystem.IsWindows())
        {
            using var identity = WindowsIdentity.GetCurrent();
            var security = new DirectoryInfo(path).GetAccessControl();
            Assert.True(security.AreAccessRulesProtected);
            Assert.Equal(identity.User, security.GetOwner(typeof(SecurityIdentifier)));
            var rule = Assert.Single(security.GetAccessRules(true, true, typeof(SecurityIdentifier)).Cast<FileSystemAccessRule>());
            Assert.Equal(identity.User, rule.IdentityReference);
            Assert.Equal(AccessControlType.Allow, rule.AccessControlType);
            Assert.Equal(FileSystemRights.FullControl, rule.FileSystemRights);
            Assert.Equal(InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, rule.InheritanceFlags);
        }
        else
        {
            Assert.Equal(DirectoryMode, File.GetUnixFileMode(path));
        }
    }
}