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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+