Browse Source

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 <ai@example.com>
pull/102/head
Raimund Andree 1 week ago
parent
commit
26b9268122
  1. 3
      .memory-bank/progress.md
  2. 6
      CHANGELOG.md
  3. 2
      Docs/Cmdlets/Add-NTFSAccess.md
  4. 4
      Docs/Cmdlets/Add-NTFSAudit.md
  5. 2
      Docs/Cmdlets/Clear-NTFSAudit.md
  6. 2
      Docs/Cmdlets/Remove-NTFSAccess.md
  7. 4
      Docs/Cmdlets/Remove-NTFSAudit.md
  8. 3
      Docs/Concepts.md
  9. 14
      NTFSSecurity/AccessCmdlets/AddAccess.cs
  10. 16
      NTFSSecurity/AccessCmdlets/ClearAccess.cs
  11. 10
      NTFSSecurity/AccessCmdlets/GetAccess.cs
  12. 12
      NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs
  13. 12
      NTFSSecurity/AccessCmdlets/GetOrphanedAccess.cs
  14. 14
      NTFSSecurity/AccessCmdlets/RemoveAccess.cs
  15. 14
      NTFSSecurity/AuditCmdlets/AddAudit.cs
  16. 16
      NTFSSecurity/AuditCmdlets/ClearAudit.cs
  17. 14
      NTFSSecurity/AuditCmdlets/RemoveAudit.cs
  18. 32
      NTFSSecurity/BaseCmdlets.cs
  19. 12
      NTFSSecurity/InheritanceCmdlets/DisableAccessInheritance.cs
  20. 12
      NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs
  21. 12
      NTFSSecurity/InheritanceCmdlets/EnableAccessInheritance.cs
  22. 12
      NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs
  23. 12
      NTFSSecurity/InheritanceCmdlets/GetInheritance.cs
  24. 12
      NTFSSecurity/InheritanceCmdlets/SetInheritance.cs
  25. 12
      NTFSSecurity/MiscCmdlets/GetFileHash2.cs
  26. 12
      NTFSSecurity/SecurityDescriptorCmdlets/GetSecurityDescriptor.cs
  27. 14
      NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml
  28. 39
      Tests/Access.Tests.ps1
  29. 29
      Tests/FileHash.Tests.ps1
  30. 18
      Tests/Inheritance.Tests.ps1
  31. 70
      Tests/TestHelpers.psm1

3
.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

6
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

2
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

4
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.

2
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

2
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

4
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.

3
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

14
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)

16
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)
{

10
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)
{

12
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)
{

12
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)
{

14
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)

14
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)

16
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)
{

14
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)

32
NTFSSecurity/BaseCmdlets.cs

@ -148,6 +148,38 @@ namespace NTFSSecurity
return path;
}
#endregion
#region InvokeAsOwner
/// <summary>
/// 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.
/// </summary>
/// <param name="item">The file or folder to take ownership of.</param>
/// <param name="path">The path the user specified, used as the error target.</param>
/// <param name="action">The operation to run while the current user owns the item.</param>
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

12
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)
{

12
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)
{

12
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)
{

12
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)
{

12
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)
{

12
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)
{

12
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)
{

12
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)
{

14
NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml

@ -712,7 +712,7 @@
<maml:name>Security2.FileSystemAccessRule2</maml:name>
</dev:type>
<maml:description>
<maml:para>With `-PassThru`, the cmdlet writes all access control entries of every processed item, explicit and inherited. Without `-PassThru` it writes nothing.</maml:para>
<maml:para>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.</maml:para>
</maml:description>
</command:returnValue>
</command:returnValues>
@ -1493,7 +1493,7 @@
<maml:name>Security2.FileSystemAuditRule2</maml:name>
</dev:type>
<maml:description>
<maml:para>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.</maml:para>
<maml:para>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.</maml:para>
</maml:description>
</command:returnValue>
</command:returnValues>
@ -1501,7 +1501,7 @@
<maml:alert>
<maml:para>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.</maml:para>
<maml:para>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.</maml:para>
<maml:para>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.</maml:para>
<maml:para>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.</maml:para>
<maml:para>`-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.</maml:para>
<maml:para>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.</maml:para>
</maml:alert>
@ -1913,7 +1913,7 @@ PS C:\&gt; Set-NTFSSecurityDescriptor -SecurityDescriptor $sd</dev:code>
<maml:alert>
<maml:para>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.</maml:para>
<maml:para>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.</maml:para>
<maml:para>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.</maml:para>
<maml:para>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.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
@ -8380,7 +8380,7 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
<maml:name>Security2.FileSystemAccessRule2</maml:name>
</dev:type>
<maml:description>
<maml:para>With `-PassThru`, the cmdlet writes all access control entries of every processed item, explicit and inherited. Without `-PassThru` it writes nothing.</maml:para>
<maml:para>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.</maml:para>
</maml:description>
</command:returnValue>
</command:returnValues>
@ -9219,7 +9219,7 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
<maml:name>Security2.FileSystemAuditRule2</maml:name>
</dev:type>
<maml:description>
<maml:para>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.</maml:para>
<maml:para>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.</maml:para>
</maml:description>
</command:returnValue>
</command:returnValues>
@ -9227,7 +9227,7 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
<maml:alert>
<maml:para>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.</maml:para>
<maml:para>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.</maml:para>
<maml:para>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.</maml:para>
<maml:para>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.</maml:para>
<maml:para>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.</maml:para>
<maml:para>Before 5.0.0, the cmdlet had no `-RemoveSpecific` switch.</maml:para>
<maml:para>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`.</maml:para>

39
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

29
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'
}
}
}

18
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
}
}

70
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

Loading…
Cancel
Save