From 26b9268122c8277b0db64bee15f60fba75a53b51 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Mon, 5 Oct 2026 02:05:44 +0200 Subject: [PATCH] fix: restore the owner and write no -PassThru after a failed change The cmdlets that take ownership of an item to repeat a denied operation left the account that ran them as the owner when the second attempt failed as well. BaseCmdlet.InvokeAsOwner now restores the previous owner on every exit path and reports a failed restore as RestoreOwnerError. Add-NTFSAccess, Add-NTFSAudit, Remove-NTFSAccess, and Remove-NTFSAudit wrote the unchanged entries of an item with -PassThru after a failed change; they now continue with the next path. The inheritance tests assert the error identity and cover a missing path on every runner. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- .memory-bank/progress.md | 3 + CHANGELOG.md | 6 ++ Docs/Cmdlets/Add-NTFSAccess.md | 2 +- Docs/Cmdlets/Add-NTFSAudit.md | 4 +- Docs/Cmdlets/Clear-NTFSAudit.md | 2 +- Docs/Cmdlets/Remove-NTFSAccess.md | 2 +- Docs/Cmdlets/Remove-NTFSAudit.md | 4 +- Docs/Concepts.md | 3 +- NTFSSecurity/AccessCmdlets/AddAccess.cs | 14 ++-- NTFSSecurity/AccessCmdlets/ClearAccess.cs | 16 ++--- NTFSSecurity/AccessCmdlets/GetAccess.cs | 10 ++- .../AccessCmdlets/GetEffectiveAccess.cs | 12 ++-- .../AccessCmdlets/GetOrphanedAccess.cs | 12 ++-- NTFSSecurity/AccessCmdlets/RemoveAccess.cs | 14 ++-- NTFSSecurity/AuditCmdlets/AddAudit.cs | 14 ++-- NTFSSecurity/AuditCmdlets/ClearAudit.cs | 16 ++--- NTFSSecurity/AuditCmdlets/RemoveAudit.cs | 14 ++-- NTFSSecurity/BaseCmdlets.cs | 32 +++++++++ .../DisableAccessInheritance.cs | 12 ++-- .../DisableAuditInheritance.cs | 12 ++-- .../EnableAccessInheritance.cs | 12 ++-- .../EnableAuditInheritance.cs | 12 ++-- .../InheritanceCmdlets/GetInheritance.cs | 12 ++-- .../InheritanceCmdlets/SetInheritance.cs | 12 ++-- NTFSSecurity/MiscCmdlets/GetFileHash2.cs | 12 ++-- .../GetSecurityDescriptor.cs | 12 ++-- NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml | 14 ++-- Tests/Access.Tests.ps1 | 39 +++++++++++ Tests/FileHash.Tests.ps1 | 29 ++++++++ Tests/Inheritance.Tests.ps1 | 18 ++++- Tests/TestHelpers.psm1 | 70 +++++++++++++++++-- 31 files changed, 284 insertions(+), 162 deletions(-) diff --git a/.memory-bank/progress.md b/.memory-bank/progress.md index 37f5297..bba0bb4 100644 --- a/.memory-bank/progress.md +++ b/.memory-bank/progress.md @@ -150,6 +150,9 @@ Numbered as agreed with the maintainer; each is documented on its page. - (20) The inheritance cmdlets write `-PassThru` output in `finally`, even after a failure. - (21) `New-NTFSHardLink` says the target path exists when it doesn't. +- Review of group C: the add and remove cmdlets also wrote `-PassThru` + after a failed change, and a failed retry after taking ownership left the + owner changed; `BaseCmdlet.InvokeAsOwner` now restores it on every path. #### D: Metadata and cosmetics diff --git a/CHANGELOG.md b/CHANGELOG.md index 1023796..2c73265 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -128,5 +128,11 @@ The format is based on that the target path existed - Fix `Get-FileHash2`, which wrote a result for a file that it couldn't read, with the hash of the previous file +- Fix `-PassThru` of `Add-NTFSAccess`, `Add-NTFSAudit`, `Remove-NTFSAccess`, + and `Remove-NTFSAudit`, which returned the unchanged entries of an item + also when the change failed +- Fix the cmdlets that take ownership of an item to repeat an operation that + was denied: when the second attempt failed as well, the account that ran + the cmdlet stayed the owner of the item; now the previous owner is restored [Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD diff --git a/Docs/Cmdlets/Add-NTFSAccess.md b/Docs/Cmdlets/Add-NTFSAccess.md index 078f24e..b39a443 100644 --- a/Docs/Cmdlets/Add-NTFSAccess.md +++ b/Docs/Cmdlets/Add-NTFSAccess.md @@ -282,7 +282,7 @@ The scope of the ACE, piped by the property `AppliesTo` in the `Simple` paramete ### Security2.FileSystemAccessRule2 -With `-PassThru`, the cmdlet writes all access control entries of every processed item, explicit and inherited. Without `-PassThru` it writes nothing. +With `-PassThru`, the cmdlet writes all access control entries, explicit and inherited, of every item it changed; an item it could not change produces only an error. Before 5.0.0, `-PassThru` also wrote the unchanged entries of such an item. Without `-PassThru` it writes nothing. ## NOTES diff --git a/Docs/Cmdlets/Add-NTFSAudit.md b/Docs/Cmdlets/Add-NTFSAudit.md index 838800e..ad280e6 100644 --- a/Docs/Cmdlets/Add-NTFSAudit.md +++ b/Docs/Cmdlets/Add-NTFSAudit.md @@ -280,7 +280,7 @@ The value passed to `-AppliesTo` is converted to this type and binds by property ### Security2.FileSystemAuditRule2 -Without `-PassThru` the cmdlet writes nothing. With `-PassThru` the cmdlet writes all audit entries of the item or the security descriptor, explicit and inherited ones, as `Security2.FileSystemAuditRule2` objects. Before 5.0.0, the `SecurityDescriptor` sets wrote the access entries of the descriptor instead. +Without `-PassThru` the cmdlet writes nothing. With `-PassThru` the cmdlet writes all audit entries of the item or the security descriptor, explicit and inherited ones, as `Security2.FileSystemAuditRule2` objects. Before 5.0.0, the `SecurityDescriptor` sets wrote the access entries of the descriptor instead. An item whose audit entries could not be changed produces only an error; before 5.0.0, `-PassThru` also wrote its unchanged entries. ## NOTES @@ -288,7 +288,7 @@ When the module setting `EnablePrivileges` is `$true` (the default in the `Priva Writing the SACL requires the Security privilege (`SeSecurityPrivilege`, "Manage auditing and security log"), so run this cmdlet in an elevated session of an account that holds that privilege. Without it, the cmdlet writes a non-terminating `AddAceError` whose message states that a required privilege is not held by the client, and the item is left unchanged. -If the security descriptor cannot be read or written because access is denied, the cmdlet takes ownership of the item, repeats the operation, and restores the previous owner. If the second attempt fails as well, the cmdlet writes an error, and the ownership change is not rolled back. +If the security descriptor cannot be read or written because access is denied, the cmdlet takes ownership of the item, repeats the operation, and restores the previous owner. If the second attempt fails as well, the cmdlet restores the previous owner and writes an error. Before 5.0.0, the account that ran the cmdlet stayed the owner of the item in that case. `-Path` or `-SecurityDescriptor`, `-Account`, and `-AccessRights` are positional parameters at positions 1, 2, and 3, like in `Remove-NTFSAudit`. Before 5.0.0, `-Account` and `-AccessRights` were both declared at position 2, so a command that passed them by position failed. diff --git a/Docs/Cmdlets/Clear-NTFSAudit.md b/Docs/Cmdlets/Clear-NTFSAudit.md index 7acbcf7..7ec4adf 100644 --- a/Docs/Cmdlets/Clear-NTFSAudit.md +++ b/Docs/Cmdlets/Clear-NTFSAudit.md @@ -142,7 +142,7 @@ When the module setting `EnablePrivileges` is `$true` (the default in the `Priva Reading and writing the SACL requires the Security privilege (`SeSecurityPrivilege`, "Manage auditing and security log"), so run this cmdlet in an elevated session of an account that holds that privilege. Without that privilege the cmdlet reads the security descriptor without its SACL, finds no audit entries to remove, and finishes without an error although nothing was changed. `-DisableInheritance` fails in that situation with a `ClearAclError` whose message states that a required privilege is not held by the client. -If the security descriptor cannot be read or written because access is denied, the cmdlet takes ownership of the item, repeats the operation, and restores the previous owner. If the second attempt fails as well, the cmdlet writes an error, and the ownership change is not rolled back. +If the security descriptor cannot be read or written because access is denied, the cmdlet takes ownership of the item, repeats the operation, and restores the previous owner. If the second attempt fails as well, the cmdlet restores the previous owner and writes an error. Before 5.0.0, the account that ran the cmdlet stayed the owner of the item in that case. ## RELATED LINKS diff --git a/Docs/Cmdlets/Remove-NTFSAccess.md b/Docs/Cmdlets/Remove-NTFSAccess.md index 6aa0d3d..e2b4c18 100644 --- a/Docs/Cmdlets/Remove-NTFSAccess.md +++ b/Docs/Cmdlets/Remove-NTFSAccess.md @@ -294,7 +294,7 @@ The scope of the entry, piped by the property `AppliesTo` in the `Simple` parame ### Security2.FileSystemAccessRule2 -With `-PassThru`, the cmdlet writes all access control entries of every processed item, explicit and inherited. Without `-PassThru` it writes nothing. +With `-PassThru`, the cmdlet writes all access control entries, explicit and inherited, of every item it changed; an item it could not change produces only an error. Before 5.0.0, `-PassThru` also wrote the unchanged entries of such an item. Without `-PassThru` it writes nothing. ## NOTES diff --git a/Docs/Cmdlets/Remove-NTFSAudit.md b/Docs/Cmdlets/Remove-NTFSAudit.md index c116fa8..c8c7835 100644 --- a/Docs/Cmdlets/Remove-NTFSAudit.md +++ b/Docs/Cmdlets/Remove-NTFSAudit.md @@ -294,7 +294,7 @@ The value passed to `-AppliesTo` is converted to this type and binds by property ### Security2.FileSystemAuditRule2 -Without `-PassThru` the cmdlet writes nothing. With `-PassThru` the cmdlet writes all audit entries of the item or the security descriptor, explicit and inherited ones, as `Security2.FileSystemAuditRule2` objects. Before 5.0.0, the `Path` sets wrote the access entries of the item instead. +Without `-PassThru` the cmdlet writes nothing. With `-PassThru` the cmdlet writes all audit entries of the item or the security descriptor, explicit and inherited ones, as `Security2.FileSystemAuditRule2` objects. Before 5.0.0, the `Path` sets wrote the access entries of the item instead. An item whose audit entries could not be changed produces only an error; before 5.0.0, `-PassThru` also wrote its unchanged entries. ## NOTES @@ -302,7 +302,7 @@ When the module setting `EnablePrivileges` is `$true` (the default in the `Priva Reading and writing the SACL requires the Security privilege (`SeSecurityPrivilege`, "Manage auditing and security log"), so run this cmdlet in an elevated session of an account that holds that privilege. Without it, the cmdlet writes a non-terminating `RemoveAceError` whose message states that a required privilege is not held by the client, and the item is left unchanged. -If the security descriptor cannot be read or written because access is denied, the cmdlet takes ownership of the item, repeats the operation, and restores the previous owner. If the second attempt fails as well, the cmdlet writes an error, and the ownership change is not rolled back. +If the security descriptor cannot be read or written because access is denied, the cmdlet takes ownership of the item, repeats the operation, and restores the previous owner. If the second attempt fails as well, the cmdlet restores the previous owner and writes an error. Before 5.0.0, the account that ran the cmdlet stayed the owner of the item in that case. The cmdlet reports no error when no entry matches the supplied values. Compare the result with `Get-NTFSAudit` to confirm that the entry is gone. diff --git a/Docs/Concepts.md b/Docs/Concepts.md index aaa2a15..18f347c 100644 --- a/Docs/Concepts.md +++ b/Docs/Concepts.md @@ -199,7 +199,8 @@ state. When reading or changing an item fails with an access-denied error, most of these cmdlets make the current user the owner of the item, retry, and then -restore the previous owner. This requires the privileges above. +restore the previous owner, also when the retry fails. Before 5.0.0, a failed +retry left the current user as the owner. This requires the privileges above. Reading or changing audit entries always requires the Security privilege. Without it, the audit cmdlets fail, and `Get-NTFSEffectiveAccess` warns that diff --git a/NTFSSecurity/AccessCmdlets/AddAccess.cs b/NTFSSecurity/AccessCmdlets/AddAccess.cs index c22e0f5..4770aa9 100644 --- a/NTFSSecurity/AccessCmdlets/AddAccess.cs +++ b/NTFSSecurity/AccessCmdlets/AddAccess.cs @@ -138,23 +138,21 @@ namespace NTFSSecurity { try { - var ownerInfo = FileSystemOwner.GetOwner(item); - var previousOwner = ownerInfo.Owner; - - FileSystemOwner.SetOwner(item, System.Security.Principal.WindowsIdentity.GetCurrent().User); - - FileSystemAccessRule2.AddFileSystemAccessRule(item, account.ToList(), accessRights, accessType, inheritanceFlags, propagationFlags); - - FileSystemOwner.SetOwner(item, previousOwner); + InvokeAsOwner(item, path, () => + { + FileSystemAccessRule2.AddFileSystemAccessRule(item, account.ToList(), accessRights, accessType, inheritanceFlags, propagationFlags); + }); } catch (Exception ex2) { WriteError(new ErrorRecord(ex2, "AddAceError", ErrorCategory.WriteError, path)); + continue; } } catch (Exception ex) { WriteError(new ErrorRecord(ex, "AddAceError", ErrorCategory.WriteError, path)); + continue; } if (passThru == true) diff --git a/NTFSSecurity/AccessCmdlets/ClearAccess.cs b/NTFSSecurity/AccessCmdlets/ClearAccess.cs index 20b1943..9e96a39 100644 --- a/NTFSSecurity/AccessCmdlets/ClearAccess.cs +++ b/NTFSSecurity/AccessCmdlets/ClearAccess.cs @@ -76,16 +76,12 @@ namespace NTFSSecurity { try { - var ownerInfo = FileSystemOwner.GetOwner(item); - var previousOwner = ownerInfo.Owner; - - FileSystemOwner.SetOwner(item, System.Security.Principal.WindowsIdentity.GetCurrent().User); - - FileSystemAccessRule2.RemoveFileSystemAccessRuleAll(item); - if (disableInheritance) - FileSystemInheritanceInfo.DisableAccessInheritance(item, true); - - FileSystemOwner.SetOwner(item, previousOwner); + InvokeAsOwner(item, path, () => + { + FileSystemAccessRule2.RemoveFileSystemAccessRuleAll(item); + if (disableInheritance) + FileSystemInheritanceInfo.DisableAccessInheritance(item, true); + }); } catch (Exception ex2) { diff --git a/NTFSSecurity/AccessCmdlets/GetAccess.cs b/NTFSSecurity/AccessCmdlets/GetAccess.cs index 6b9339a..f010e18 100644 --- a/NTFSSecurity/AccessCmdlets/GetAccess.cs +++ b/NTFSSecurity/AccessCmdlets/GetAccess.cs @@ -104,12 +104,10 @@ namespace NTFSSecurity { try { - var ownerInfo = FileSystemOwner.GetOwner(item); - var previousOwner = ownerInfo.Owner; - - FileSystemOwner.SetOwner(item, System.Security.Principal.WindowsIdentity.GetCurrent().User); - acl = FileSystemAccessRule2.GetFileSystemAccessRules(item, !excludeExplicit, !excludeInherited, getInheritedFrom); - FileSystemOwner.SetOwner(item, previousOwner); + InvokeAsOwner(item, path, () => + { + acl = FileSystemAccessRule2.GetFileSystemAccessRules(item, !excludeExplicit, !excludeInherited, getInheritedFrom); + }); } catch (Exception ex2) { diff --git a/NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs b/NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs index 4fb3c23..d929bbf 100644 --- a/NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs +++ b/NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs @@ -133,14 +133,10 @@ namespace NTFSSecurity { try { - var ownerInfo = FileSystemOwner.GetOwner(item); - var previousOwner = ownerInfo.Owner; - - FileSystemOwner.SetOwner(item, System.Security.Principal.WindowsIdentity.GetCurrent().User); - - result = EffectiveAccess.GetEffectiveAccess(item, account, serverName); - - FileSystemOwner.SetOwner(item, previousOwner); + InvokeAsOwner(item, path, () => + { + result = EffectiveAccess.GetEffectiveAccess(item, account, serverName); + }); } catch (Exception ex2) { diff --git a/NTFSSecurity/AccessCmdlets/GetOrphanedAccess.cs b/NTFSSecurity/AccessCmdlets/GetOrphanedAccess.cs index fbe88ce..94ad30c 100644 --- a/NTFSSecurity/AccessCmdlets/GetOrphanedAccess.cs +++ b/NTFSSecurity/AccessCmdlets/GetOrphanedAccess.cs @@ -48,14 +48,10 @@ namespace NTFSSecurity { try { - var ownerInfo = FileSystemOwner.GetOwner(item); - var previousOwner = ownerInfo.Owner; - - FileSystemOwner.SetOwner(item, System.Security.Principal.WindowsIdentity.GetCurrent().User); - - acl = FileSystemAccessRule2.GetFileSystemAccessRules(item, !ExcludeExplicit, !ExcludeInherited, getInheritedFrom); - - FileSystemOwner.SetOwner(item, previousOwner); + InvokeAsOwner(item, path, () => + { + acl = FileSystemAccessRule2.GetFileSystemAccessRules(item, !ExcludeExplicit, !ExcludeInherited, getInheritedFrom); + }); } catch (Exception ex2) { diff --git a/NTFSSecurity/AccessCmdlets/RemoveAccess.cs b/NTFSSecurity/AccessCmdlets/RemoveAccess.cs index 5115150..bd4c04d 100644 --- a/NTFSSecurity/AccessCmdlets/RemoveAccess.cs +++ b/NTFSSecurity/AccessCmdlets/RemoveAccess.cs @@ -154,23 +154,21 @@ namespace NTFSSecurity { try { - var ownerInfo = FileSystemOwner.GetOwner(item); - var previousOwner = ownerInfo.Owner; - - FileSystemOwner.SetOwner(item, System.Security.Principal.WindowsIdentity.GetCurrent().User); - - FileSystemAccessRule2.RemoveFileSystemAccessRule(item, account.ToList(), accessRights, accessType, inheritanceFlags, propagationFlags, removeSpecific); - - FileSystemOwner.SetOwner(item, previousOwner); + InvokeAsOwner(item, path, () => + { + FileSystemAccessRule2.RemoveFileSystemAccessRule(item, account.ToList(), accessRights, accessType, inheritanceFlags, propagationFlags, removeSpecific); + }); } catch (Exception ex2) { WriteError(new ErrorRecord(ex2, "RemoveAceError", ErrorCategory.WriteError, path)); + continue; } } catch (Exception ex) { WriteError(new ErrorRecord(ex, "RemoveAceError", ErrorCategory.WriteError, path)); + continue; } if (passThru == true) diff --git a/NTFSSecurity/AuditCmdlets/AddAudit.cs b/NTFSSecurity/AuditCmdlets/AddAudit.cs index 7470de6..4ce3edb 100644 --- a/NTFSSecurity/AuditCmdlets/AddAudit.cs +++ b/NTFSSecurity/AuditCmdlets/AddAudit.cs @@ -137,23 +137,21 @@ namespace NTFSSecurity { try { - var ownerInfo = FileSystemOwner.GetOwner(item); - var previousOwner = ownerInfo.Owner; - - FileSystemOwner.SetOwner(item, System.Security.Principal.WindowsIdentity.GetCurrent().User); - - FileSystemAuditRule2.AddFileSystemAuditRule(item, account.ToList(), accessRights, auditFlags, inheritanceFlags, propagationFlags); - - FileSystemOwner.SetOwner(item, previousOwner); + InvokeAsOwner(item, path, () => + { + FileSystemAuditRule2.AddFileSystemAuditRule(item, account.ToList(), accessRights, auditFlags, inheritanceFlags, propagationFlags); + }); } catch (Exception ex2) { WriteError(new ErrorRecord(ex2, "AddAceError", ErrorCategory.WriteError, path)); + continue; } } catch (Exception ex) { WriteError(new ErrorRecord(ex, "AddAceError", ErrorCategory.WriteError, path)); + continue; } if (passThru == true) diff --git a/NTFSSecurity/AuditCmdlets/ClearAudit.cs b/NTFSSecurity/AuditCmdlets/ClearAudit.cs index c34bb29..622e5d4 100644 --- a/NTFSSecurity/AuditCmdlets/ClearAudit.cs +++ b/NTFSSecurity/AuditCmdlets/ClearAudit.cs @@ -76,16 +76,12 @@ namespace NTFSSecurity { try { - var ownerInfo = FileSystemOwner.GetOwner(item); - var previousOwner = ownerInfo.Owner; - - FileSystemOwner.SetOwner(item, System.Security.Principal.WindowsIdentity.GetCurrent().User); - - FileSystemAuditRule2.RemoveFileSystemAuditRuleAll(item); - if (disableInheritance) - FileSystemInheritanceInfo.DisableAuditInheritance(item, true); - - FileSystemOwner.SetOwner(item, previousOwner); + InvokeAsOwner(item, path, () => + { + FileSystemAuditRule2.RemoveFileSystemAuditRuleAll(item); + if (disableInheritance) + FileSystemInheritanceInfo.DisableAuditInheritance(item, true); + }); } catch (Exception ex2) { diff --git a/NTFSSecurity/AuditCmdlets/RemoveAudit.cs b/NTFSSecurity/AuditCmdlets/RemoveAudit.cs index 9a4e3dc..f075b30 100644 --- a/NTFSSecurity/AuditCmdlets/RemoveAudit.cs +++ b/NTFSSecurity/AuditCmdlets/RemoveAudit.cs @@ -153,23 +153,21 @@ namespace NTFSSecurity { try { - var ownerInfo = FileSystemOwner.GetOwner(item); - var previousOwner = ownerInfo.Owner; - - FileSystemOwner.SetOwner(item, System.Security.Principal.WindowsIdentity.GetCurrent().User); - - FileSystemAuditRule2.RemoveFileSystemAuditRule(item, account.ToList(), accessRights, auditFlags, inheritanceFlags, propagationFlags, removeSpecific); - - FileSystemOwner.SetOwner(item, previousOwner); + InvokeAsOwner(item, path, () => + { + FileSystemAuditRule2.RemoveFileSystemAuditRule(item, account.ToList(), accessRights, auditFlags, inheritanceFlags, propagationFlags, removeSpecific); + }); } catch (Exception ex2) { WriteError(new ErrorRecord(ex2, "RemoveAceError", ErrorCategory.WriteError, path)); + continue; } } catch (Exception ex) { WriteError(new ErrorRecord(ex, "RemoveAceError", ErrorCategory.WriteError, path)); + continue; } if (passThru == true) diff --git a/NTFSSecurity/BaseCmdlets.cs b/NTFSSecurity/BaseCmdlets.cs index 0a5eb49..8cdb84d 100644 --- a/NTFSSecurity/BaseCmdlets.cs +++ b/NTFSSecurity/BaseCmdlets.cs @@ -148,6 +148,38 @@ namespace NTFSSecurity return path; } #endregion + + #region InvokeAsOwner + /// + /// Takes ownership of the item, runs the action, and restores the previous owner on every exit path. + /// A failure to restore the owner is written as a RestoreOwnerError and doesn't hide an error of the action. + /// + /// The file or folder to take ownership of. + /// The path the user specified, used as the error target. + /// The operation to run while the current user owns the item. + protected void InvokeAsOwner(Alphaleonis.Win32.Filesystem.FileSystemInfo item, string path, Action action) + { + var previousOwner = FileSystemOwner.GetOwner(item).Owner; + + FileSystemOwner.SetOwner(item, System.Security.Principal.WindowsIdentity.GetCurrent().User); + + try + { + action(); + } + finally + { + try + { + FileSystemOwner.SetOwner(item, previousOwner); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "RestoreOwnerError", ErrorCategory.WriteError, path)); + } + } + } + #endregion } public class BaseCmdletWithPrivControl : BaseCmdlet diff --git a/NTFSSecurity/InheritanceCmdlets/DisableAccessInheritance.cs b/NTFSSecurity/InheritanceCmdlets/DisableAccessInheritance.cs index c5f301c..f459e61 100644 --- a/NTFSSecurity/InheritanceCmdlets/DisableAccessInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/DisableAccessInheritance.cs @@ -81,14 +81,10 @@ namespace NTFSSecurity { try { - var ownerInfo = FileSystemOwner.GetOwner(item); - var previousOwner = ownerInfo.Owner; - - FileSystemOwner.SetOwner(item, System.Security.Principal.WindowsIdentity.GetCurrent().User); - - FileSystemInheritanceInfo.DisableAccessInheritance(item, removeInheritedAccessRules); - - FileSystemOwner.SetOwner(item, previousOwner); + InvokeAsOwner(item, path, () => + { + FileSystemInheritanceInfo.DisableAccessInheritance(item, removeInheritedAccessRules); + }); } catch (Exception ex2) { diff --git a/NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs b/NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs index f8dbcef..bf51018 100644 --- a/NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs @@ -82,14 +82,10 @@ namespace NTFSSecurity { try { - var ownerInfo = FileSystemOwner.GetOwner(item); - var previousOwner = ownerInfo.Owner; - - FileSystemOwner.SetOwner(item, System.Security.Principal.WindowsIdentity.GetCurrent().User); - - FileSystemInheritanceInfo.DisableAuditInheritance(item, removeInheritedAccessRules); - - FileSystemOwner.SetOwner(item, previousOwner); + InvokeAsOwner(item, path, () => + { + FileSystemInheritanceInfo.DisableAuditInheritance(item, removeInheritedAccessRules); + }); } catch (Exception ex2) { diff --git a/NTFSSecurity/InheritanceCmdlets/EnableAccessInheritance.cs b/NTFSSecurity/InheritanceCmdlets/EnableAccessInheritance.cs index de3b55a..1821e49 100644 --- a/NTFSSecurity/InheritanceCmdlets/EnableAccessInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/EnableAccessInheritance.cs @@ -81,14 +81,10 @@ namespace NTFSSecurity { try { - var ownerInfo = FileSystemOwner.GetOwner(item); - var previousOwner = ownerInfo.Owner; - - FileSystemOwner.SetOwner(item, System.Security.Principal.WindowsIdentity.GetCurrent().User); - - FileSystemInheritanceInfo.EnableAccessInheritance(item, removeExplicitAccessRules); - - FileSystemOwner.SetOwner(item, previousOwner); + InvokeAsOwner(item, path, () => + { + FileSystemInheritanceInfo.EnableAccessInheritance(item, removeExplicitAccessRules); + }); } catch (Exception ex2) { diff --git a/NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs b/NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs index 943234b..d3c753c 100644 --- a/NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs @@ -81,14 +81,10 @@ namespace NTFSSecurity { try { - var ownerInfo = FileSystemOwner.GetOwner(item); - var previousOwner = ownerInfo.Owner; - - FileSystemOwner.SetOwner(item, System.Security.Principal.WindowsIdentity.GetCurrent().User); - - FileSystemInheritanceInfo.EnableAuditInheritance(item, removeExplicitAccessRules); - - FileSystemOwner.SetOwner(item, previousOwner); + InvokeAsOwner(item, path, () => + { + FileSystemInheritanceInfo.EnableAuditInheritance(item, removeExplicitAccessRules); + }); } catch (Exception ex2) { diff --git a/NTFSSecurity/InheritanceCmdlets/GetInheritance.cs b/NTFSSecurity/InheritanceCmdlets/GetInheritance.cs index df19f8d..07d510c 100644 --- a/NTFSSecurity/InheritanceCmdlets/GetInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/GetInheritance.cs @@ -72,14 +72,10 @@ namespace NTFSSecurity { try { - var ownerInfo = FileSystemOwner.GetOwner(item); - var previousOwner = ownerInfo.Owner; - - FileSystemOwner.SetOwner(item, System.Security.Principal.WindowsIdentity.GetCurrent().User); - - inheritanceInfo = FileSystemInheritanceInfo.GetFileSystemInheritanceInfo(item); - - FileSystemOwner.SetOwner(item, previousOwner); + InvokeAsOwner(item, path, () => + { + inheritanceInfo = FileSystemInheritanceInfo.GetFileSystemInheritanceInfo(item); + }); } catch (Exception ex2) { diff --git a/NTFSSecurity/InheritanceCmdlets/SetInheritance.cs b/NTFSSecurity/InheritanceCmdlets/SetInheritance.cs index 1a07fa7..3dcc8b3 100644 --- a/NTFSSecurity/InheritanceCmdlets/SetInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/SetInheritance.cs @@ -89,14 +89,10 @@ namespace NTFSSecurity { try { - var ownerInfo = FileSystemOwner.GetOwner(item); - var previousOwner = ownerInfo.Owner; - - FileSystemOwner.SetOwner(item, System.Security.Principal.WindowsIdentity.GetCurrent().User); - - SetInheritanceState(item); - - FileSystemOwner.SetOwner(item, previousOwner); + InvokeAsOwner(item, path, () => + { + SetInheritanceState(item); + }); } catch (Exception ex2) { diff --git a/NTFSSecurity/MiscCmdlets/GetFileHash2.cs b/NTFSSecurity/MiscCmdlets/GetFileHash2.cs index 52a01b8..2e46f78 100644 --- a/NTFSSecurity/MiscCmdlets/GetFileHash2.cs +++ b/NTFSSecurity/MiscCmdlets/GetFileHash2.cs @@ -68,14 +68,10 @@ namespace NTFSSecurity { try { - var ownerInfo = FileSystemOwner.GetOwner(item); - var previousOwner = ownerInfo.Owner; - - FileSystemOwner.SetOwner(item, System.Security.Principal.WindowsIdentity.GetCurrent().User); - - hash = ((FileInfo)item).GetHash(algorithm); - - FileSystemOwner.SetOwner(item, previousOwner); + InvokeAsOwner(item, path, () => + { + hash = ((FileInfo)item).GetHash(algorithm); + }); } catch (Exception ex2) { diff --git a/NTFSSecurity/SecurityDescriptorCmdlets/GetSecurityDescriptor.cs b/NTFSSecurity/SecurityDescriptorCmdlets/GetSecurityDescriptor.cs index 0b9dc93..4d5e3f4 100644 --- a/NTFSSecurity/SecurityDescriptorCmdlets/GetSecurityDescriptor.cs +++ b/NTFSSecurity/SecurityDescriptorCmdlets/GetSecurityDescriptor.cs @@ -56,14 +56,10 @@ namespace NTFSSecurity { try { - var ownerInfo = FileSystemOwner.GetOwner(item); - var previousOwner = ownerInfo.Owner; - - FileSystemOwner.SetOwner(item, System.Security.Principal.WindowsIdentity.GetCurrent().User); - - WriteObject(new FileSystemSecurity2(item)); - - FileSystemOwner.SetOwner(item, previousOwner); + InvokeAsOwner(item, path, () => + { + WriteObject(new FileSystemSecurity2(item)); + }); } catch (Exception ex2) { diff --git a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml index 23a434b..9425ccf 100644 --- a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml +++ b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml @@ -712,7 +712,7 @@ Security2.FileSystemAccessRule2 - With `-PassThru`, the cmdlet writes all access control entries of every processed item, explicit and inherited. Without `-PassThru` it writes nothing. + With `-PassThru`, the cmdlet writes all access control entries, explicit and inherited, of every item it changed; an item it could not change produces only an error. Before 5.0.0, `-PassThru` also wrote the unchanged entries of such an item. Without `-PassThru` it writes nothing. @@ -1493,7 +1493,7 @@ Security2.FileSystemAuditRule2 - Without `-PassThru` the cmdlet writes nothing. With `-PassThru` the cmdlet writes all audit entries of the item or the security descriptor, explicit and inherited ones, as `Security2.FileSystemAuditRule2` objects. Before 5.0.0, the `SecurityDescriptor` sets wrote the access entries of the descriptor instead. + Without `-PassThru` the cmdlet writes nothing. With `-PassThru` the cmdlet writes all audit entries of the item or the security descriptor, explicit and inherited ones, as `Security2.FileSystemAuditRule2` objects. Before 5.0.0, the `SecurityDescriptor` sets wrote the access entries of the descriptor instead. An item whose audit entries could not be changed produces only an error; before 5.0.0, `-PassThru` also wrote its unchanged entries. @@ -1501,7 +1501,7 @@ When the module setting `EnablePrivileges` is `$true` (the default in the `PrivateData` section of NTFSSecurity.psd1), this cmdlet tries to enable the Backup, Restore, Take Ownership, and Security privileges while it runs and disables the privileges it enabled when it finishes. These privileges are only available in an elevated session of an account that holds them, such as a member of the local Administrators group. If a privilege cannot be enabled, the cmdlet continues without it and writes a debug message. Writing the SACL requires the Security privilege (`SeSecurityPrivilege`, "Manage auditing and security log"), so run this cmdlet in an elevated session of an account that holds that privilege. Without it, the cmdlet writes a non-terminating `AddAceError` whose message states that a required privilege is not held by the client, and the item is left unchanged. - If the security descriptor cannot be read or written because access is denied, the cmdlet takes ownership of the item, repeats the operation, and restores the previous owner. If the second attempt fails as well, the cmdlet writes an error, and the ownership change is not rolled back. + If the security descriptor cannot be read or written because access is denied, the cmdlet takes ownership of the item, repeats the operation, and restores the previous owner. If the second attempt fails as well, the cmdlet restores the previous owner and writes an error. Before 5.0.0, the account that ran the cmdlet stayed the owner of the item in that case. `-Path` or `-SecurityDescriptor`, `-Account`, and `-AccessRights` are positional parameters at positions 1, 2, and 3, like in `Remove-NTFSAudit`. Before 5.0.0, `-Account` and `-AccessRights` were both declared at position 2, so a command that passed them by position failed. An audit entry alone does not create events. Windows writes the events to the security log only while the "Audit object access" policy, or the corresponding "Audit File System" advanced audit policy, is enabled for success, failure, or both. That policy is a Windows setting and is not managed by this module. @@ -1913,7 +1913,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd When the module setting `EnablePrivileges` is `$true` (the default in the `PrivateData` section of NTFSSecurity.psd1), this cmdlet tries to enable the Backup, Restore, Take Ownership, and Security privileges while it runs and disables the privileges it enabled when it finishes. These privileges are only available in an elevated session of an account that holds them, such as a member of the local Administrators group. If a privilege cannot be enabled, the cmdlet continues without it and writes a debug message. Reading and writing the SACL requires the Security privilege (`SeSecurityPrivilege`, "Manage auditing and security log"), so run this cmdlet in an elevated session of an account that holds that privilege. Without that privilege the cmdlet reads the security descriptor without its SACL, finds no audit entries to remove, and finishes without an error although nothing was changed. `-DisableInheritance` fails in that situation with a `ClearAclError` whose message states that a required privilege is not held by the client. - If the security descriptor cannot be read or written because access is denied, the cmdlet takes ownership of the item, repeats the operation, and restores the previous owner. If the second attempt fails as well, the cmdlet writes an error, and the ownership change is not rolled back. + If the security descriptor cannot be read or written because access is denied, the cmdlet takes ownership of the item, repeats the operation, and restores the previous owner. If the second attempt fails as well, the cmdlet restores the previous owner and writes an error. Before 5.0.0, the account that ran the cmdlet stayed the owner of the item in that case. @@ -8380,7 +8380,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor Security2.FileSystemAccessRule2 - With `-PassThru`, the cmdlet writes all access control entries of every processed item, explicit and inherited. Without `-PassThru` it writes nothing. + With `-PassThru`, the cmdlet writes all access control entries, explicit and inherited, of every item it changed; an item it could not change produces only an error. Before 5.0.0, `-PassThru` also wrote the unchanged entries of such an item. Without `-PassThru` it writes nothing. @@ -9219,7 +9219,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor Security2.FileSystemAuditRule2 - Without `-PassThru` the cmdlet writes nothing. With `-PassThru` the cmdlet writes all audit entries of the item or the security descriptor, explicit and inherited ones, as `Security2.FileSystemAuditRule2` objects. Before 5.0.0, the `Path` sets wrote the access entries of the item instead. + Without `-PassThru` the cmdlet writes nothing. With `-PassThru` the cmdlet writes all audit entries of the item or the security descriptor, explicit and inherited ones, as `Security2.FileSystemAuditRule2` objects. Before 5.0.0, the `Path` sets wrote the access entries of the item instead. An item whose audit entries could not be changed produces only an error; before 5.0.0, `-PassThru` also wrote its unchanged entries. @@ -9227,7 +9227,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor When the module setting `EnablePrivileges` is `$true` (the default in the `PrivateData` section of NTFSSecurity.psd1), this cmdlet tries to enable the Backup, Restore, Take Ownership, and Security privileges while it runs and disables the privileges it enabled when it finishes. These privileges are only available in an elevated session of an account that holds them, such as a member of the local Administrators group. If a privilege cannot be enabled, the cmdlet continues without it and writes a debug message. Reading and writing the SACL requires the Security privilege (`SeSecurityPrivilege`, "Manage auditing and security log"), so run this cmdlet in an elevated session of an account that holds that privilege. Without it, the cmdlet writes a non-terminating `RemoveAceError` whose message states that a required privilege is not held by the client, and the item is left unchanged. - If the security descriptor cannot be read or written because access is denied, the cmdlet takes ownership of the item, repeats the operation, and restores the previous owner. If the second attempt fails as well, the cmdlet writes an error, and the ownership change is not rolled back. + If the security descriptor cannot be read or written because access is denied, the cmdlet takes ownership of the item, repeats the operation, and restores the previous owner. If the second attempt fails as well, the cmdlet restores the previous owner and writes an error. Before 5.0.0, the account that ran the cmdlet stayed the owner of the item in that case. The cmdlet reports no error when no entry matches the supplied values. Compare the result with `Get-NTFSAudit` to confirm that the entry is gone. Before 5.0.0, the cmdlet had no `-RemoveSpecific` switch. A path that does not exist produces the non-terminating error `ReadFileError`, and the cmdlet continues with the next path. Before 5.0.0, the cmdlet also wrote a misleading `RemoveAceError` for that path, and with `-PassThru` it stopped with a `NullReferenceException`. diff --git a/Tests/Access.Tests.ps1 b/Tests/Access.Tests.ps1 index 11b11c8..bede5be 100644 --- a/Tests/Access.Tests.ps1 +++ b/Tests/Access.Tests.ps1 @@ -6,6 +6,12 @@ )] param () +BeforeDiscovery { + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force + # With the Restore privilege, Windows may grant writing the DACL despite a deny entry. + $canBypassWriteDeny = Test-PrivilegeHeld -Name 'SeRestorePrivilege' +} + BeforeAll { Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force $modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\NTFSSecurity\bin\Release\NTFSSecurity.psd1' @@ -168,6 +174,22 @@ Describe 'Remove-NTFSAccess' { } } + Context 'When the entries cannot be changed' { + # Before 5.0.0, -PassThru returned the unchanged entries of the item after the error. + It 'Should write an error and return nothing with -PassThru' -Skip:$canBypassWriteDeny { + $protected = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveProtected' + Assert-TestSandboxPath -Sandbox $sandbox -Path $protected + Add-NTFSAccess -Path $protected -Account 'Everyone' -AccessRights ReadData + Block-TestWritePermission -Sandbox $sandbox -Path $protected + + $result = @(Remove-NTFSAccess -Path $protected -Account 'Everyone' -AccessRights ReadData -PassThru -ErrorVariable removeErrors -ErrorAction SilentlyContinue) + + $removeErrors | Should -HaveCount 1 + $removeErrors[0].FullyQualifiedErrorId | Should -BeLike 'RemoveAceError,*' + $result | Should -BeNullOrEmpty + } + } + Context 'With -RemoveSpecific' { BeforeEach { $removeFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveSpecific' -Directory @@ -202,6 +224,23 @@ Describe 'Remove-NTFSAccess' { } } +Describe 'Add-NTFSAccess' { + Context 'When the entries cannot be changed' { + # Before 5.0.0, -PassThru returned the unchanged entries of the item after the error. + It 'Should write an error and return nothing with -PassThru' -Skip:$canBypassWriteDeny { + $protected = New-TestSandboxItem -Sandbox $sandbox -Name 'AddProtected' + Assert-TestSandboxPath -Sandbox $sandbox -Path $protected + Block-TestWritePermission -Sandbox $sandbox -Path $protected + + $result = @(Add-NTFSAccess -Path $protected -Account 'Everyone' -AccessRights ReadData -PassThru -ErrorVariable addErrors -ErrorAction SilentlyContinue) + + $addErrors | Should -HaveCount 1 + $addErrors[0].FullyQualifiedErrorId | Should -BeLike 'AddAceError,*' + $result | Should -BeNullOrEmpty + } + } +} + Describe 'Security descriptor parameter sets' { BeforeAll { $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ParameterSets' -Directory diff --git a/Tests/FileHash.Tests.ps1 b/Tests/FileHash.Tests.ps1 index 2ffb4c4..91eeb2c 100644 --- a/Tests/FileHash.Tests.ps1 +++ b/Tests/FileHash.Tests.ps1 @@ -6,6 +6,11 @@ )] param () +BeforeDiscovery { + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force + $isElevated = Test-IsElevated +} + BeforeAll { Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force $modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\NTFSSecurity\bin\Release\NTFSSecurity.psd1' @@ -62,4 +67,28 @@ Describe 'Get-FileHash2' { $results.Name | Should -Be @('One.txt') } } + + Context 'When the file cannot be read after taking ownership' { + # Before 5.0.0, the account that ran the cmdlet stayed the owner when the second attempt failed. Only an + # elevated process can make another account the owner first, so the test runs in CI. + It 'Should restore the previous owner' -Skip:(-not $isElevated) { + if ($PSVersionTable.PSEdition -eq 'Core') { + Set-ItResult -Skipped -Because 'Get-FileHash2 fails in PowerShell 7 until it no longer references RIPEMD160' + } + $denied = New-TestSandboxItem -Sandbox $sandbox -Name 'Denied' + Assert-TestSandboxPath -Sandbox $sandbox -Path $denied + Set-NTFSOwner -Path $denied -Account 'S-1-5-32-544' + Add-NTFSAccess -Path $denied -Account 'S-1-1-0' -AccessRights ReadData -AccessType Deny + + $results = @(Get-FileHash2 -Path $denied -ErrorVariable hashErrors -ErrorAction SilentlyContinue) + if (-not $hashErrors) { + Set-ItResult -Inconclusive -Because 'the elevated process could read the file despite the deny entry' + } + + $hashErrors | Should -HaveCount 1 + $hashErrors[0].FullyQualifiedErrorId | Should -BeLike 'GetHashError,*' + $results | Should -BeNullOrEmpty + (Get-NTFSOwner -Path $denied).Owner.Sid | Should -Be 'S-1-5-32-544' + } + } } diff --git a/Tests/Inheritance.Tests.ps1 b/Tests/Inheritance.Tests.ps1 index e809462..80d5fc3 100644 --- a/Tests/Inheritance.Tests.ps1 +++ b/Tests/Inheritance.Tests.ps1 @@ -80,7 +80,7 @@ Describe 'Inheritance cmdlets with -PassThru' { $result | Should -BeNullOrEmpty } - It '<_> should write an error and return nothing when the item cannot be read' -Skip:$canBypassDeny -ForEach @( + It '<_> should write an error and return nothing when the security descriptor cannot be read' -Skip:$canBypassDeny -ForEach @( 'Enable-NTFSAccessInheritance', 'Disable-NTFSAccessInheritance' ) { $file = New-TestSandboxItem -Sandbox $sandbox -Name 'Denied' @@ -88,7 +88,21 @@ Describe 'Inheritance cmdlets with -PassThru' { $result = @(& $_ -Path $file -PassThru -ErrorVariable inheritanceErrors -ErrorAction SilentlyContinue) - $inheritanceErrors | Should -Not -BeNullOrEmpty + $inheritanceErrors | Should -HaveCount 1 + $inheritanceErrors[0].FullyQualifiedErrorId | Should -BeLike 'ModifySdError,*' + $result | Should -BeNullOrEmpty + } + + It '<_> should write a read error and return nothing for a path that does not exist' -ForEach @( + 'Enable-NTFSAccessInheritance', 'Disable-NTFSAccessInheritance', + 'Enable-NTFSAuditInheritance', 'Disable-NTFSAuditInheritance' + ) { + $missing = Join-Path -Path $sandbox -ChildPath 'Missing.txt' + + $result = @(& $_ -Path $missing -PassThru -ErrorVariable inheritanceErrors -ErrorAction SilentlyContinue) + + $inheritanceErrors | Should -HaveCount 1 + $inheritanceErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadFileError,*' $result | Should -BeNullOrEmpty } } diff --git a/Tests/TestHelpers.psm1 b/Tests/TestHelpers.psm1 index 5f11a15..b47faaf 100644 --- a/Tests/TestHelpers.psm1 +++ b/Tests/TestHelpers.psm1 @@ -195,14 +195,70 @@ function Block-TestReadPermission { $Path ) + Add-TestDenyRule -Sandbox $Sandbox -Path $Path -Rights @{ 'S-1-3-4' = 'ReadPermissions' } +} + +function Block-TestWritePermission { + <# + .SYNOPSIS + Denies the owner of an item in the sandbox to change its permissions, so that writing its DACL fails. + .DESCRIPTION + Adds a deny entry for OWNER RIGHTS (S-1-3-4) with ChangePermissions, which replaces the implicit right of the + owner to write the DACL. Taking ownership drops that entry, so the current user is also denied + TakeOwnership; without a privilege, an attempt of the module to take ownership and try again fails as well. + Reading the item and its security descriptor still works, and Remove-TestSandbox deletes the item through + the rights on its folder. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [string] + $Sandbox, + + [Parameter(Mandatory)] + [string] + $Path + ) + + $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value + Add-TestDenyRule -Sandbox $Sandbox -Path $Path -Rights @{ + 'S-1-3-4' = 'ChangePermissions' + $currentUser = 'TakeOwnership' + } +} + +function Add-TestDenyRule { + <# + .SYNOPSIS + Adds deny entries to an item in the sandbox in one write, so that an entry cannot block writing the next. + .PARAMETER Rights + The rights to deny, keyed by the SID of the account. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [string] + $Sandbox, + + [Parameter(Mandatory)] + [string] + $Path, + + [Parameter(Mandatory)] + [System.Collections.IDictionary] + $Rights + ) + Assert-TestSandboxPath -Sandbox $Sandbox -Path $Path $acl = Get-Acl -LiteralPath $Path - $ownerRights = New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList 'S-1-3-4' - $rule = New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList ( - $ownerRights, [System.Security.AccessControl.FileSystemRights]::ReadPermissions, - [System.Security.AccessControl.AccessControlType]::Deny - ) - $acl.AddAccessRule($rule) + foreach ($sid in $Rights.Keys) { + $identity = New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList $sid + $rule = New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList ( + $identity, [System.Security.AccessControl.FileSystemRights] $Rights[$sid], + [System.Security.AccessControl.AccessControlType]::Deny + ) + $acl.AddAccessRule($rule) + } Set-Acl -LiteralPath $Path -AclObject $acl } @@ -247,4 +303,4 @@ function Test-PrivilegeHeld { } Export-ModuleMember -Function New-TestSandbox, Assert-TestSandboxPath, Remove-TestSandbox, New-TestSandboxItem, - Block-TestReadPermission, Test-IsElevated, Test-PrivilegeHeld + Block-TestReadPermission, Block-TestWritePermission, Test-IsElevated, Test-PrivilegeHeld