Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Protect-AzDoCachePath.ps1

<#
.SYNOPSIS
    Restricts a file or directory the module writes under AZDODSC_CACHE_DIRECTORY to its owner.
 
.DESCRIPTION
    Everything the module persists to the cache directory is security relevant. ModuleSettings.clixml
    carries the credential (a PAT, client secret, PFX password or access token) and, outside Windows,
    Export-Clixml stores a SecureString in a recoverable form, so file permissions are the only thing
    protecting it. The identity caches under Cache\ map names to descriptors, so anyone who can write
    them can redirect a permission grant. Both are created with default permissions unless this is
    called after they are written.
 
    On Windows the ACL is replaced with an explicit, non-inherited DACL that grants FullControl to
    the current user, SYSTEM and BUILTIN\Administrators only; a directory's rules inherit to what is
    created under it. Elsewhere the mode is set to 600 for a file and 700 for a directory, through
    [System.IO.File]::SetUnixFileMode where the runtime has it (.NET 7 / PowerShell 7.3 and later)
    and chmod otherwise.
 
    The function never throws. A path that does not exist is ignored silently, and any failure
    to apply the permissions is reported as one warning naming the path, so the caller's write
    is never undone by a permissions problem.
 
.PARAMETER Path
    The file or directory to protect.
 
.EXAMPLE
    Protect-AzDoCachePath -Path $ENV:AZDODSC_CACHE_DIRECTORY
    Protect-AzDoCachePath -Path (Join-Path $ENV:AZDODSC_CACHE_DIRECTORY 'ModuleSettings.clixml')
#>

function Protect-AzDoCachePath
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Path
    )

    try
    {
        if (-not (Test-Path -LiteralPath $Path))
        {
            Write-Verbose "[Protect-AzDoCachePath] '$Path' does not exist; nothing to protect."
            return
        }

        $item        = Get-Item -LiteralPath $Path -Force
        $isDirectory = [bool]$item.PSIsContainer

        if (Test-AzDoIsWindows)
        {
            Write-Verbose "[Protect-AzDoCachePath] Replacing the ACL on '$Path' with an owner-only DACL."

            $rights      = [System.Security.AccessControl.FileSystemRights]::FullControl
            $propagation = [System.Security.AccessControl.PropagationFlags]::None
            $allow       = [System.Security.AccessControl.AccessControlType]::Allow
            $inheritance = if ($isDirectory)
            {
                [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
                [System.Security.AccessControl.InheritanceFlags]::ObjectInherit
            }
            else
            {
                [System.Security.AccessControl.InheritanceFlags]::None
            }

            $identities = @(
                [System.Security.Principal.WindowsIdentity]::GetCurrent().User,
                [System.Security.Principal.SecurityIdentifier]::new('S-1-5-18'),       # NT AUTHORITY\SYSTEM
                [System.Security.Principal.SecurityIdentifier]::new('S-1-5-32-544')    # BUILTIN\Administrators
            )

            $acl = Get-Acl -LiteralPath $Path

            # Stop inheriting from the parent and drop the inherited rules rather than copying them.
            $acl.SetAccessRuleProtection($true, $false)

            # Remove every explicit rule that is already there so the result is exactly the three below.
            foreach ($existing in @($acl.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier])))
            {
                $null = $acl.RemoveAccessRule($existing)
            }

            foreach ($identity in $identities)
            {
                $rule = [System.Security.AccessControl.FileSystemAccessRule]::new($identity, $rights, $inheritance, $propagation, $allow)
                $acl.AddAccessRule($rule)
            }

            Set-Acl -LiteralPath $Path -AclObject $acl
        }
        else
        {
            $octal = if ($isDirectory) { '700' } else { '600' }
            Write-Verbose "[Protect-AzDoCachePath] Setting mode $octal on '$Path'."

            # [System.IO.File]::SetUnixFileMode arrived with .NET 7 (PowerShell 7.3). Resolve the enum
            # at run time so this parses and runs on older runtimes, where chmod does the same job.
            $unixFileModeType = 'System.IO.UnixFileMode' -as [System.Type]
            $setUnixFileMode  = $null
            if ($null -ne $unixFileModeType)
            {
                $setUnixFileMode = [System.IO.File].GetMethod('SetUnixFileMode', [System.Type[]]@([System.String], $unixFileModeType))
            }

            if ($null -ne $setUnixFileMode)
            {
                $modeName = if ($isDirectory) { 'UserRead, UserWrite, UserExecute' } else { 'UserRead, UserWrite' }
                $mode     = [System.Enum]::Parse($unixFileModeType, $modeName)
                $null     = $setUnixFileMode.Invoke($null, @($Path, $mode))
            }
            else
            {
                $chmod  = Get-Command -Name 'chmod' -CommandType Application -ErrorAction Stop | Select-Object -First 1
                $output = & $chmod.Source $octal -- $Path 2>&1
                if ($LASTEXITCODE -ne 0)
                {
                    throw "chmod exited with code $LASTEXITCODE. $output"
                }
            }
        }
    }
    catch
    {
        Write-Warning "[Protect-AzDoCachePath] Could not restrict the permissions on '$Path'. It may be readable by other accounts on this machine. $($_.Exception.Message)"
    }
}