diff --git a/src/Files.App/Data/Contracts/IStorageSecurityService.cs b/src/Files.App/Data/Contracts/IStorageSecurityService.cs index 6aba4c92554f..95554e1fdad9 100644 --- a/src/Files.App/Data/Contracts/IStorageSecurityService.cs +++ b/src/Files.App/Data/Contracts/IStorageSecurityService.cs @@ -33,6 +33,14 @@ public interface IStorageSecurityService /// If the function succeeds, an instance of AccessControlList; otherwise, null. To get extended error information, call GetLastError. WIN32_ERROR GetAcl(string path, bool isFolder, out AccessControlList acl); + /// + /// Check whether the current process is allowed to write the DACL of the object specified by the path. + /// + /// The file full path + /// Whether the path indicates a folder or not + /// False if the write is known to be denied; otherwise, true. + bool CanWriteAcl(string path, bool isFolder); + /// /// Add an default Access Control Entry (ACE) to the specified object's DACL /// diff --git a/src/Files.App/Services/Storage/StorageSecurityService.cs b/src/Files.App/Services/Storage/StorageSecurityService.cs index 46c57d784860..60a7172dc32c 100644 --- a/src/Files.App/Services/Storage/StorageSecurityService.cs +++ b/src/Files.App/Services/Storage/StorageSecurityService.cs @@ -7,6 +7,7 @@ using Windows.Win32.Foundation; using Windows.Win32.Security; using Windows.Win32.Security.Authorization; +using Windows.Win32.Storage.FileSystem; using Windows.Win32.System.Memory; using SystemSecurity = System.Security.AccessControl; @@ -79,9 +80,17 @@ public unsafe bool SetOwner(string path, string sid) // Run PowerShell as Admin if (result is not WIN32_ERROR.ERROR_SUCCESS) { - return Win32Helper.RunPowershellCommand( - $"-command \"try {{ $path = {Win32Helper.ToPowerShellStringLiteral(path)}; $ID = new-object System.Security.Principal.SecurityIdentifier({Win32Helper.ToPowerShellStringLiteral(sid)}); $acl = get-acl -LiteralPath $path; $acl.SetOwner($ID); set-acl -LiteralPath $path -aclObject $acl }} catch {{ exit 1; }}\"", + var script = SetOwnerScript + .Replace("__PATH__", Win32Helper.ToPowerShellStringLiteral(path)) + .Replace("__SID__", Win32Helper.ToPowerShellStringLiteral(sid)); + + var encodedScript = Convert.ToBase64String(System.Text.Encoding.Unicode.GetBytes(script)); + + Win32Helper.RunPowershellCommand( + $"-NoProfile -EncodedCommand {encodedScript}", PowerShellExecutionOptions.Elevated | PowerShellExecutionOptions.Hidden); + + return string.Equals(GetOwner(path), sid, StringComparison.OrdinalIgnoreCase); } return true; @@ -92,6 +101,89 @@ public unsafe bool SetOwner(string path, string sid) } } + private const string SetOwnerScript = """ + Add-Type -TypeDefinition @' + using System; + using System.Runtime.InteropServices; + + public static class FilesSetOwner + { + [StructLayout(LayoutKind.Sequential)] + public struct LUID { public uint LowPart; public int HighPart; } + + [StructLayout(LayoutKind.Sequential)] + public struct LUID_AND_ATTRIBUTES { public LUID Luid; public uint Attributes; } + + [StructLayout(LayoutKind.Sequential)] + public struct TOKEN_PRIVILEGES { public uint PrivilegeCount; public LUID_AND_ATTRIBUTES Privilege; } + + private const int SE_FILE_OBJECT = 1; + private const uint OWNER_SECURITY_INFORMATION = 0x00000001; + + [DllImport("kernel32.dll")] + private static extern IntPtr GetCurrentProcess(); + + [DllImport("kernel32.dll")] + private static extern IntPtr LocalFree(IntPtr handle); + + [DllImport("kernel32.dll")] + private static extern bool CloseHandle(IntPtr handle); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool OpenProcessToken(IntPtr process, uint access, out IntPtr token); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool LookupPrivilegeValue(string host, string name, out LUID luid); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool AdjustTokenPrivileges(IntPtr token, bool disableAll, ref TOKEN_PRIVILEGES state, uint length, IntPtr previous, IntPtr returnLength); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool ConvertStringSidToSid(string sid, out IntPtr pSid); + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode)] + private static extern uint SetNamedSecurityInfo(string objectName, int objectType, uint securityInformation, IntPtr owner, IntPtr group, IntPtr dacl, IntPtr sacl); + + public static uint Run(string path, string sid) + { + IntPtr token; + if (OpenProcessToken(GetCurrentProcess(), 0x0020 | 0x0008, out token)) + { + string[] names = new string[] { "SeTakeOwnershipPrivilege", "SeRestorePrivilege" }; + + foreach (string name in names) + { + LUID luid; + if (!LookupPrivilegeValue(null, name, out luid)) + continue; + + TOKEN_PRIVILEGES privileges = new TOKEN_PRIVILEGES(); + privileges.PrivilegeCount = 1; + privileges.Privilege.Luid = luid; + privileges.Privilege.Attributes = 0x0002; + + AdjustTokenPrivileges(token, false, ref privileges, (uint)Marshal.SizeOf(privileges), IntPtr.Zero, IntPtr.Zero); + } + + CloseHandle(token); + } + + IntPtr pSid; + if (!ConvertStringSidToSid(sid, out pSid)) + return 87; + + uint result = SetNamedSecurityInfo(path, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION, pSid, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + + LocalFree(pSid); + + return result; + } + } + '@ + + exit [FilesSetOwner]::Run(__PATH__, __SID__) + """; + /// public unsafe WIN32_ERROR GetAcl(string path, bool isFolder, out AccessControlList acl) { @@ -199,6 +291,26 @@ public unsafe WIN32_ERROR GetAcl(string path, bool isFolder, out AccessControlLi } } + /// + public bool CanWriteAcl(string path, bool isFolder) + { + using var handle = PInvoke.CreateFile( + path, + (uint)FILE_ACCESS_RIGHTS.WRITE_DAC, + FILE_SHARE_MODE.FILE_SHARE_READ | FILE_SHARE_MODE.FILE_SHARE_WRITE | FILE_SHARE_MODE.FILE_SHARE_DELETE, + null, + FILE_CREATION_DISPOSITION.OPEN_EXISTING, + isFolder ? FILE_FLAGS_AND_ATTRIBUTES.FILE_FLAG_BACKUP_SEMANTICS : FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL, + null); + + if (!handle.IsInvalid) + return true; + + // Only an explicit denial proves the write would fail. Any other failure is + // inconclusive, so let the attempt through and report the real error instead + return (WIN32_ERROR)Marshal.GetLastPInvokeError() != WIN32_ERROR.ERROR_ACCESS_DENIED; + } + /// public unsafe WIN32_ERROR AddAce(string szPath, bool isFolder, string szSid) { diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw index 8a545f03cf78..fe0343516de0 100644 --- a/src/Files.App/Strings/en-US/Resources.resw +++ b/src/Files.App/Strings/en-US/Resources.resw @@ -3234,6 +3234,9 @@ Click 'Advanced permissions' to continue. + + You must be the owner of this item, or have permission to change permissions, to edit these entries. + You must have Read permissions to view the properties of this item. @@ -3243,6 +3246,18 @@ Unable to display permissions. + + Unable to change the owner of this item. + + + Changing owner... + + + Unable to add the permission entry. + + + Unable to remove the permission entry. + Leave my changes on '{0}' diff --git a/src/Files.App/ViewModels/Properties/SecurityAdvancedViewModel.cs b/src/Files.App/ViewModels/Properties/SecurityAdvancedViewModel.cs index 14d12b781970..bfa7583270eb 100644 --- a/src/Files.App/ViewModels/Properties/SecurityAdvancedViewModel.cs +++ b/src/Files.App/ViewModels/Properties/SecurityAdvancedViewModel.cs @@ -29,11 +29,13 @@ public sealed partial class SecurityAdvancedViewModel : ObservableObject public bool IsAddAccessControlEntryButtonEnabled => AccessControlList is not null && - AccessControlList.IsValid; + AccessControlList.IsValid && + CurrentInstanceCanChangePermissions; public bool IsDeleteAccessControlEntryButtonEnabled => AccessControlList is not null && AccessControlList.IsValid && + CurrentInstanceCanChangePermissions && SelectedAccessControlEntry is not null && SelectedAccessControlEntry.IsInherited is false; @@ -100,6 +102,43 @@ public string ErrorMessage set => SetProperty(ref _ErrorMessage, value); } + private bool _IsPermissionChangeInfoBarOpen; + public bool IsPermissionChangeInfoBarOpen + { + get => _IsPermissionChangeInfoBarOpen; + set => SetProperty(ref _IsPermissionChangeInfoBarOpen, value); + } + + private bool _IsUnableToChangePermissionsInfoBarOpen; + public bool IsUnableToChangePermissionsInfoBarOpen + { + get => _IsUnableToChangePermissionsInfoBarOpen; + set => SetProperty(ref _IsUnableToChangePermissionsInfoBarOpen, value); + } + + private bool _IsChangingOwner; + public bool IsChangingOwner + { + get => _IsChangingOwner; + set + { + if (SetProperty(ref _IsChangingOwner, value)) + OnPropertyChanged(nameof(IsOwnerDisplayed)); + } + } + + public bool IsOwnerDisplayed => + !IsChangingOwner && + AccessControlList is not null && + AccessControlList.Owner.IsValid; + + private string _PermissionChangeInfoBarMessage = string.Empty; + public string PermissionChangeInfoBarMessage + { + get => _PermissionChangeInfoBarMessage; + set => SetProperty(ref _PermissionChangeInfoBarMessage, value); + } + private GridLength _ColumnTypeGridLength = new(64d); public GridLength ColumnTypeGridLength { @@ -182,6 +221,7 @@ private void LoadAccessControlEntry() OnPropertyChanged(nameof(AccessControlList)); SelectedAccessControlEntry = AccessControlList.AccessControlEntries.FirstOrDefault(); + OnPropertyChanged(nameof(IsOwnerDisplayed)); if (!AccessControlList.IsValid) { @@ -206,7 +246,13 @@ private void LoadAccessControlEntry() { DisplayElements = true; ErrorMessage = string.Empty; + + CurrentInstanceCanChangePermissions = StorageSecurityService.CanWriteAcl(_path, _isFolder); + IsUnableToChangePermissionsInfoBarOpen = !CurrentInstanceCanChangePermissions; } + + OnPropertyChanged(nameof(IsAddAccessControlEntryButtonEnabled)); + OnPropertyChanged(nameof(IsDeleteAccessControlEntryButtonEnabled)); } private async Task ExecuteChangeOwnerCommandAsync() @@ -215,10 +261,30 @@ private async Task ExecuteChangeOwnerCommandAsync() if (string.IsNullOrEmpty(sid)) return; + IsChangingOwner = true; + + bool setOwnerResult; + + try + { + setOwnerResult = await Task.Run(() => StorageSecurityService.SetOwner(_path, sid)); + } + finally + { + IsChangingOwner = false; + } + await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => { - // Set owner - StorageSecurityService.SetOwner(_path, sid); + if (!setOwnerResult) + { + PermissionChangeInfoBarMessage = Strings.SecurityFailedToChangeOwner.GetLocalizedResource(); + IsPermissionChangeInfoBarOpen = true; + + return; + } + + ClearPermissionChangeError(); // Reload LoadAccessControlEntry(); @@ -237,6 +303,14 @@ await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => // Run Win32API var win32Result = StorageSecurityService.AddAce(_path, _isFolder, sid); + if (win32Result != WIN32_ERROR.ERROR_SUCCESS) + { + SetPermissionChangeError(Strings.SecurityFailedToAddAccessControlEntry.GetLocalizedResource(), win32Result); + return; + } + + ClearPermissionChangeError(); + // Add a new ACE to the ACL var ace = AccessControlEntry.GetDefault(_isFolder, sid); AccessControlList.AccessControlEntries.Insert(0, ace); @@ -256,6 +330,14 @@ await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => // Run Win32API var win32Result = StorageSecurityService.DeleteAce(_path, (uint)index); + if (win32Result != WIN32_ERROR.ERROR_SUCCESS) + { + SetPermissionChangeError(Strings.SecurityFailedToRemoveAccessControlEntry.GetLocalizedResource(), win32Result); + return; + } + + ClearPermissionChangeError(); + // Remove the ACE AccessControlList.AccessControlEntries.Remove(SelectedAccessControlEntry); @@ -266,5 +348,17 @@ await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => SelectedAccessControlEntry = AccessControlList.AccessControlEntries.First(); }); } + + private void SetPermissionChangeError(string message, WIN32_ERROR error) + { + PermissionChangeInfoBarMessage = message + "\r\n\r\n" + error.ToString(); + IsPermissionChangeInfoBarOpen = true; + } + + private void ClearPermissionChangeError() + { + PermissionChangeInfoBarMessage = string.Empty; + IsPermissionChangeInfoBarOpen = false; + } } } diff --git a/src/Files.App/ViewModels/Properties/SecurityViewModel.cs b/src/Files.App/ViewModels/Properties/SecurityViewModel.cs index c96c00aff4dd..3090b7f160b6 100644 --- a/src/Files.App/ViewModels/Properties/SecurityViewModel.cs +++ b/src/Files.App/ViewModels/Properties/SecurityViewModel.cs @@ -24,13 +24,38 @@ public sealed partial class SecurityViewModel : ObservableObject public string ErrorMessage { get; private set; } + private bool _IsPermissionChangeInfoBarOpen; + public bool IsPermissionChangeInfoBarOpen + { + get => _IsPermissionChangeInfoBarOpen; + set => SetProperty(ref _IsPermissionChangeInfoBarOpen, value); + } + + private string _PermissionChangeInfoBarMessage = string.Empty; + public string PermissionChangeInfoBarMessage + { + get => _PermissionChangeInfoBarMessage; + set => SetProperty(ref _PermissionChangeInfoBarMessage, value); + } + + private bool _IsUnableToChangePermissionsInfoBarOpen; + public bool IsUnableToChangePermissionsInfoBarOpen + { + get => _IsUnableToChangePermissionsInfoBarOpen; + set => SetProperty(ref _IsUnableToChangePermissionsInfoBarOpen, value); + } + + public bool CurrentInstanceCanChangePermissions { get; private set; } + public bool IsAddAccessControlEntryButtonEnabled => AccessControlList is not null && - AccessControlList.IsValid; + AccessControlList.IsValid && + CurrentInstanceCanChangePermissions; public bool IsDeleteAccessControlEntryButtonEnabled => AccessControlList is not null && AccessControlList.IsValid && + CurrentInstanceCanChangePermissions && SelectedAccessControlEntry is not null && SelectedAccessControlEntry.IsInherited is false; @@ -107,6 +132,9 @@ public SecurityViewModel(PropertiesPageNavigationParameter parameter) { DisplayElements = true; ErrorMessage = string.Empty; + + CurrentInstanceCanChangePermissions = StorageSecurityService.CanWriteAcl(_path, _isFolder); + IsUnableToChangePermissionsInfoBarOpen = !CurrentInstanceCanChangePermissions; } AddAccessControlEntryCommand = new AsyncRelayCommand(ExecuteAddAccessControlEntryCommandAsync); @@ -125,6 +153,14 @@ await MainWindow.Instance.DispatcherQueue.EnqueueAsync(() => // Run Win32API var win32Result = StorageSecurityService.AddAce(_path, _isFolder, sid); + if (win32Result != WIN32_ERROR.ERROR_SUCCESS) + { + SetPermissionChangeError(Strings.SecurityFailedToAddAccessControlEntry.GetLocalizedResource(), win32Result); + return; + } + + ClearPermissionChangeError(); + // Add a new ACE to the ACL var ace = AccessControlEntry.GetDefault(_isFolder, sid); AccessControlList.AccessControlEntries.Insert(0, ace); @@ -146,6 +182,14 @@ await MainWindow.Instance.DispatcherQueue.EnqueueAsync(() => // Run Win32API var win32Result = StorageSecurityService.DeleteAce(_path, (uint)index); + if (win32Result != WIN32_ERROR.ERROR_SUCCESS) + { + SetPermissionChangeError(Strings.SecurityFailedToRemoveAccessControlEntry.GetLocalizedResource(), win32Result); + return; + } + + ClearPermissionChangeError(); + // Remove the ACE AccessControlList.AccessControlEntries.Remove(selectedEntry); @@ -156,5 +200,17 @@ await MainWindow.Instance.DispatcherQueue.EnqueueAsync(() => SelectedAccessControlEntry = AccessControlList.AccessControlEntries.First(); }); } + + private void SetPermissionChangeError(string message, WIN32_ERROR error) + { + PermissionChangeInfoBarMessage = message + "\r\n\r\n" + error.ToString(); + IsPermissionChangeInfoBarOpen = true; + } + + private void ClearPermissionChangeError() + { + PermissionChangeInfoBarMessage = string.Empty; + IsPermissionChangeInfoBarOpen = false; + } } } diff --git a/src/Files.App/Views/Properties/SecurityAdvancedPage.xaml b/src/Files.App/Views/Properties/SecurityAdvancedPage.xaml index 0f524f2fb1a6..51131c61a68b 100644 --- a/src/Files.App/Views/Properties/SecurityAdvancedPage.xaml +++ b/src/Files.App/Views/Properties/SecurityAdvancedPage.xaml @@ -68,6 +68,7 @@ Padding="12" RowSpacing="8"> + @@ -97,10 +98,27 @@ Orientation="Horizontal" Spacing="8"> + + + + + @@ -108,7 +126,7 @@ @@ -140,10 +158,34 @@ + + + + + + + + + + + + + + + + + + + + +