diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b29616..b49427e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,6 +97,37 @@ The format is based on `FileNotFoundException`. A volume that is mounted into a folder, and a junction or symbolic link to another volume, probably fail the same way. Use a path that starts at the root of the volume +- **Breaking:** match `Get-ChildItem2 -Filter` once, in the cmdlet, with the + wildcard rules that `Get-ChildItem -Filter` applies in Windows PowerShell + 5.1, in both editions, and on the real name of an item, not its 8.3 short + name. `Report.*` also returns `Report`, which has no dot; a pattern that + ends in a dot, such as `Report.` or `*.`, returns the names without a dot; + `*.*.*` returns every item; and a question mark also matches nothing at + the end of a name and before a dot, so `a?` also returns `a`. An empty + `-Filter` returns every item, like `*`, instead of nothing, and a pattern + of more than 32,767 characters is a parameter error. Two kinds of pattern + are not like Windows PowerShell 5.1: blanks at the end of a pattern, also + before a final dot, are part of it, where Windows PowerShell 5.1 drops them, + and a pattern that ends in an even number of dots, such as two, is + accepted, where Windows PowerShell 5.1 stops with an error. In + PowerShell 7, `Get-ChildItem2` deliberately differs from + `Get-ChildItem -Filter` for some patterns, for example for a pattern that + ends in a dot, unless an asterisk comes right before the dot (the pattern + of a single dot returns every item), and for a pattern in which an asterisk + is followed by a dot and question marks +- **Breaking:** write a non-terminating error in `Get-NTFSOrphanedAccess` for + an item that it can't read for a reason other than a denied access, and + continue with the next item: `ReadSecurityError` with the category + `OpenError`; it wrote a warning. A script that reads the failure with + `-WarningVariable` needs `-ErrorVariable`, and `-ErrorAction Stop` now + stops at it +- Take `-SecurityDescriptor` of `Set-NTFSSecurityDescriptor` at position 1, + like the first positional parameter of the other cmdlets; it was at + position 2. A positional call isn't affected, because it is the only + positional parameter +- Document that `Get-NTFSEffectiveAccess` writes the error "Access is denied" + for a user account of the domain when the user who runs the cmdlet has no + domain credentials, such as a local standard user ### Deprecated @@ -121,6 +152,32 @@ The format is based on - Compare boxed privilege output values by their privilege and attributes; the object overload rejected privilege values and recursively compared an attributes enum instead +- Convert a `FileSecurity` or a `DirectorySecurity` object to + `FileSystemSecurity2` in the .NET API of `Security2.dll`: the conversions + ignored the descriptor and threw an exception for an empty path. The result + wraps the descriptor and has no item: `FullName` and `Name` are `$null`, + `Write()` throws an `InvalidOperationException`, and the entries, the + owner, the inheritance, and the effective access can be read; an inherited + entry has no source. PowerShell uses the conversion to bind + `-SecurityDescriptor`, so the cmdlets now accept the object that `Get-Acl` + returns, where they stopped with a parameter binding error. Such a + descriptor can't be written, so `Set-NTFSSecurityDescriptor` writes a + `WriteSdError` for it, and the audit cmdlets write a `ReadSecurityError` + for one that has no audit entries +- **Breaking:** fix `RemoveFileSystemAccessRuleAll` and + `RemoveFileSystemAuditRuleAll` of the .NET API, which ignored the list of + accounts and removed every explicit entry; they remove only the entries of + the listed accounts, and an empty list removes none +- **Breaking:** fix the overloads of `AddFileSystemAccessRule` and + `AddFileSystemAuditRule` of the .NET API that take a path and a list of + accounts, which did nothing until the caller enumerated the result; they + write at once, and an exception reaches the caller of the method +- Fix a module setting `EnablePrivileges` that is missing or not `$true` or + `$false`, for example after + `(Get-Module NTFSSecurity).PrivateData['EnablePrivileges'] = 'false'`: the + cmdlets that use privileges failed with an `InvalidCastException` or a + `NullReferenceException`; they stop with one error, + `EnablePrivilegesSettingInvalid`, which names the setting - Fix `Clear-NTFSAccess -DisableInheritance` and `Set-NTFSSecurityDescriptor`, which reported a `RestoreOwnerError` for an owner that had not changed: after they took ownership of an item that the user owned already and left a @@ -150,6 +207,15 @@ The format is based on exception of a later command when that command took the debug message after the enabling, for example with `5>&1 | Select-Object -First 2`; they now disable the privilege and pass the exception on +- Fix the 17 cmdlets that take ownership of an item to retry a denied read or + change and then set the previous owner back: the `Add-`, `Remove-`, and + `Clear-` cmdlets for access and audit entries, `Get-NTFSAccess`, + `Get-NTFSOrphanedAccess`, `Get-NTFSEffectiveAccess`, the six inheritance + cmdlets, `Get-FileHash2`, and `Set-NTFSSecurityDescriptor`. When a later + command in the pipeline threw at the `RestoreOwnerError` that setting the + owner back can write, they reported the exception as the error of the item + and went on with the next one, so that the caller never saw it; they now + pass it on - Fix `Get-ChildItem2 -Filter`, which read a bracket as the start of a character class, so that it did not return a file with brackets in its name, such as `Report[1].txt`, for that name; only `*` and `?` are wildcards. A @@ -243,22 +309,55 @@ The format is based on `-PropagationFlags`; `-AppliesTo` is now mandatory in the `Simple` parameter sets, so such a command uses the flag parameters and their defaults, as for a path -- Fix `Add-NTFSAccess`, `Remove-NTFSAccess`, `Add-NTFSAudit`, and +- **Breaking:** fix `Add-NTFSAccess`, `Remove-NTFSAccess`, `Add-NTFSAudit`, and `Remove-NTFSAudit`, which ignored the `AppliesTo` property of an object piped to them with a path and used the default scope, and which gave a - piped security descriptor object without a scope the scope of the object - before it; the property now sets the scope of its own object only. A piped + piped security descriptor object that had only `InheritanceFlags` or + `PropagationFlags` the scope of the object before it; the property now sets + the scope of its own object only. A piped object that has `AppliesTo` and `InheritanceFlags` or `PropagationFlags` now fails with `AmbiguousParameterSet`, like the same parameters on the command line, and so does an object that has both `Path` and - `SecurityDescriptor`, where the path won before. An `AppliesTo` that is - `$null` fails to bind for that object (a row of a CSV file that has fewer - fields than its header gives `$null`). A value that PowerShell can't + `SecurityDescriptor`, where the path won before. The empty cell of a CSV + file, `$null`, and `DBNull` count as a value of the other group here, + because PowerShell binds them to the parameters of both groups: a file + that has columns of both groups fails for a row that leaves one of them + empty, so give such a file the columns of one group only. An `AppliesTo` + that is `$null` fails to bind for that object (a row of a CSV file that has + fewer fields than its header gives `$null`). A value that PowerShell can't convert to a scope, such as the empty cell of a CSV file or a misspelled - name, is ignored, and the object is handled as one without `AppliesTo`: it - gets its own flags, or the default scope if it has none. A call with + name, is an error for that object, see the next entry. A call with neither `-Path` nor `-SecurityDescriptor` now fails with `AmbiguousParameterSet` instead of a missing-parameter error for `-Path` +- Fix `Add-NTFSAccess`, `Remove-NTFSAccess`, `Add-NTFSAudit`, and + `Remove-NTFSAudit`, which silently used the default when a piped object had + a value of `AccessType` (`AuditFlags` for the audit cmdlets), + `InheritanceFlags`, `PropagationFlags`, or `AppliesTo` that PowerShell + could not convert, such as a misspelled name or the empty cell of a CSV + file: a typo for `Deny` created an `Allow` entry, and a misspelled scope + used the default scope, without a message. The cmdlets now write the + non-terminating error `InvalidParameterValue` for that object, change + nothing for it, and go on with the next one; a blank value is such an + error too, where it used the default. **Breaking:** the five parameters + have the type `Object`, because the cmdlets convert the values themselves. + A bad value that you name on the command line, and a named `$null`, still + stop the command, but with the error id `InvalidParameterValue`, not the id + of a parameter binding error; `-AppliesTo $null` keeps the error of the + binder, because the parameter is mandatory. `-AccessType` and `-AppliesTo` + take one value: a comma list or a collection of several values is an + `InvalidParameterValue` error, where PowerShell combined the numbers of the + values bit by bit, so that `-AppliesTo 'ThisFolderOnly,FilesOnly'` created a + `FilesOnly` entry and `-AccessType 'Allow,Deny'` a `Deny` entry. A script + block, such as `-AccessType { 'Deny' }`, is no longer evaluated for every + piped object and is a bad value. `Get-Help` shows the type `Object`, and + tab completion still offers the valid names +- **Breaking:** fix `Add-NTFSAccess`, `Remove-NTFSAccess`, `Add-NTFSAudit`, and + `Remove-NTFSAudit` with `-SecurityDescriptor`, which ended the whole + command with a terminating error when the change of one descriptor failed, + such as for a deny entry without rights; they write the non-terminating + error `AddAceError` or `RemoveAceError` for that descriptor and go on with + the next one, as they do for a path. A script that relies on the stop needs + `-ErrorAction Stop` - Fix `Get-NTFSEffectiveAccess`: `-ExcludeNoneAccessEntries` now leaves out items without access, the cmdlet uses the current location when `-Path` is omitted, and `-SecurityDescriptor` returns the effective access of the diff --git a/Docs/Cmdlets/Add-NTFSAccess.md b/Docs/Cmdlets/Add-NTFSAccess.md index 3938707..19871b7 100644 --- a/Docs/Cmdlets/Add-NTFSAccess.md +++ b/Docs/Cmdlets/Add-NTFSAccess.md @@ -16,28 +16,28 @@ Adds an access control entry (ACE) to a file, a folder, or a security descriptor ### PathSimple ``` Add-NTFSAccess [-Path] [-Account] [-AccessRights] - [-AccessType ] -AppliesTo [-PassThru] [] + [-AccessType ] -AppliesTo [-PassThru] [] ``` ### PathComplex ``` Add-NTFSAccess [-Path] [-Account] [-AccessRights] - [-AccessType ] [-InheritanceFlags ] - [-PropagationFlags ] [-PassThru] [] + [-AccessType ] [-InheritanceFlags ] [-PropagationFlags ] [-PassThru] + [] ``` ### SDSimple ``` Add-NTFSAccess [-SecurityDescriptor] [-Account] - [-AccessRights] [-AccessType ] -AppliesTo [-PassThru] + [-AccessRights] [-AccessType ] -AppliesTo [-PassThru] [] ``` ### SDComplex ``` Add-NTFSAccess [-SecurityDescriptor] [-Account] - [-AccessRights] [-AccessType ] [-InheritanceFlags ] - [-PropagationFlags ] [-PassThru] [] + [-AccessRights] [-AccessType ] [-InheritanceFlags ] + [-PropagationFlags ] [-PassThru] [] ``` ## DESCRIPTION @@ -46,7 +46,7 @@ Adds an access control entry (ACE) to the discretionary access control list (DAC `-AccessRights` accepts the basic rights such as `Read`, `Modify`, and `FullControl` as well as the granular rights such as `CreateFiles` or `WriteAttributes`, and several values can be combined, for example `-AccessRights ReadData, WriteData, Delete`. For the mapping between the values of this module, the rights that Windows displays, and the entries of the advanced security dialog, see [Concepts](../Concepts.md). -The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`; both describe the same ACE flags. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo` that PowerShell can't convert to a scope, such as the blank cell of a CSV file or a misspelled name, is ignored, and the object is handled as one without `AppliesTo`: it uses its own `InheritanceFlags` and `PropagationFlags`, or the default scope if it has none. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. +The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`; both describe the same ACE flags. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a piped value was silently ignored, and the object was handled as one without it. A script block is a plain value for these parameters, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type; before 5.0.0, a script block was evaluated once for each piped object. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When `-AccessType`, `-AppliesTo`, `-InheritanceFlags`, and `-PropagationFlags` are omitted, the cmdlet adds an `Allow` ACE that applies to this folder, subfolders, and files, which corresponds to the inheritance flags `ContainerInherit, ObjectInherit` and no propagation flags. An `Allow` ACE always receives the `Synchronize` right in addition to the requested rights, inheritance and propagation flags are ignored on files, and rights for an account that already has an ACE with the same access type and the same flags are merged into that ACE. The cmdlet writes no output unless `-PassThru` is used, and a failure on one item is reported as a non-terminating error while the remaining items are processed. @@ -107,10 +107,10 @@ Accept wildcard characters: False ### -AccessType -Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. +Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. ```yaml -Type: AccessControlType +Type: Object Parameter Sets: (All) Aliases: AccessControlType Accepted values: Allow, Deny @@ -140,10 +140,10 @@ Accept wildcard characters: False ### -AppliesTo -Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. +Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ```yaml -Type: ApplyTo +Type: Object Parameter Sets: PathSimple, SDSimple Aliases: Accepted values: ThisFolderOnly, ThisFolderSubfoldersAndFiles, ThisFolderAndSubfolders, ThisFolderAndFiles, SubfoldersAndFilesOnly, SubfoldersOnly, FilesOnly, ThisFolderSubfoldersAndFilesOneLevel, ThisFolderAndSubfoldersOneLevel, ThisFolderAndFilesOneLevel, SubfoldersAndFilesOnlyOneLevel, SubfoldersOnlyOneLevel, FilesOnlyOneLevel @@ -157,12 +157,12 @@ Accept wildcard characters: False ### -InheritanceFlags -Specifies which kind of child objects inherit the ACE. `ContainerInherit` passes the ACE on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the ACE on the item itself. The default is `ContainerInherit, ObjectInherit`. Inheritance flags have no effect on files, where the ACE is always created with `None`. +Specifies which kind of child objects inherit the ACE. `ContainerInherit` passes the ACE on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the ACE on the item itself. The default is `ContainerInherit, ObjectInherit`. Inheritance flags have no effect on files, where the ACE is always created with `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. For details about the flags, see the .NET documentation of the [InheritanceFlags Enum](https://learn.microsoft.com/en-us/dotnet/api/system.security.accesscontrol.inheritanceflags). ```yaml -Type: InheritanceFlags +Type: Object Parameter Sets: PathComplex, SDComplex Aliases: Accepted values: None, ContainerInherit, ObjectInherit @@ -208,10 +208,10 @@ Accept wildcard characters: False ### -PropagationFlags -Specifies how the ACE is propagated to child objects. `None` propagates the ACE to all levels that the inheritance flags allow, `InheritOnly` keeps the ACE from applying to the item it is defined on, and `NoPropagateInherit` limits inheritance to the direct children of the folder. The default is `None`, and propagation flags only have an effect in combination with `-InheritanceFlags`. +Specifies how the ACE is propagated to child objects. `None` propagates the ACE to all levels that the inheritance flags allow, `InheritOnly` keeps the ACE from applying to the item it is defined on, and `NoPropagateInherit` limits inheritance to the direct children of the folder. The default is `None`, and propagation flags only have an effect in combination with `-InheritanceFlags`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. ```yaml -Type: PropagationFlags +Type: Object Parameter Sets: PathComplex, SDComplex Aliases: Accepted values: None, NoPropagateInherit, InheritOnly diff --git a/Docs/Cmdlets/Add-NTFSAudit.md b/Docs/Cmdlets/Add-NTFSAudit.md index 3ee64ab..7d9d4d2 100644 --- a/Docs/Cmdlets/Add-NTFSAudit.md +++ b/Docs/Cmdlets/Add-NTFSAudit.md @@ -16,28 +16,28 @@ Adds an audit entry to a file or folder. ### PathSimple ``` Add-NTFSAudit [-Path] [-Account] [-AccessRights] - [-AuditFlags ] -AppliesTo [-PassThru] [] + [-AuditFlags ] -AppliesTo [-PassThru] [] ``` ### PathComplex ``` Add-NTFSAudit [-Path] [-Account] [-AccessRights] - [-AuditFlags ] [-InheritanceFlags ] [-PropagationFlags ] - [-PassThru] [] + [-AuditFlags ] [-InheritanceFlags ] [-PropagationFlags ] [-PassThru] + [] ``` ### SDSimple ``` Add-NTFSAudit [-SecurityDescriptor] [-Account] - [-AccessRights] [-AuditFlags ] -AppliesTo [-PassThru] + [-AccessRights] [-AuditFlags ] -AppliesTo [-PassThru] [] ``` ### SDComplex ``` Add-NTFSAudit [-SecurityDescriptor] [-Account] - [-AccessRights] [-AuditFlags ] [-InheritanceFlags ] - [-PropagationFlags ] [-PassThru] [] + [-AccessRights] [-AuditFlags ] [-InheritanceFlags ] + [-PropagationFlags ] [-PassThru] [] ``` ## DESCRIPTION @@ -46,7 +46,7 @@ The `Add-NTFSAudit` cmdlet adds an audit entry to the system access control list In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path`, adds the entry, and writes the descriptor back right away. In the `SDSimple` and `SDComplex` parameter sets it adds the entry to an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned; that change only reaches the file system when you pass the object to `Set-NTFSSecurityDescriptor`. The simple sets describe the scope of the entry with the single `-AppliesTo` parameter, the complex sets with `-InheritanceFlags` and `-PropagationFlags`. -A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo` that PowerShell can't convert to a scope, such as the blank cell of a CSV file or a misspelled name, is ignored, and the object is handled as one without `AppliesTo`: it uses its own `InheritanceFlags` and `PropagationFlags`, or the default scope if it has none. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. +A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a piped value was silently ignored, and the object was handled as one without it. A script block is a plain value for these parameters, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type; before 5.0.0, a script block was evaluated once for each piped object. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When you omit them, `-AuditFlags` is `Success, Failure`, `-InheritanceFlags` is `ContainerInherit, ObjectInherit`, `-PropagationFlags` is `None`, and `-AppliesTo` is `ThisFolderSubfoldersAndFiles`, so both the simple and the complex set audit the item, its subfolders, and its files by default. Inheritance applies to folders only: when the item is a file, the cmdlet stores the entry without inheritance and propagation flags. @@ -125,10 +125,10 @@ Accept wildcard characters: False ### -AppliesTo -Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. +Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ```yaml -Type: ApplyTo +Type: Object Parameter Sets: PathSimple, SDSimple Aliases: Accepted values: ThisFolderOnly, ThisFolderSubfoldersAndFiles, ThisFolderAndSubfolders, ThisFolderAndFiles, SubfoldersAndFilesOnly, SubfoldersOnly, FilesOnly, ThisFolderSubfoldersAndFilesOneLevel, ThisFolderAndSubfoldersOneLevel, ThisFolderAndFilesOneLevel, SubfoldersAndFilesOnlyOneLevel, SubfoldersOnlyOneLevel, FilesOnlyOneLevel @@ -142,10 +142,10 @@ Accept wildcard characters: False ### -AuditFlags -Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. +Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. ```yaml -Type: AuditFlags +Type: Object Parameter Sets: (All) Aliases: Accepted values: None, Success, Failure @@ -159,10 +159,10 @@ Accept wildcard characters: False ### -InheritanceFlags -Specifies which child items inherit the audit entry. `ContainerInherit` passes the entry on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the entry on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. Use `-PropagationFlags` to control whether the entry also applies to the item itself and how far it propagates. +Specifies which child items inherit the audit entry. `ContainerInherit` passes the entry on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the entry on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. Use `-PropagationFlags` to control whether the entry also applies to the item itself and how far it propagates. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. ```yaml -Type: InheritanceFlags +Type: Object Parameter Sets: PathComplex, SDComplex Aliases: Accepted values: None, ContainerInherit, ObjectInherit @@ -208,10 +208,10 @@ Accept wildcard characters: False ### -PropagationFlags -Specifies how the inheritance selected with `-InheritanceFlags` propagates. `None` applies the entry to the item itself and to all inheriting child items, `InheritOnly` applies it to the inheriting child items but not to the item itself, and `NoPropagateInherit` limits inheritance to the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. The parameter has no effect when `-InheritanceFlags` is `None`. +Specifies how the inheritance selected with `-InheritanceFlags` propagates. `None` applies the entry to the item itself and to all inheriting child items, `InheritOnly` applies it to the inheriting child items but not to the item itself, and `NoPropagateInherit` limits inheritance to the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. The parameter has no effect when `-InheritanceFlags` is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. ```yaml -Type: PropagationFlags +Type: Object Parameter Sets: PathComplex, SDComplex Aliases: Accepted values: None, NoPropagateInherit, InheritOnly diff --git a/Docs/Cmdlets/Get-ChildItem2.md b/Docs/Cmdlets/Get-ChildItem2.md index 52275af..4e140ae 100644 --- a/Docs/Cmdlets/Get-ChildItem2.md +++ b/Docs/Cmdlets/Get-ChildItem2.md @@ -23,7 +23,7 @@ Get-ChildItem2 [[-Path] ] [[-Filter] ] [-Recurse] [-Directory] The `Get-ChildItem2` cmdlet lists the files and folders in the folders that you specify with `-Path`. It returns an `Alphaleonis.Win32.Filesystem.FileInfo` object for every file and an `Alphaleonis.Win32.Filesystem.DirectoryInfo` object for every folder. The cmdlet is the long-path counterpart of the built-in `Get-ChildItem` cmdlet: it enumerates the file system through the AlphaFS library (`Alphaleonis.Win32.Filesystem`) instead of `System.IO`, so it also returns items whose path is longer than the 260-character `MAX_PATH` limit. -If you omit `-Path`, the cmdlet lists the current location. Relative paths and the `.` and `..` notations are resolved against the current location. Each path must name a folder, and wildcard characters are not supported. The parameter accepts pipeline input by value and by the property name `FullName`, so you can pipe folders from `Get-ChildItem2` or `Get-Item2` into another `Get-ChildItem2` call, and you can pipe the result into `Get-NTFSAccess` and the other NTFSSecurity cmdlets. +If you omit `-Path`, the cmdlet lists the current location. Relative paths and the `.` and `..` notations are resolved against the current location. Each path must name an existing folder or file, and wildcard characters are not supported. The parameter accepts pipeline input by value and by the property name `FullName`, so you can pipe folders from `Get-ChildItem2` or `Get-Item2` into another `Get-ChildItem2` call, and you can pipe the result into `Get-NTFSAccess` and the other NTFSSecurity cmdlets. By default the cmdlet returns the immediate content of each folder and omits hidden items. Use `-Recurse` to walk the whole tree, `-Depth` to limit how deep the recursion goes, `-Filter` to restrict the result by name, `-Directory` or `-File` to restrict it by item type, and `-Force`, `-Hidden`, `-System`, `-ReadOnly`, or `-Attributes` to restrict it by file attributes. @@ -142,7 +142,17 @@ Accept wildcard characters: False ### -Filter -Specifies a name pattern that an item must match to be returned. The pattern supports the asterisk and the question mark as wildcard characters, an asterisk for any number of characters and a question mark for exactly one, and the match ignores case. Any other character stands for itself; a bracket is an ordinary character, so `Report[1].txt` returns the file of that name. As for `Get-ChildItem`, a pattern of an asterisk, a dot, and an asterisk returns every item, also an item without a dot in its name. The default value is `*`, which returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder. +Specifies a name pattern that an item must match to be returned. The pattern follows the wildcard rules that Windows PowerShell 5.1 applies to `Get-ChildItem -Filter`, in both editions, on the real name of the item, not its 8.3 short name. An asterisk stands for a run of any characters, and a question mark stands for one character that is not a dot, or for nothing where a dot or the end of the name follows. A dot stands for a literal dot, or for nothing where only wildcards or the end of the pattern follow. The match ignores case and is not affected by the culture of the session. Any other character stands for itself, including a bracket, so `Report[1].txt` returns the file of that name. + +For example, `Report.*` also returns the item `Report`, which has no dot. + +A pattern of an asterisk, a dot, and an asterisk returns every item, also an item without a dot in its name, as `Get-ChildItem` does for this pattern. + +`Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions, so the result does not depend on the edition. The built-in `Get-ChildItem -Filter` of PowerShell 7 differs for some patterns, for example for a pattern that ends in a dot, for which it returns nothing unless an asterisk comes right before the dot (the pattern of a single dot returns every item), and for a pattern in which an asterisk is followed by a dot and question marks, for which it ignores the dot and the question marks. + +Two kinds of pattern are not like Windows PowerShell 5.1: blanks at the end of the pattern, also before a final dot, are part of it, where Windows PowerShell 5.1 drops them, and a pattern that ends in an even number of dots, such as two, is accepted, where Windows PowerShell 5.1 stops with an error. + +The default value is `*`, which returns every item, and an empty value also returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder. ```yaml Type: String @@ -319,7 +329,11 @@ Before 5.0.0, a `-Path` value that points to a file stopped the cmdlet with an ` Before 5.0.0, `-Filter` read a bracket as the start of a character class, so a file with brackets in its name, such as `Report[1].txt`, was not returned for its name, and a pattern of an asterisk, a dot, and an asterisk dropped the items without a dot in their names, most folders among them. -The names are matched twice, by the enumeration of the AlphaFS library and by the cmdlet, which reads a dot as an ordinary character, and their rules for a dot can differ from those of `Get-ChildItem`: a pattern such as `Report.*` does not return the file `Report`, which has no dot, a pattern that ends in a dot returns nothing, and an empty value returns nothing. Only the pattern of an asterisk, a dot, and an asterisk is treated as a single asterisk. +The cmdlet enumerates broadly and matches each name once, with the Win32 "DOS" wildcard rules that Windows PowerShell 5.1 applies to `Get-ChildItem -Filter`, in both editions: a pattern that ends in a dot returns the names without a dot, and an empty value returns every item, the same as a plain asterisk. + +For example, `Report.*` also returns the item `Report`, which has no dot. A star in front of a dot token may cross every dot in the name but the last one, so a pattern such as a star followed by a dot also reaches the final segment of a name with two or more dots, for example `archive.tar.gz`. + +PowerShell 7 itself differs from Windows PowerShell 5.1 here: its own `Get-ChildItem -Filter` returns nothing for a pattern that ends in a dot, unless the character right before the dot is an asterisk (the pattern of a single dot returns every item), and it ignores the dot and the question marks of a pattern in which an asterisk is followed by a dot and question marks. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead of the edition it runs in, so this is a deliberate difference from the built-in `Get-ChildItem` in PowerShell 7 for these and some similar patterns. Before 5.0.0, a `break`, a `continue`, or a `throw` in a later command of the pipeline did not end the cmdlet for an item below the first folder, also when the later command took the error of a folder that the cmdlet cannot read, for example with `2>&1`. diff --git a/Docs/Cmdlets/Get-NTFSEffectiveAccess.md b/Docs/Cmdlets/Get-NTFSEffectiveAccess.md index d64e3ca..167c622 100644 --- a/Docs/Cmdlets/Get-NTFSEffectiveAccess.md +++ b/Docs/Cmdlets/Get-NTFSEffectiveAccess.md @@ -190,6 +190,8 @@ Before 5.0.0-rc7, a user who wasn't an administrator got the error "Access is de Before 5.0.0-rc8, the cmdlet returned no access and no error when Windows refused to start the authorization manager for the computer of `-ServerName`, for example for a name of 32,718 characters or more, so that the result looked like an account without any rights. The cmdlet now writes a non-terminating `GetEffectiveAccessError` with the error that Windows reported and returns no result for the item, as it does for every other failure of the calculation. It warns that the result is based on this computer only when it calculated a result; before 5.0.0-rc8, the warning also came before an error when the computer couldn't be reached. +A user who has no domain credentials, such as a local standard user, still gets the error "Access is denied" from Windows when it asks for the effective access of a user account of the domain, also for a name of this computer; the cmdlet writes it as a non-terminating `GetEffectiveAccessError` and returns no result for that item. Accounts of this computer and groups such as `Everyone` or `Domain Users` give a result. + ## RELATED LINKS [Get-NTFSAccess](Get-NTFSAccess.md) diff --git a/Docs/Cmdlets/Get-NTFSOrphanedAccess.md b/Docs/Cmdlets/Get-NTFSOrphanedAccess.md index aa3b8d8..2aa63d2 100644 --- a/Docs/Cmdlets/Get-NTFSOrphanedAccess.md +++ b/Docs/Cmdlets/Get-NTFSOrphanedAccess.md @@ -174,6 +174,8 @@ When the module setting `EnablePrivileges` is `$true` (the default in the `Priva If the ACL of an item cannot be read because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. Reading the owner, which that fallback needs first, requires the same Read Permissions right as reading the ACL, so the cmdlet then writes a non-terminating `ReadSecurityError` and continues with the next item. Before 5.0.0, it reported that error as an `AddAceError`. +A read failure that is not an access denial is reported the same way, as a non-terminating `ReadSecurityError`, categorized as `OpenError` and targeting the path; before 5.0.0-rc9, the cmdlet wrote a warning instead (`Could not read item {0}. The error was: {1}`) and returned no entry for the item, without an error to say why. + Before 5.0.0, the cmdlet ignored `-Account` and `-SecurityDescriptor`, and after a path whose ACL could not be read, it returned the orphaned entries of the previous item again. ## RELATED LINKS diff --git a/Docs/Cmdlets/Remove-NTFSAccess.md b/Docs/Cmdlets/Remove-NTFSAccess.md index b849ae3..f10b697 100644 --- a/Docs/Cmdlets/Remove-NTFSAccess.md +++ b/Docs/Cmdlets/Remove-NTFSAccess.md @@ -16,28 +16,28 @@ Removes rights from the access control entries (ACEs) of a file, a folder, or a ### PathSimple ``` Remove-NTFSAccess [-Path] [-Account] [-AccessRights] - [-AccessType ] -AppliesTo [-RemoveSpecific] [-PassThru] [] + [-AccessType ] -AppliesTo [-RemoveSpecific] [-PassThru] [] ``` ### PathComplex ``` Remove-NTFSAccess [-Path] [-Account] [-AccessRights] - [-AccessType ] [-InheritanceFlags ] - [-PropagationFlags ] [-RemoveSpecific] [-PassThru] [] + [-AccessType ] [-InheritanceFlags ] [-PropagationFlags ] [-RemoveSpecific] [-PassThru] + [] ``` ### SDSimple ``` Remove-NTFSAccess [-SecurityDescriptor] [-Account] - [-AccessRights] [-AccessType ] -AppliesTo [-RemoveSpecific] - [-PassThru] [] + [-AccessRights] [-AccessType ] -AppliesTo [-RemoveSpecific] [-PassThru] + [] ``` ### SDComplex ``` Remove-NTFSAccess [-SecurityDescriptor] [-Account] - [-AccessRights] [-AccessType ] [-InheritanceFlags ] - [-PropagationFlags ] [-RemoveSpecific] [-PassThru] [] + [-AccessRights] [-AccessType ] [-InheritanceFlags ] + [-PropagationFlags ] [-RemoveSpecific] [-PassThru] [] ``` ## DESCRIPTION @@ -48,7 +48,7 @@ Only the specified rights are taken away: when an entry grants more than `-Acces Inherited entries cannot be removed from the item that inherits them. Remove them from the folder named in the `InheritedFrom` property, or run `Disable-NTFSAccessInheritance` on the item first, which copies the inherited entries into it as explicit ones that this cmdlet can then remove. -The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo` that PowerShell can't convert to a scope, such as the blank cell of a CSV file or a misspelled name, is ignored, and the object is handled as one without `AppliesTo`: it uses its own `InheritanceFlags` and `PropagationFlags`, or the default scope if it has none. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. All relevant parameters bind by property name, so the output of `Get-NTFSAccess` and `Get-NTFSOrphanedAccess` can be piped directly into this cmdlet. The cmdlet writes no output unless `-PassThru` is used. +The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a piped value was silently ignored, and the object was handled as one without it. A script block is a plain value for these parameters, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type; before 5.0.0, a script block was evaluated once for each piped object. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. All relevant parameters bind by property name, so the output of `Get-NTFSAccess` and `Get-NTFSOrphanedAccess` can be piped directly into this cmdlet. The cmdlet writes no output unless `-PassThru` is used. ## EXAMPLES @@ -105,10 +105,10 @@ Accept wildcard characters: False ### -AccessType -Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. +Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. ```yaml -Type: AccessControlType +Type: Object Parameter Sets: (All) Aliases: AccessControlType Accepted values: Allow, Deny @@ -138,10 +138,10 @@ Accept wildcard characters: False ### -AppliesTo -Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. +Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ```yaml -Type: ApplyTo +Type: Object Parameter Sets: PathSimple, SDSimple Aliases: Accepted values: ThisFolderOnly, ThisFolderSubfoldersAndFiles, ThisFolderAndSubfolders, ThisFolderAndFiles, SubfoldersAndFilesOnly, SubfoldersOnly, FilesOnly, ThisFolderSubfoldersAndFilesOneLevel, ThisFolderAndSubfoldersOneLevel, ThisFolderAndFilesOneLevel, SubfoldersAndFilesOnlyOneLevel, SubfoldersOnlyOneLevel, FilesOnlyOneLevel @@ -155,10 +155,10 @@ Accept wildcard characters: False ### -InheritanceFlags -Specifies the inheritance flags of the entry that is addressed. `ContainerInherit` marks an entry that child folders inherit, `ObjectInherit` marks an entry that child files inherit, and `None` marks an entry that is not inherited at all. The default is `ContainerInherit, ObjectInherit`, which is the scope `ThisFolderSubfoldersAndFiles`. Entries on files always carry `None`. +Specifies the inheritance flags of the entry that is addressed. `ContainerInherit` marks an entry that child folders inherit, `ObjectInherit` marks an entry that child files inherit, and `None` marks an entry that is not inherited at all. The default is `ContainerInherit, ObjectInherit`, which is the scope `ThisFolderSubfoldersAndFiles`. Entries on files always carry `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. ```yaml -Type: InheritanceFlags +Type: Object Parameter Sets: PathComplex, SDComplex Aliases: Accepted values: None, ContainerInherit, ObjectInherit @@ -204,10 +204,10 @@ Accept wildcard characters: False ### -PropagationFlags -Specifies the propagation flags of the entry that is addressed. `None` marks an entry that is inherited by all levels allowed by its inheritance flags, `InheritOnly` marks an entry that does not apply to the item it is defined on, and `NoPropagateInherit` marks an entry that is only inherited by the direct children of the folder. The default is `None`. +Specifies the propagation flags of the entry that is addressed. `None` marks an entry that is inherited by all levels allowed by its inheritance flags, `InheritOnly` marks an entry that does not apply to the item it is defined on, and `NoPropagateInherit` marks an entry that is only inherited by the direct children of the folder. The default is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. ```yaml -Type: PropagationFlags +Type: Object Parameter Sets: PathComplex, SDComplex Aliases: Accepted values: None, NoPropagateInherit, InheritOnly diff --git a/Docs/Cmdlets/Remove-NTFSAudit.md b/Docs/Cmdlets/Remove-NTFSAudit.md index fe4d1af..154b4c4 100644 --- a/Docs/Cmdlets/Remove-NTFSAudit.md +++ b/Docs/Cmdlets/Remove-NTFSAudit.md @@ -16,28 +16,28 @@ Removes an audit entry from a file or folder. ### PathSimple ``` Remove-NTFSAudit [-Path] [-Account] [-AccessRights] - [-AuditFlags ] -AppliesTo [-RemoveSpecific] [-PassThru] [] + [-AuditFlags ] -AppliesTo [-RemoveSpecific] [-PassThru] [] ``` ### PathComplex ``` Remove-NTFSAudit [-Path] [-Account] [-AccessRights] - [-AuditFlags ] [-InheritanceFlags ] [-PropagationFlags ] - [-RemoveSpecific] [-PassThru] [] + [-AuditFlags ] [-InheritanceFlags ] [-PropagationFlags ] [-RemoveSpecific] [-PassThru] + [] ``` ### SDSimple ``` Remove-NTFSAudit [-SecurityDescriptor] [-Account] - [-AccessRights] [-AuditFlags ] -AppliesTo [-RemoveSpecific] - [-PassThru] [] + [-AccessRights] [-AuditFlags ] -AppliesTo [-RemoveSpecific] [-PassThru] + [] ``` ### SDComplex ``` Remove-NTFSAudit [-SecurityDescriptor] [-Account] - [-AccessRights] [-AuditFlags ] [-InheritanceFlags ] - [-PropagationFlags ] [-RemoveSpecific] [-PassThru] [] + [-AccessRights] [-AuditFlags ] [-InheritanceFlags ] + [-PropagationFlags ] [-RemoveSpecific] [-PassThru] [] ``` ## DESCRIPTION @@ -46,7 +46,7 @@ The `Remove-NTFSAudit` cmdlet removes an audit entry from the system access cont Because the inheritance and propagation flags take part in the match, they must describe the entry you want to remove. `-AppliesTo ThisFolderOnly` removes an entry that is not inherited by child items, which is also the shape of every audit entry on a file, while the default of the complex parameter sets removes an entry that applies to the folder, its subfolders, and its files. An entry that an item inherits from a parent folder is stored on that parent, so remove it there, or use `Clear-NTFSAudit` with `-DisableInheritance` to drop the inherited entries on the item. -In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path` and writes it back right away. In the `SDSimple` and `SDComplex` parameter sets it changes an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, and the change reaches the file system only when you pass the object to `Set-NTFSSecurityDescriptor`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo` that PowerShell can't convert to a scope, such as the blank cell of a CSV file or a misspelled name, is ignored, and the object is handled as one without `AppliesTo`: it uses its own `InheritanceFlags` and `PropagationFlags`, or the default scope if it has none. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. +In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path` and writes it back right away. In the `SDSimple` and `SDComplex` parameter sets it changes an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, and the change reaches the file system only when you pass the object to `Set-NTFSSecurityDescriptor`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a piped value was silently ignored, and the object was handled as one without it. A script block is a plain value for these parameters, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type; before 5.0.0, a script block was evaluated once for each piped object. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When you omit them, `-AuditFlags` is `Success, Failure`, `-InheritanceFlags` is `ContainerInherit, ObjectInherit`, `-PropagationFlags` is `None`. All parameters bind by property name, and `-Path` also binds by value and through its `FullName` alias, so you can pipe the output of `Get-NTFSAudit`, `Get-ChildItem`, `Get-ChildItem2`, and `Get-Item2` into the cmdlet. The cmdlet writes no object unless you use `-PassThru`. @@ -123,10 +123,10 @@ Accept wildcard characters: False ### -AppliesTo -Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. +Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ```yaml -Type: ApplyTo +Type: Object Parameter Sets: PathSimple, SDSimple Aliases: Accepted values: ThisFolderOnly, ThisFolderSubfoldersAndFiles, ThisFolderAndSubfolders, ThisFolderAndFiles, SubfoldersAndFilesOnly, SubfoldersOnly, FilesOnly, ThisFolderSubfoldersAndFilesOneLevel, ThisFolderAndSubfoldersOneLevel, ThisFolderAndFilesOneLevel, SubfoldersAndFilesOnlyOneLevel, SubfoldersOnlyOneLevel, FilesOnlyOneLevel @@ -140,10 +140,10 @@ Accept wildcard characters: False ### -AuditFlags -Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. +Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. ```yaml -Type: AuditFlags +Type: Object Parameter Sets: (All) Aliases: Accepted values: None, Success, Failure @@ -157,10 +157,10 @@ Accept wildcard characters: False ### -InheritanceFlags -Specifies the inheritance flags of the audit entry to remove. `ContainerInherit` addresses an entry that child folders inherit, `ObjectInherit` an entry that child files inherit, and `None` an entry that stays on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. The flags must match the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. +Specifies the inheritance flags of the audit entry to remove. `ContainerInherit` addresses an entry that child folders inherit, `ObjectInherit` an entry that child files inherit, and `None` an entry that stays on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. The flags must match the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. ```yaml -Type: InheritanceFlags +Type: Object Parameter Sets: PathComplex, SDComplex Aliases: Accepted values: None, ContainerInherit, ObjectInherit @@ -206,10 +206,10 @@ Accept wildcard characters: False ### -PropagationFlags -Specifies the propagation flags of the audit entry to remove. `None` addresses an entry that applies to the item itself and to all inheriting child items, `InheritOnly` an entry that applies to the child items only, and `NoPropagateInherit` an entry whose inheritance stops at the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. +Specifies the propagation flags of the audit entry to remove. `None` addresses an entry that applies to the item itself and to all inheriting child items, `InheritOnly` an entry that applies to the child items only, and `NoPropagateInherit` an entry whose inheritance stops at the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. ```yaml -Type: PropagationFlags +Type: Object Parameter Sets: PathComplex, SDComplex Aliases: Accepted values: None, NoPropagateInherit, InheritOnly diff --git a/Docs/Cmdlets/Set-NTFSSecurityDescriptor.md b/Docs/Cmdlets/Set-NTFSSecurityDescriptor.md index 1251fb2..bd0ccfc 100644 --- a/Docs/Cmdlets/Set-NTFSSecurityDescriptor.md +++ b/Docs/Cmdlets/Set-NTFSSecurityDescriptor.md @@ -95,7 +95,7 @@ Parameter Sets: (All) Aliases: Required: True -Position: 2 +Position: 1 Default value: None Accept pipeline input: True (ByPropertyName, ByValue) Accept wildcard characters: False diff --git a/Docs/Concepts.md b/Docs/Concepts.md index e63abbc..aeca9c3 100644 --- a/Docs/Concepts.md +++ b/Docs/Concepts.md @@ -274,6 +274,12 @@ change the module's behavior: cost extra work for every item. Turn them off to speed up large folder trees when you don't need the information. +A malformed `EnablePrivileges` setting, such as a string instead of +`$true` or `$false`, or a missing key, stops every cmdlet that uses +privileges, and `Enable-Privileges`, with the error +`EnablePrivilegesSettingInvalid` naming the setting, instead of an +unrelated exception. + To change a setting for the current session only, change the value after you import the module: diff --git a/NTFSSecurity/AccessCmdlets/AddAccess.cs b/NTFSSecurity/AccessCmdlets/AddAccess.cs index 5907a1a..d3f93c3 100644 --- a/NTFSSecurity/AccessCmdlets/AddAccess.cs +++ b/NTFSSecurity/AccessCmdlets/AddAccess.cs @@ -13,10 +13,10 @@ namespace NTFSSecurity { private IdentityReference2[] account; private FileSystemRights2 accessRights; - private AccessControlType accessType = AccessControlType.Allow; - private InheritanceFlags inheritanceFlags = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit; - private PropagationFlags propagationFlags = PropagationFlags.None; - private ApplyTo appliesTo = ApplyTo.ThisFolderSubfoldersAndFiles; + private object accessType; + private object inheritanceFlags; + private object propagationFlags; + private object appliesTo; private bool passThru; [Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "PathSimple")] @@ -65,7 +65,8 @@ namespace NTFSSecurity [Parameter(ValueFromPipelineByPropertyName = true)] [Alias("AccessControlType")] - public AccessControlType AccessType + [ArgumentCompleter(typeof(AccessControlTypeArgumentCompleter))] + public object AccessType { get { return accessType; } set { accessType = value; } @@ -73,7 +74,8 @@ namespace NTFSSecurity [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")] [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "SDComplex")] - public InheritanceFlags InheritanceFlags + [ArgumentCompleter(typeof(InheritanceFlagsArgumentCompleter))] + public object InheritanceFlags { get { return inheritanceFlags; } set { inheritanceFlags = value; } @@ -81,7 +83,8 @@ namespace NTFSSecurity [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")] [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "SDComplex")] - public PropagationFlags PropagationFlags + [ArgumentCompleter(typeof(PropagationFlagsArgumentCompleter))] + public object PropagationFlags { get { return propagationFlags; } set { propagationFlags = value; } @@ -90,7 +93,8 @@ namespace NTFSSecurity // Mandatory, so that a command without it resolves to the Complex parameter set, also for -SecurityDescriptor. [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "PathSimple")] [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "SDSimple")] - public ApplyTo AppliesTo + [ArgumentCompleter(typeof(ApplyToArgumentCompleter))] + public object AppliesTo { get { return appliesTo; } set { appliesTo = value; } @@ -105,19 +109,54 @@ namespace NTFSSecurity protected override void BeginProcessing() { + // Checked here, before privileges are enabled, because only a value that came from the command line is + // already bound at this point; a bad value from the pipeline is handled per record in ProcessRecord. + ValidateNamedValue(accessType, "AccessType"); + ValidateNamedValue(inheritanceFlags, "InheritanceFlags"); + ValidateNamedValue(propagationFlags, "PropagationFlags"); + ValidateNamedValue(appliesTo, "AppliesTo"); + base.BeginProcessing(); } protected override void ProcessRecord() { + AccessControlType accessTypeValue; + if (!TryConvertValue(accessType, "AccessType", AccessControlType.Allow, out accessTypeValue)) + { + return; + } + // The flags of this record are locals: the fields behind -InheritanceFlags and -PropagationFlags keep the value of the // previous piped object when the next one binds neither, so the scope that -AppliesTo converts to must not stay in them. - var inheritance = inheritanceFlags; - var propagation = propagationFlags; + var inheritance = System.Security.AccessControl.InheritanceFlags.ContainerInherit | System.Security.AccessControl.InheritanceFlags.ObjectInherit; + var propagation = System.Security.AccessControl.PropagationFlags.None; if (ParameterSetName.EndsWith("Simple")) { - FileSystemSecurity2.ConvertToFileSystemFlags(appliesTo, out inheritance, out propagation); + ApplyTo appliesToValue; + if (!TryConvertValue(appliesTo, "AppliesTo", default(ApplyTo), out appliesToValue)) + { + return; + } + + FileSystemSecurity2.ConvertToFileSystemFlags(appliesToValue, out inheritance, out propagation); + } + else + { + InheritanceFlags inheritanceValue; + if (!TryConvertValue(inheritanceFlags, "InheritanceFlags", inheritance, out inheritanceValue)) + { + return; + } + inheritance = inheritanceValue; + + PropagationFlags propagationValue; + if (!TryConvertValue(propagationFlags, "PropagationFlags", propagation, out propagationValue)) + { + return; + } + propagation = propagationValue; } if (ParameterSetName.StartsWith("Path")) @@ -138,7 +177,7 @@ namespace NTFSSecurity try { - FileSystemAccessRule2.AddFileSystemAccessRule(item, account.ToList(), accessRights, accessType, inheritance, propagation); + FileSystemAccessRule2.AddFileSystemAccessRule(item, account.ToList(), accessRights, accessTypeValue, inheritance, propagation); } catch (UnauthorizedAccessException) { @@ -146,11 +185,17 @@ namespace NTFSSecurity { InvokeAsOwner(item, path, () => { - FileSystemAccessRule2.AddFileSystemAccessRule(item, account.ToList(), accessRights, accessType, inheritance, propagation); + FileSystemAccessRule2.AddFileSystemAccessRule(item, account.ToList(), accessRights, accessTypeValue, inheritance, propagation); }); } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "AddAceError", ErrorCategory.WriteError, path)); continue; } @@ -171,7 +216,15 @@ namespace NTFSSecurity { foreach (var sd in securityDescriptors) { - FileSystemAccessRule2.AddFileSystemAccessRule(sd, account.ToList(), accessRights, accessType, inheritance, propagation); + try + { + FileSystemAccessRule2.AddFileSystemAccessRule(sd, account.ToList(), accessRights, accessTypeValue, inheritance, propagation); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "AddAceError", ErrorCategory.WriteError, sd)); + continue; + } if (passThru == true) { @@ -185,5 +238,77 @@ namespace NTFSSecurity { base.EndProcessing(); } + + // A bad value named on the command line is already bound when BeginProcessing runs, so it is reported there + // as a terminating error, the way the binder itself would have reported it for a typed parameter. A named + // $null is bound too, whether it came from a literal $null, a variable, the alias -AccessControlType, + // splatting, or $PSDefaultParameterValues (MyInvocation.BoundParameters always has the canonical name), and + // is an error the same way; a $null that a piped object's property carries is not bound yet at this point, + // so it keeps the parameter's own default instead, as before. + private void ValidateNamedValue(object value, string parameterName) + { + if (value == null) + { + if (!MyInvocation.BoundParameters.ContainsKey(parameterName)) + { + return; + } + + var nullException = new ArgumentException(ParameterConversion.DescribeNull(parameterName)); + ThrowTerminatingError(new ErrorRecord(nullException, "InvalidParameterValue", ErrorCategory.InvalidArgument, value)); + return; + } + + try + { + ParameterConversion.Convert(value, parameterName); + } + catch (ArgumentException ex) + { + ThrowTerminatingError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, value)); + } + } + + // A bad value that a piped object bound converts here, once per record. A null rawValue means the property + // was not bound for this record, so the parameter's own default applies. A bad value is reported as one + // non-terminating error per path or per descriptor, and the caller leaves the record alone and returns. + private bool TryConvertValue(object rawValue, string parameterName, T defaultValue, out T result) + { + if (rawValue == null) + { + result = defaultValue; + return true; + } + + try + { + result = ParameterConversion.Convert(rawValue, parameterName); + return true; + } + catch (ArgumentException ex) + { + result = defaultValue; + WriteInvalidParameterValueErrors(ex); + return false; + } + } + + private void WriteInvalidParameterValueErrors(Exception ex) + { + if (ParameterSetName.StartsWith("Path")) + { + foreach (var path in paths) + { + WriteError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, path)); + } + } + else + { + foreach (var sd in securityDescriptors) + { + WriteError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, sd)); + } + } + } } } diff --git a/NTFSSecurity/AccessCmdlets/ClearAccess.cs b/NTFSSecurity/AccessCmdlets/ClearAccess.cs index 1e4a1c6..28cb1bb 100644 --- a/NTFSSecurity/AccessCmdlets/ClearAccess.cs +++ b/NTFSSecurity/AccessCmdlets/ClearAccess.cs @@ -86,6 +86,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "ClearAclError", ErrorCategory.WriteError, path)); } } diff --git a/NTFSSecurity/AccessCmdlets/GetAccess.cs b/NTFSSecurity/AccessCmdlets/GetAccess.cs index 95af170..fc7e7ce 100644 --- a/NTFSSecurity/AccessCmdlets/GetAccess.cs +++ b/NTFSSecurity/AccessCmdlets/GetAccess.cs @@ -112,6 +112,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "ReadSecurityError", ErrorCategory.WriteError, path)); continue; } diff --git a/NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs b/NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs index 01ab384..5104efa 100644 --- a/NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs +++ b/NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs @@ -141,6 +141,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + this.WriteError(new ErrorRecord(ex2, "ReadSecurityError", ErrorCategory.WriteError, path)); continue; } diff --git a/NTFSSecurity/AccessCmdlets/GetOrphanedAccess.cs b/NTFSSecurity/AccessCmdlets/GetOrphanedAccess.cs index 14ecb9d..0ef9f49 100644 --- a/NTFSSecurity/AccessCmdlets/GetOrphanedAccess.cs +++ b/NTFSSecurity/AccessCmdlets/GetOrphanedAccess.cs @@ -55,6 +55,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + // A read error; before 5.0.0-rc6, it was reported as an AddAceError. this.WriteError(new ErrorRecord(ex2, "ReadSecurityError", ErrorCategory.ReadError, path)); continue; @@ -62,7 +68,9 @@ namespace NTFSSecurity } catch (Exception ex) { - this.WriteWarning(string.Format("Could not read item {0}. The error was: {1}", path, ex.Message)); + // A read failure that is not an access denial; before 5.0.0-rc9, the cmdlet warned about it + // instead ("Could not read item {0}. The error was: {1}") and returned no entry for the item. + this.WriteError(new ErrorRecord(ex, "ReadSecurityError", ErrorCategory.OpenError, path)); continue; } diff --git a/NTFSSecurity/AccessCmdlets/RemoveAccess.cs b/NTFSSecurity/AccessCmdlets/RemoveAccess.cs index 757ac61..cb24b15 100644 --- a/NTFSSecurity/AccessCmdlets/RemoveAccess.cs +++ b/NTFSSecurity/AccessCmdlets/RemoveAccess.cs @@ -13,10 +13,10 @@ namespace NTFSSecurity { private IdentityReference2[] account; private FileSystemRights2 accessRights; - private AccessControlType accessType = AccessControlType.Allow; - private InheritanceFlags inheritanceFlags = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit; - private PropagationFlags propagationFlags = PropagationFlags.None; - private ApplyTo appliesTo = ApplyTo.ThisFolderSubfoldersAndFiles; + private object accessType; + private object inheritanceFlags; + private object propagationFlags; + private object appliesTo; private bool removeSpecific; private bool passThru; @@ -66,7 +66,8 @@ namespace NTFSSecurity [Parameter(ValueFromPipelineByPropertyName = true)] [Alias("AccessControlType")] - public AccessControlType AccessType + [ArgumentCompleter(typeof(AccessControlTypeArgumentCompleter))] + public object AccessType { get { return accessType; } set { accessType = value; } @@ -74,7 +75,8 @@ namespace NTFSSecurity [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")] [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "SDComplex")] - public InheritanceFlags InheritanceFlags + [ArgumentCompleter(typeof(InheritanceFlagsArgumentCompleter))] + public object InheritanceFlags { get { return inheritanceFlags; } set { inheritanceFlags = value; } @@ -82,7 +84,8 @@ namespace NTFSSecurity [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")] [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "SDComplex")] - public PropagationFlags PropagationFlags + [ArgumentCompleter(typeof(PropagationFlagsArgumentCompleter))] + public object PropagationFlags { get { return propagationFlags; } set { propagationFlags = value; } @@ -91,7 +94,8 @@ namespace NTFSSecurity // Mandatory, so that a command without it resolves to the Complex parameter set, also for -SecurityDescriptor. [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "PathSimple")] [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "SDSimple")] - public ApplyTo AppliesTo + [ArgumentCompleter(typeof(ApplyToArgumentCompleter))] + public object AppliesTo { get { return appliesTo; } set { appliesTo = value; } @@ -116,19 +120,54 @@ namespace NTFSSecurity protected override void BeginProcessing() { + // Checked here, before privileges are enabled, because only a value that came from the command line is + // already bound at this point; a bad value from the pipeline is handled per record in ProcessRecord. + ValidateNamedValue(accessType, "AccessType"); + ValidateNamedValue(inheritanceFlags, "InheritanceFlags"); + ValidateNamedValue(propagationFlags, "PropagationFlags"); + ValidateNamedValue(appliesTo, "AppliesTo"); + base.BeginProcessing(); } protected override void ProcessRecord() { + AccessControlType accessTypeValue; + if (!TryConvertValue(accessType, "AccessType", AccessControlType.Allow, out accessTypeValue)) + { + return; + } + // The flags of this record are locals: the fields behind -InheritanceFlags and -PropagationFlags keep the value of the // previous piped object when the next one binds neither, so the scope that -AppliesTo converts to must not stay in them. - var inheritance = inheritanceFlags; - var propagation = propagationFlags; + var inheritance = System.Security.AccessControl.InheritanceFlags.ContainerInherit | System.Security.AccessControl.InheritanceFlags.ObjectInherit; + var propagation = System.Security.AccessControl.PropagationFlags.None; if (ParameterSetName.EndsWith("Simple")) { - FileSystemSecurity2.ConvertToFileSystemFlags(appliesTo, out inheritance, out propagation); + ApplyTo appliesToValue; + if (!TryConvertValue(appliesTo, "AppliesTo", default(ApplyTo), out appliesToValue)) + { + return; + } + + FileSystemSecurity2.ConvertToFileSystemFlags(appliesToValue, out inheritance, out propagation); + } + else + { + InheritanceFlags inheritanceValue; + if (!TryConvertValue(inheritanceFlags, "InheritanceFlags", inheritance, out inheritanceValue)) + { + return; + } + inheritance = inheritanceValue; + + PropagationFlags propagationValue; + if (!TryConvertValue(propagationFlags, "PropagationFlags", propagation, out propagationValue)) + { + return; + } + propagation = propagationValue; } if (ParameterSetName.StartsWith("Path")) @@ -149,7 +188,7 @@ namespace NTFSSecurity try { - FileSystemAccessRule2.RemoveFileSystemAccessRule(item, account.ToList(), accessRights, accessType, inheritance, propagation, removeSpecific); + FileSystemAccessRule2.RemoveFileSystemAccessRule(item, account.ToList(), accessRights, accessTypeValue, inheritance, propagation, removeSpecific); } catch (UnauthorizedAccessException) { @@ -157,11 +196,17 @@ namespace NTFSSecurity { InvokeAsOwner(item, path, () => { - FileSystemAccessRule2.RemoveFileSystemAccessRule(item, account.ToList(), accessRights, accessType, inheritance, propagation, removeSpecific); + FileSystemAccessRule2.RemoveFileSystemAccessRule(item, account.ToList(), accessRights, accessTypeValue, inheritance, propagation, removeSpecific); }); } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "RemoveAceError", ErrorCategory.WriteError, path)); continue; } @@ -182,7 +227,15 @@ namespace NTFSSecurity { foreach (var sd in securityDescriptors) { - FileSystemAccessRule2.RemoveFileSystemAccessRule(sd, account.ToList(), accessRights, accessType, inheritance, propagation, removeSpecific); + try + { + FileSystemAccessRule2.RemoveFileSystemAccessRule(sd, account.ToList(), accessRights, accessTypeValue, inheritance, propagation, removeSpecific); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "RemoveAceError", ErrorCategory.WriteError, sd)); + continue; + } if (passThru == true) { @@ -196,5 +249,77 @@ namespace NTFSSecurity { base.EndProcessing(); } + + // A bad value named on the command line is already bound when BeginProcessing runs, so it is reported there + // as a terminating error, the way the binder itself would have reported it for a typed parameter. A named + // $null is bound too, whether it came from a literal $null, a variable, the alias -AccessControlType, + // splatting, or $PSDefaultParameterValues (MyInvocation.BoundParameters always has the canonical name), and + // is an error the same way; a $null that a piped object's property carries is not bound yet at this point, + // so it keeps the parameter's own default instead, as before. + private void ValidateNamedValue(object value, string parameterName) + { + if (value == null) + { + if (!MyInvocation.BoundParameters.ContainsKey(parameterName)) + { + return; + } + + var nullException = new ArgumentException(ParameterConversion.DescribeNull(parameterName)); + ThrowTerminatingError(new ErrorRecord(nullException, "InvalidParameterValue", ErrorCategory.InvalidArgument, value)); + return; + } + + try + { + ParameterConversion.Convert(value, parameterName); + } + catch (ArgumentException ex) + { + ThrowTerminatingError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, value)); + } + } + + // A bad value that a piped object bound converts here, once per record. A null rawValue means the property + // was not bound for this record, so the parameter's own default applies. A bad value is reported as one + // non-terminating error per path or per descriptor, and the caller leaves the record alone and returns. + private bool TryConvertValue(object rawValue, string parameterName, T defaultValue, out T result) + { + if (rawValue == null) + { + result = defaultValue; + return true; + } + + try + { + result = ParameterConversion.Convert(rawValue, parameterName); + return true; + } + catch (ArgumentException ex) + { + result = defaultValue; + WriteInvalidParameterValueErrors(ex); + return false; + } + } + + private void WriteInvalidParameterValueErrors(Exception ex) + { + if (ParameterSetName.StartsWith("Path")) + { + foreach (var path in paths) + { + WriteError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, path)); + } + } + else + { + foreach (var sd in securityDescriptors) + { + WriteError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, sd)); + } + } + } } } \ No newline at end of file diff --git a/NTFSSecurity/AuditCmdlets/AddAudit.cs b/NTFSSecurity/AuditCmdlets/AddAudit.cs index e9ea655..d06f424 100644 --- a/NTFSSecurity/AuditCmdlets/AddAudit.cs +++ b/NTFSSecurity/AuditCmdlets/AddAudit.cs @@ -13,10 +13,10 @@ namespace NTFSSecurity { private IdentityReference2[] account; private FileSystemRights2 accessRights; - private AuditFlags auditFlags = AuditFlags.Failure | AuditFlags.Success; - private InheritanceFlags inheritanceFlags = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit; - private PropagationFlags propagationFlags = PropagationFlags.None; - private ApplyTo appliesTo = ApplyTo.ThisFolderSubfoldersAndFiles; + private object auditFlags; + private object inheritanceFlags; + private object propagationFlags; + private object appliesTo; private bool passThru; [Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "PathSimple")] @@ -64,7 +64,8 @@ namespace NTFSSecurity } [Parameter(ValueFromPipelineByPropertyName = true)] - public AuditFlags AuditFlags + [ArgumentCompleter(typeof(AuditFlagsArgumentCompleter))] + public object AuditFlags { get { return auditFlags; } set { auditFlags = value; } @@ -72,7 +73,8 @@ namespace NTFSSecurity [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")] [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "SDComplex")] - public InheritanceFlags InheritanceFlags + [ArgumentCompleter(typeof(InheritanceFlagsArgumentCompleter))] + public object InheritanceFlags { get { return inheritanceFlags; } set { inheritanceFlags = value; } @@ -80,7 +82,8 @@ namespace NTFSSecurity [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")] [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "SDComplex")] - public PropagationFlags PropagationFlags + [ArgumentCompleter(typeof(PropagationFlagsArgumentCompleter))] + public object PropagationFlags { get { return propagationFlags; } set { propagationFlags = value; } @@ -89,7 +92,8 @@ namespace NTFSSecurity // Mandatory, so that a command without it resolves to the Complex parameter set, also for -SecurityDescriptor. [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "PathSimple")] [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "SDSimple")] - public ApplyTo AppliesTo + [ArgumentCompleter(typeof(ApplyToArgumentCompleter))] + public object AppliesTo { get { return appliesTo; } set { appliesTo = value; } @@ -104,19 +108,54 @@ namespace NTFSSecurity protected override void BeginProcessing() { + // Checked here, before privileges are enabled, because only a value that came from the command line is + // already bound at this point; a bad value from the pipeline is handled per record in ProcessRecord. + ValidateNamedValue(auditFlags, "AuditFlags"); + ValidateNamedValue(inheritanceFlags, "InheritanceFlags"); + ValidateNamedValue(propagationFlags, "PropagationFlags"); + ValidateNamedValue(appliesTo, "AppliesTo"); + base.BeginProcessing(); } protected override void ProcessRecord() { + AuditFlags auditFlagsValue; + if (!TryConvertValue(auditFlags, "AuditFlags", System.Security.AccessControl.AuditFlags.Failure | System.Security.AccessControl.AuditFlags.Success, out auditFlagsValue)) + { + return; + } + // The flags of this record are locals: the fields behind -InheritanceFlags and -PropagationFlags keep the value of the // previous piped object when the next one binds neither, so the scope that -AppliesTo converts to must not stay in them. - var inheritance = inheritanceFlags; - var propagation = propagationFlags; + var inheritance = System.Security.AccessControl.InheritanceFlags.ContainerInherit | System.Security.AccessControl.InheritanceFlags.ObjectInherit; + var propagation = System.Security.AccessControl.PropagationFlags.None; if (ParameterSetName.EndsWith("Simple")) { - FileSystemSecurity2.ConvertToFileSystemFlags(appliesTo, out inheritance, out propagation); + ApplyTo appliesToValue; + if (!TryConvertValue(appliesTo, "AppliesTo", default(ApplyTo), out appliesToValue)) + { + return; + } + + FileSystemSecurity2.ConvertToFileSystemFlags(appliesToValue, out inheritance, out propagation); + } + else + { + InheritanceFlags inheritanceValue; + if (!TryConvertValue(inheritanceFlags, "InheritanceFlags", inheritance, out inheritanceValue)) + { + return; + } + inheritance = inheritanceValue; + + PropagationFlags propagationValue; + if (!TryConvertValue(propagationFlags, "PropagationFlags", propagation, out propagationValue)) + { + return; + } + propagation = propagationValue; } if (ParameterSetName.StartsWith("Path")) @@ -137,7 +176,7 @@ namespace NTFSSecurity try { - FileSystemAuditRule2.AddFileSystemAuditRule(item, account.ToList(), accessRights, auditFlags, inheritance, propagation); + FileSystemAuditRule2.AddFileSystemAuditRule(item, account.ToList(), accessRights, auditFlagsValue, inheritance, propagation); } catch (UnauthorizedAccessException) { @@ -145,11 +184,17 @@ namespace NTFSSecurity { InvokeAsOwner(item, path, () => { - FileSystemAuditRule2.AddFileSystemAuditRule(item, account.ToList(), accessRights, auditFlags, inheritance, propagation); + FileSystemAuditRule2.AddFileSystemAuditRule(item, account.ToList(), accessRights, auditFlagsValue, inheritance, propagation); }); } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "AddAceError", ErrorCategory.WriteError, path)); continue; } @@ -175,7 +220,15 @@ namespace NTFSSecurity continue; } - FileSystemAuditRule2.AddFileSystemAuditRule(sd, account.ToList(), accessRights, auditFlags, inheritance, propagation); + try + { + FileSystemAuditRule2.AddFileSystemAuditRule(sd, account.ToList(), accessRights, auditFlagsValue, inheritance, propagation); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "AddAceError", ErrorCategory.WriteError, sd)); + continue; + } if (passThru == true) { @@ -189,5 +242,77 @@ namespace NTFSSecurity { base.EndProcessing(); } + + // A bad value named on the command line is already bound when BeginProcessing runs, so it is reported there + // as a terminating error, the way the binder itself would have reported it for a typed parameter. A named + // $null is bound too, whether it came from a literal $null, a variable, the alias -AccessControlType, + // splatting, or $PSDefaultParameterValues (MyInvocation.BoundParameters always has the canonical name), and + // is an error the same way; a $null that a piped object's property carries is not bound yet at this point, + // so it keeps the parameter's own default instead, as before. + private void ValidateNamedValue(object value, string parameterName) + { + if (value == null) + { + if (!MyInvocation.BoundParameters.ContainsKey(parameterName)) + { + return; + } + + var nullException = new ArgumentException(ParameterConversion.DescribeNull(parameterName)); + ThrowTerminatingError(new ErrorRecord(nullException, "InvalidParameterValue", ErrorCategory.InvalidArgument, value)); + return; + } + + try + { + ParameterConversion.Convert(value, parameterName); + } + catch (ArgumentException ex) + { + ThrowTerminatingError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, value)); + } + } + + // A bad value that a piped object bound converts here, once per record. A null rawValue means the property + // was not bound for this record, so the parameter's own default applies. A bad value is reported as one + // non-terminating error per path or per descriptor, and the caller leaves the record alone and returns. + private bool TryConvertValue(object rawValue, string parameterName, T defaultValue, out T result) + { + if (rawValue == null) + { + result = defaultValue; + return true; + } + + try + { + result = ParameterConversion.Convert(rawValue, parameterName); + return true; + } + catch (ArgumentException ex) + { + result = defaultValue; + WriteInvalidParameterValueErrors(ex); + return false; + } + } + + private void WriteInvalidParameterValueErrors(Exception ex) + { + if (ParameterSetName.StartsWith("Path")) + { + foreach (var path in paths) + { + WriteError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, path)); + } + } + else + { + foreach (var sd in securityDescriptors) + { + WriteError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, sd)); + } + } + } } } \ No newline at end of file diff --git a/NTFSSecurity/AuditCmdlets/ClearAudit.cs b/NTFSSecurity/AuditCmdlets/ClearAudit.cs index 88aef1e..c5ce3ec 100644 --- a/NTFSSecurity/AuditCmdlets/ClearAudit.cs +++ b/NTFSSecurity/AuditCmdlets/ClearAudit.cs @@ -86,6 +86,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "ClearAclError", ErrorCategory.WriteError, path)); } } diff --git a/NTFSSecurity/AuditCmdlets/RemoveAudit.cs b/NTFSSecurity/AuditCmdlets/RemoveAudit.cs index 352b65d..8aefd04 100644 --- a/NTFSSecurity/AuditCmdlets/RemoveAudit.cs +++ b/NTFSSecurity/AuditCmdlets/RemoveAudit.cs @@ -13,10 +13,10 @@ namespace NTFSSecurity { private IdentityReference2[] account; private FileSystemRights2 accessRights; - private AuditFlags auditFlags = AuditFlags.Failure | AuditFlags.Success; - private InheritanceFlags inheritanceFlags = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit; - private PropagationFlags propagationFlags = PropagationFlags.None; - private ApplyTo appliesTo = ApplyTo.ThisFolderSubfoldersAndFiles; + private object auditFlags; + private object inheritanceFlags; + private object propagationFlags; + private object appliesTo; private bool removeSpecific; private bool passThru; @@ -65,7 +65,8 @@ namespace NTFSSecurity } [Parameter(ValueFromPipelineByPropertyName = true)] - public AuditFlags AuditFlags + [ArgumentCompleter(typeof(AuditFlagsArgumentCompleter))] + public object AuditFlags { get { return auditFlags; } set { auditFlags = value; } @@ -73,7 +74,8 @@ namespace NTFSSecurity [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")] [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "SDComplex")] - public InheritanceFlags InheritanceFlags + [ArgumentCompleter(typeof(InheritanceFlagsArgumentCompleter))] + public object InheritanceFlags { get { return inheritanceFlags; } set { inheritanceFlags = value; } @@ -81,7 +83,8 @@ namespace NTFSSecurity [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")] [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "SDComplex")] - public PropagationFlags PropagationFlags + [ArgumentCompleter(typeof(PropagationFlagsArgumentCompleter))] + public object PropagationFlags { get { return propagationFlags; } set { propagationFlags = value; } @@ -90,7 +93,8 @@ namespace NTFSSecurity // Mandatory, so that a command without it resolves to the Complex parameter set, also for -SecurityDescriptor. [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "PathSimple")] [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "SDSimple")] - public ApplyTo AppliesTo + [ArgumentCompleter(typeof(ApplyToArgumentCompleter))] + public object AppliesTo { get { return appliesTo; } set { appliesTo = value; } @@ -115,19 +119,54 @@ namespace NTFSSecurity protected override void BeginProcessing() { + // Checked here, before privileges are enabled, because only a value that came from the command line is + // already bound at this point; a bad value from the pipeline is handled per record in ProcessRecord. + ValidateNamedValue(auditFlags, "AuditFlags"); + ValidateNamedValue(inheritanceFlags, "InheritanceFlags"); + ValidateNamedValue(propagationFlags, "PropagationFlags"); + ValidateNamedValue(appliesTo, "AppliesTo"); + base.BeginProcessing(); } protected override void ProcessRecord() { + AuditFlags auditFlagsValue; + if (!TryConvertValue(auditFlags, "AuditFlags", System.Security.AccessControl.AuditFlags.Failure | System.Security.AccessControl.AuditFlags.Success, out auditFlagsValue)) + { + return; + } + // The flags of this record are locals: the fields behind -InheritanceFlags and -PropagationFlags keep the value of the // previous piped object when the next one binds neither, so the scope that -AppliesTo converts to must not stay in them. - var inheritance = inheritanceFlags; - var propagation = propagationFlags; + var inheritance = System.Security.AccessControl.InheritanceFlags.ContainerInherit | System.Security.AccessControl.InheritanceFlags.ObjectInherit; + var propagation = System.Security.AccessControl.PropagationFlags.None; if (ParameterSetName.EndsWith("Simple")) { - FileSystemSecurity2.ConvertToFileSystemFlags(appliesTo, out inheritance, out propagation); + ApplyTo appliesToValue; + if (!TryConvertValue(appliesTo, "AppliesTo", default(ApplyTo), out appliesToValue)) + { + return; + } + + FileSystemSecurity2.ConvertToFileSystemFlags(appliesToValue, out inheritance, out propagation); + } + else + { + InheritanceFlags inheritanceValue; + if (!TryConvertValue(inheritanceFlags, "InheritanceFlags", inheritance, out inheritanceValue)) + { + return; + } + inheritance = inheritanceValue; + + PropagationFlags propagationValue; + if (!TryConvertValue(propagationFlags, "PropagationFlags", propagation, out propagationValue)) + { + return; + } + propagation = propagationValue; } if (ParameterSetName.StartsWith("Path")) @@ -148,7 +187,7 @@ namespace NTFSSecurity try { - FileSystemAuditRule2.RemoveFileSystemAuditRule(item, account.ToList(), accessRights, auditFlags, inheritance, propagation, removeSpecific); + FileSystemAuditRule2.RemoveFileSystemAuditRule(item, account.ToList(), accessRights, auditFlagsValue, inheritance, propagation, removeSpecific); } catch (UnauthorizedAccessException) { @@ -156,11 +195,17 @@ namespace NTFSSecurity { InvokeAsOwner(item, path, () => { - FileSystemAuditRule2.RemoveFileSystemAuditRule(item, account.ToList(), accessRights, auditFlags, inheritance, propagation, removeSpecific); + FileSystemAuditRule2.RemoveFileSystemAuditRule(item, account.ToList(), accessRights, auditFlagsValue, inheritance, propagation, removeSpecific); }); } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "RemoveAceError", ErrorCategory.WriteError, path)); continue; } @@ -186,7 +231,15 @@ namespace NTFSSecurity continue; } - FileSystemAuditRule2.RemoveFileSystemAuditRule(sd, account.ToList(), accessRights, auditFlags, inheritance, propagation, removeSpecific); + try + { + FileSystemAuditRule2.RemoveFileSystemAuditRule(sd, account.ToList(), accessRights, auditFlagsValue, inheritance, propagation, removeSpecific); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "RemoveAceError", ErrorCategory.WriteError, sd)); + continue; + } if (passThru == true) { @@ -200,5 +253,77 @@ namespace NTFSSecurity { base.EndProcessing(); } + + // A bad value named on the command line is already bound when BeginProcessing runs, so it is reported there + // as a terminating error, the way the binder itself would have reported it for a typed parameter. A named + // $null is bound too, whether it came from a literal $null, a variable, the alias -AccessControlType, + // splatting, or $PSDefaultParameterValues (MyInvocation.BoundParameters always has the canonical name), and + // is an error the same way; a $null that a piped object's property carries is not bound yet at this point, + // so it keeps the parameter's own default instead, as before. + private void ValidateNamedValue(object value, string parameterName) + { + if (value == null) + { + if (!MyInvocation.BoundParameters.ContainsKey(parameterName)) + { + return; + } + + var nullException = new ArgumentException(ParameterConversion.DescribeNull(parameterName)); + ThrowTerminatingError(new ErrorRecord(nullException, "InvalidParameterValue", ErrorCategory.InvalidArgument, value)); + return; + } + + try + { + ParameterConversion.Convert(value, parameterName); + } + catch (ArgumentException ex) + { + ThrowTerminatingError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, value)); + } + } + + // A bad value that a piped object bound converts here, once per record. A null rawValue means the property + // was not bound for this record, so the parameter's own default applies. A bad value is reported as one + // non-terminating error per path or per descriptor, and the caller leaves the record alone and returns. + private bool TryConvertValue(object rawValue, string parameterName, T defaultValue, out T result) + { + if (rawValue == null) + { + result = defaultValue; + return true; + } + + try + { + result = ParameterConversion.Convert(rawValue, parameterName); + return true; + } + catch (ArgumentException ex) + { + result = defaultValue; + WriteInvalidParameterValueErrors(ex); + return false; + } + } + + private void WriteInvalidParameterValueErrors(Exception ex) + { + if (ParameterSetName.StartsWith("Path")) + { + foreach (var path in paths) + { + WriteError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, path)); + } + } + else + { + foreach (var sd in securityDescriptors) + { + WriteError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, sd)); + } + } + } } } \ No newline at end of file diff --git a/NTFSSecurity/BaseCmdlets.cs b/NTFSSecurity/BaseCmdlets.cs index 28a2164..528d315 100644 --- a/NTFSSecurity/BaseCmdlets.cs +++ b/NTFSSecurity/BaseCmdlets.cs @@ -354,7 +354,6 @@ namespace NTFSSecurity protected PrivilegeAndAttributesCollection privileges = null; protected PrivilegeControl privControl = new PrivilegeControl(); private List enabledPrivileges = new List(); - Hashtable privateData = null; // A cmdlet that enables the privileges for the session, such as Enable-Privileges, keeps them enabled. protected virtual bool KeepEnabledPrivileges @@ -364,9 +363,7 @@ namespace NTFSSecurity protected override void BeginProcessing() { - privateData = (Hashtable)MyInvocation.MyCommand.Module.PrivateData; - - if ((bool)privateData["EnablePrivileges"]) + if (GetEnablePrivilegesSetting()) { WriteVerbose("EnablePrivileges enabled in PrivateDate"); EnableFileSystemPrivileges(true); @@ -375,7 +372,7 @@ namespace NTFSSecurity protected override void EndProcessing() { - if ((bool)privateData["EnablePrivileges"]) + if (GetEnablePrivilegesSetting()) { WriteVerbose("EnablePrivileges enabled in PrivateDate"); @@ -395,6 +392,34 @@ namespace NTFSSecurity } } + /// + /// Reads the EnablePrivileges setting of the module manifest (NTFSSecurity.psd1, section PrivateData) and + /// terminates with one EnablePrivilegesSettingInvalid error when the setting is missing or is not a Boolean. + /// Before 5.0.0, BeginProcessing, EndProcessing, and Enable-Privileges each cast the setting on their own + /// without checking it first, so a missing key or a value that was not a Boolean, such as the string '$true' + /// from a typo in the manifest, failed with a NullReferenceException or an InvalidCastException before + /// anything useful was said, and Enable-Privileges raised a second, different message for the same cause + /// that was never reached, because BeginProcessing always failed first. + /// + protected bool GetEnablePrivilegesSetting() + { + var moduleData = (Hashtable)MyInvocation.MyCommand.Module.PrivateData; + var value = moduleData == null ? null : moduleData["EnablePrivileges"]; + + if (!(value is bool)) + { + var found = value == null ? "missing" : string.Format("'{0}'", value); + var message = string.Format( + "The 'EnablePrivileges' setting in the PrivateData section of the module manifest (NTFSSecurity.psd1) " + + "must be $true or $false, but it is {0}. See the NTFSSecurity documentation for EnablePrivileges.", + found); + + ThrowTerminatingError(new ErrorRecord(new ArgumentException(message), "EnablePrivilegesSettingInvalid", ErrorCategory.InvalidData, value)); + } + + return (bool)value; + } + // PowerShell calls Dispose also when a later command or a terminating error stops the pipeline, and then // skips EndProcessing. Before 5.0.0-rc6, the privileges that the cmdlet had enabled stayed enabled in the // session in that case. PowerShell ignores an exception from Dispose and no stream is open anymore, so a diff --git a/NTFSSecurity/EnumArgumentCompleters.cs b/NTFSSecurity/EnumArgumentCompleters.cs new file mode 100644 index 0000000..5b9a280 --- /dev/null +++ b/NTFSSecurity/EnumArgumentCompleters.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Management.Automation; +using System.Management.Automation.Language; +using System.Security.AccessControl; +using Security2; + +namespace NTFSSecurity +{ + // Completes the names of an enumeration for a parameter that is declared as object (Decision 27, fix B) and so + // does not get the completion that a parameter of an enumeration type gets from PowerShell itself. + internal static class EnumArgumentCompletion + { + /// + /// Returns the names of the enumeration that match the word the user already typed, compared without + /// regard to case, in the order Enum.GetNames returns them. A leading quote is stripped before matching, + /// together with the matching trailing quote that PowerShell adds to a word it completes while the quote + /// is still open, and every match is returned quoted the same way, so that completing inside an open quote + /// keeps the command line valid. The word itself may already contain a wildcard, such as "*Only" or "D*", + /// the same as PowerShell completes a parameter of an enumeration type. + /// + /// The enumeration to complete the names of. + /// The word the user already typed; empty or null completes every name. + internal static IEnumerable Complete(Type enumType, string wordToComplete) + { + var word = wordToComplete ?? string.Empty; + char? quote = null; + + if (word.Length > 0 && (word[0] == '\'' || word[0] == '"')) + { + quote = word[0]; + word = word.Substring(1); + + if (word.Length > 0 && word[word.Length - 1] == quote.Value) + { + word = word.Substring(0, word.Length - 1); + } + } + + var pattern = new WildcardPattern(word + "*", WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant); + var results = new List(); + + foreach (var name in Enum.GetNames(enumType)) + { + if (pattern.IsMatch(name)) + { + var completionText = quote.HasValue ? quote.Value + name + quote.Value : name; + results.Add(new CompletionResult(completionText, name, CompletionResultType.ParameterValue, name)); + } + } + + return results; + } + } + + /// Completes the names of System.Security.AccessControl.AccessControlType for -AccessType. + public class AccessControlTypeArgumentCompleter : IArgumentCompleter + { + public IEnumerable CompleteArgument( + string commandName, string parameterName, string wordToComplete, CommandAst commandAst, IDictionary fakeBoundParameters) + { + return EnumArgumentCompletion.Complete(typeof(AccessControlType), wordToComplete); + } + } + + /// Completes the names of System.Security.AccessControl.AuditFlags for -AuditFlags. + public class AuditFlagsArgumentCompleter : IArgumentCompleter + { + public IEnumerable CompleteArgument( + string commandName, string parameterName, string wordToComplete, CommandAst commandAst, IDictionary fakeBoundParameters) + { + return EnumArgumentCompletion.Complete(typeof(AuditFlags), wordToComplete); + } + } + + /// Completes the names of System.Security.AccessControl.InheritanceFlags for -InheritanceFlags. + public class InheritanceFlagsArgumentCompleter : IArgumentCompleter + { + public IEnumerable CompleteArgument( + string commandName, string parameterName, string wordToComplete, CommandAst commandAst, IDictionary fakeBoundParameters) + { + return EnumArgumentCompletion.Complete(typeof(InheritanceFlags), wordToComplete); + } + } + + /// Completes the names of System.Security.AccessControl.PropagationFlags for -PropagationFlags. + public class PropagationFlagsArgumentCompleter : IArgumentCompleter + { + public IEnumerable CompleteArgument( + string commandName, string parameterName, string wordToComplete, CommandAst commandAst, IDictionary fakeBoundParameters) + { + return EnumArgumentCompletion.Complete(typeof(PropagationFlags), wordToComplete); + } + } + + /// Completes the names of Security2.ApplyTo for -AppliesTo. + public class ApplyToArgumentCompleter : IArgumentCompleter + { + public IEnumerable CompleteArgument( + string commandName, string parameterName, string wordToComplete, CommandAst commandAst, IDictionary fakeBoundParameters) + { + return EnumArgumentCompletion.Complete(typeof(ApplyTo), wordToComplete); + } + } +} diff --git a/NTFSSecurity/InheritanceCmdlets/DisableAccessInheritance.cs b/NTFSSecurity/InheritanceCmdlets/DisableAccessInheritance.cs index bdb964d..56b3e89 100644 --- a/NTFSSecurity/InheritanceCmdlets/DisableAccessInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/DisableAccessInheritance.cs @@ -90,6 +90,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "ModifySdError", ErrorCategory.WriteError, path)); continue; } diff --git a/NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs b/NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs index 6505e1c..46c4cda 100644 --- a/NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs @@ -96,6 +96,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "ModifySdError", ErrorCategory.WriteError, path)); continue; } diff --git a/NTFSSecurity/InheritanceCmdlets/EnableAccessInheritance.cs b/NTFSSecurity/InheritanceCmdlets/EnableAccessInheritance.cs index 16ea497..1e6c7d7 100644 --- a/NTFSSecurity/InheritanceCmdlets/EnableAccessInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/EnableAccessInheritance.cs @@ -90,6 +90,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "ModifySdError", ErrorCategory.WriteError, path)); continue; } diff --git a/NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs b/NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs index 0c1a362..ce3d89e 100644 --- a/NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs @@ -95,6 +95,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "ModifySdError", ErrorCategory.WriteError, path)); continue; } diff --git a/NTFSSecurity/InheritanceCmdlets/GetInheritance.cs b/NTFSSecurity/InheritanceCmdlets/GetInheritance.cs index 9b02228..2a21f29 100644 --- a/NTFSSecurity/InheritanceCmdlets/GetInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/GetInheritance.cs @@ -80,6 +80,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "ReadSecurityError", ErrorCategory.WriteError, path)); continue; } @@ -89,12 +95,12 @@ namespace NTFSSecurity WriteError(new ErrorRecord(ex, "ReadSecurityError", ErrorCategory.OpenError, path)); continue; } - finally + + // After the try, not in a finally: what a later command raises while the retry restores the owner + // leaves the loop through the check above, and the object must not go to that command then. + if (inheritanceInfo != null) { - if (inheritanceInfo != null) - { - WriteObject(inheritanceInfo); - } + WriteObject(inheritanceInfo); } } } diff --git a/NTFSSecurity/InheritanceCmdlets/SetInheritance.cs b/NTFSSecurity/InheritanceCmdlets/SetInheritance.cs index 16ef6e6..893d202 100644 --- a/NTFSSecurity/InheritanceCmdlets/SetInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/SetInheritance.cs @@ -98,6 +98,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "ModifySdError", ErrorCategory.WriteError, path)); continue; } diff --git a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs index 66ad109..fd66987 100644 --- a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs +++ b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs @@ -26,8 +26,6 @@ namespace NTFSSecurity private bool identifyHardLinks = false; private int? depth; - WildcardPattern wildcard = null; - System.Reflection.MethodInfo modeMethodInfo = null; [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] @@ -46,6 +44,7 @@ namespace NTFSSecurity [Parameter(Position = 2)] [ValidateNotNull] + [ValidateLength(0, 32767)] public string Filter { get { return filter; } @@ -146,15 +145,9 @@ namespace NTFSSecurity paths = new List() { GetCurrentLocation() }; } - // Only * and ? are wildcards, like in the pattern that the enumeration matches; a bracket or a backtick stands - // for itself. Before 5.0.0, [1] was read as a character class, so a file with brackets in its name was not - // returned for its name. The enumeration returns every item for *.* as Windows does, so the comparison does - // too; with the dot as an ordinary character, it would drop the items without a dot, most folders. - // CultureInvariant: like the enumeration (see MoveNextInvariant), the comparison would otherwise treat I and i as - // different letters with tr-TR and drop items that the enumeration returned. - var pattern = filter == "*.*" ? "*" : filter; - wildcard = new WildcardPattern(pattern.Replace("`", "``").Replace("[", "`[").Replace("]", "`]"), WildcardOptions.Compiled | WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant); - + // The names are matched by DosWildcard, not by the enumeration (see WriteFileSystem, which always asks + // AlphaFS for "*"), so there is nothing to pre-compile here: DosWildcard reads the raw value of -Filter + // at the time each name is compared. modeMethodInfo = typeof(FileSystemCodeMembers).GetMethod("Mode"); getFileSystemModeProperty = (bool)((Hashtable)MyInvocation.MyCommand.Module.PrivateData)["GetFileSystemModeProperty"]; @@ -211,7 +204,10 @@ namespace NTFSSecurity IEnumerator files; using (new InvariantCultureScope()) { - files = di.EnumerateDirectories(filter, global::System.IO.SearchOption.TopDirectoryOnly).GetEnumerator(); + // The enumeration applies no pattern of its own; DosWildcard decides in + // WriteFileSystemInfoCollection, on the real name, with the dot rules of Windows + // PowerShell 5.1. + files = di.EnumerateDirectories("*", global::System.IO.SearchOption.TopDirectoryOnly).GetEnumerator(); } WriteFileSystemInfoCollection(files); @@ -221,7 +217,7 @@ namespace NTFSSecurity IEnumerator files; using (new InvariantCultureScope()) { - files = di.EnumerateFiles(filter, global::System.IO.SearchOption.TopDirectoryOnly).GetEnumerator(); + files = di.EnumerateFiles("*", global::System.IO.SearchOption.TopDirectoryOnly).GetEnumerator(); } WriteFileSystemInfoCollection(files); @@ -231,7 +227,7 @@ namespace NTFSSecurity IEnumerator files; using (new InvariantCultureScope()) { - files = di.EnumerateFileSystemInfos(filter, global::System.IO.SearchOption.TopDirectoryOnly).GetEnumerator(); + files = di.EnumerateFileSystemInfos("*", global::System.IO.SearchOption.TopDirectoryOnly).GetEnumerator(); } WriteFileSystemInfoCollection(files); @@ -304,7 +300,7 @@ namespace NTFSSecurity while (MoveNextInvariant(fileSystemInfos)) { FileSystemInfo current = (FileSystemInfo)fileSystemInfos.Current; - if (!wildcard.IsMatch(current.Name)) + if (!DosWildcard.IsMatch(current.Name, filter)) { continue; } @@ -401,5 +397,196 @@ namespace NTFSSecurity global::System.Threading.Thread.CurrentThread.CurrentCulture = previous; } } + + // Matches a -Filter pattern against the real name of an item with the Win32 "DOS" wildcard rules that + // Windows PowerShell 5.1 applies to Get-ChildItem -Filter, in both editions, so that the two editions and + // the short-name based matching that a past fix removed do not disagree. The rules come from + // FsRtlIsNameInExpression with the Win32 translation of a DOS pattern (DOS_STAR, DOS_QM, DOS_DOT): an + // asterisk matches a run of characters (but not past the LAST dot in the name, which a following dot + // token still has to account for); a question mark matches one character that is not a dot, or nothing + // where the name has no more characters left or the next one is a dot; and a dot matches a literal dot in + // the name, or nothing where the name has no more characters left, which is why "Report.*" also returns + // "Report" and "Report." returns only the names without a dot. Comparisons are case-insensitive with the + // invariant culture, never the thread culture, matching the enumeration (see InvariantCultureScope). + // Every character other than an asterisk or a question mark is ordinary, including a bracket and a + // backtick. + private static class DosWildcard + { + private enum TokenKind + { + Literal, + Star, + QuestionMark, + Dot + } + + private struct Token + { + public TokenKind Kind; + public char Char; + } + + public static bool IsMatch(string name, string pattern) + { + // An empty value matches every item, like a plain asterisk; Get-ChildItem2 keeps its own default + // of "*" for -Filter, but once a caller passes an explicit empty string this is the policy. + if (pattern.Length == 0) + { + return true; + } + + // A single dot is not a meaningful base/extension split, so Get-ChildItem treats it the same as + // no filter at all, in both editions. + if (pattern == ".") + { + return true; + } + + var tokens = Tokenize(pattern); + var matched = new Dictionary(); + return Match(name, 0, tokens, 0, matched); + } + + private static List Tokenize(string pattern) + { + var tokens = new List(); + + // A run of one or more dots that reaches the end of the pattern collapses into a single DOS_DOT: + // "Report..." and "Report." both ask for "Report" with nothing left of the name to account for. + // A dot elsewhere becomes a DOS_DOT only when the single next character is an asterisk or a + // question mark; otherwise it is an ordinary character that requires a literal dot in the name. + var trailingDotsFrom = pattern.Length; + while (trailingDotsFrom > 0 && pattern[trailingDotsFrom - 1] == '.') + { + trailingDotsFrom--; + } + var hasTrailingDotRun = trailingDotsFrom < pattern.Length; + + for (var i = 0; i < pattern.Length; i++) + { + if (hasTrailingDotRun && i == trailingDotsFrom) + { + tokens.Add(new Token { Kind = TokenKind.Dot, Char = '.' }); + break; + } + + var c = pattern[i]; + if (c == '*') + { + tokens.Add(new Token { Kind = TokenKind.Star }); + } + else if (c == '?') + { + tokens.Add(new Token { Kind = TokenKind.QuestionMark }); + } + else if (c == '.') + { + var next = (i + 1 < pattern.Length) ? pattern[i + 1] : '\0'; + var qualifies = (i + 1 >= pattern.Length) || next == '*' || next == '?'; + tokens.Add(new Token { Kind = qualifies ? TokenKind.Dot : TokenKind.Literal, Char = '.' }); + } + else + { + tokens.Add(new Token { Kind = TokenKind.Literal, Char = c }); + } + } + return tokens; + } + + // Matches the name, starting at name position ni, against the tokens from token position ti. The + // dictionary memoizes (ti, ni) pairs, because a star token tries every name position from ni onward + // and the same (ti, ni) pair can otherwise be re-examined many times over for a pattern with several + // stars. + private static bool Match(string name, int ni, List tokens, int ti, Dictionary matched) + { + var key = ((long)ti << 32) | (uint)ni; + bool cached; + if (matched.TryGetValue(key, out cached)) + { + return cached; + } + + var result = MatchCore(name, ni, tokens, ti, matched); + matched[key] = result; + return result; + } + + private static bool MatchCore(string name, int ni, List tokens, int ti, Dictionary matched) + { + if (ti == tokens.Count) + { + return ni == name.Length; + } + + var tok = tokens[ti]; + + if (tok.Kind == TokenKind.Star) + { + // A star that is directly followed by a dot (a DOS_DOT or an ordinary dot) must leave that dot + // for the dot token to account for: it may cross every dot in the name but the LAST one (Win32 + // DOS_STAR), which is why "Rep*." does not return "Report.txt" although a plain "*" could reach + // the end of the name by crossing the dot, and why "*.?" reaches the final segment of a name + // with two or more dots, such as "archive.tar.gz". A star with anything else after it, + // including another star, is free to cross every dot, which is why "**." returns every item + // and "R*t" returns "Report.txt". + var max = name.Length; + if (ti + 1 < tokens.Count && tokens[ti + 1].Kind == TokenKind.Dot) + { + var lastDot = name.LastIndexOf('.'); + if (lastDot >= ni) + { + max = lastDot; + } + } + + for (var k = ni; k <= max; k++) + { + if (Match(name, k, tokens, ti + 1, matched)) + { + return true; + } + } + return false; + } + + if (ni == name.Length) + { + // A question mark or a dot can match nothing at the end of the name; anything else needs a + // character that is not there. + if (tok.Kind == TokenKind.QuestionMark || tok.Kind == TokenKind.Dot) + { + return Match(name, ni, tokens, ti + 1, matched); + } + return false; + } + + if (tok.Kind == TokenKind.QuestionMark) + { + // A question mark does not match a dot; it matches nothing there instead, leaving the dot for + // a later token. + if (name[ni] == '.') + { + return Match(name, ni, tokens, ti + 1, matched); + } + return Match(name, ni + 1, tokens, ti + 1, matched); + } + + if (tok.Kind == TokenKind.Dot) + { + if (name[ni] == '.') + { + return Match(name, ni + 1, tokens, ti + 1, matched); + } + return false; + } + + if (char.ToUpperInvariant(tok.Char) == char.ToUpperInvariant(name[ni])) + { + return Match(name, ni + 1, tokens, ti + 1, matched); + } + + return false; + } + } } } diff --git a/NTFSSecurity/MiscCmdlets/GetFileHash2.cs b/NTFSSecurity/MiscCmdlets/GetFileHash2.cs index 74e07d9..667715e 100644 --- a/NTFSSecurity/MiscCmdlets/GetFileHash2.cs +++ b/NTFSSecurity/MiscCmdlets/GetFileHash2.cs @@ -103,6 +103,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "GetHashError", ErrorCategory.WriteError, path)); continue; } diff --git a/NTFSSecurity/NTFSSecurity.csproj b/NTFSSecurity/NTFSSecurity.csproj index bed8c19..d962e2f 100644 --- a/NTFSSecurity/NTFSSecurity.csproj +++ b/NTFSSecurity/NTFSSecurity.csproj @@ -73,6 +73,8 @@ + + diff --git a/NTFSSecurity/NTFSSecurity.psd1 b/NTFSSecurity/NTFSSecurity.psd1 index 4b32da8..0de98bc 100644 --- a/NTFSSecurity/NTFSSecurity.psd1 +++ b/NTFSSecurity/NTFSSecurity.psd1 @@ -102,7 +102,7 @@ ProjectUri = 'https://github.com/raandree/NTFSSecurity' ReleaseNotes = 'https://github.com/raandree/NTFSSecurity/blob/master/CHANGELOG.md' # Remove the prerelease label for the final release, see Docs/Contributing/05-Releasing.md - Prerelease = 'rc8' + Prerelease = 'rc9' } } } \ No newline at end of file diff --git a/NTFSSecurity/OtherCmdlets.cs b/NTFSSecurity/OtherCmdlets.cs index fd21efd..79f5c72 100644 --- a/NTFSSecurity/OtherCmdlets.cs +++ b/NTFSSecurity/OtherCmdlets.cs @@ -29,17 +29,12 @@ namespace NTFSSecurity protected override void ProcessRecord() { - var privateData = (System.Collections.Hashtable)this.MyInvocation.MyCommand.Module.PrivateData; var psCallStack = (CallStackFrame)this.InvokeCommand.InvokeScript("Get-PSCallStack")[1].BaseObject; - try - { - enablePrivileges = (bool)privateData["EnablePrivileges"]; - } - catch (Exception ex) - { - throw new ParseException("Could not parse the module's PrivateData field in the module's psd1 file. Please refer to the documentation for further details", ex); - } + // The base class raises one EnablePrivilegesSettingInvalid error for a setting that is not $true or + // $false; before 5.0.0, this had its own try/catch that raised a ParseException for the same cast, but + // it never ran, because BeginProcessing always failed first with its own uncaught cast. + enablePrivileges = GetEnablePrivilegesSetting(); //if the command is called from NTFSSecurity.Init.ps1 and EnablePrivileges is set to true in the NTFSSecurity.psd1 or if the cmdlet is called from somewhere else if ((psCallStack.InvocationInfo.MyCommand.Name == "NTFSSecurity.Init.ps1" && enablePrivileges == true)) diff --git a/NTFSSecurity/ParameterConversion.cs b/NTFSSecurity/ParameterConversion.cs new file mode 100644 index 0000000..56a2457 --- /dev/null +++ b/NTFSSecurity/ParameterConversion.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections; +using System.Management.Automation; + +namespace NTFSSecurity +{ + /// + /// Converts the raw value of a parameter that is declared as object back into the enumeration the parameter + /// describes, the way the PowerShell binder converts an argument for a typed parameter. AccessType, AuditFlags, + /// InheritanceFlags, PropagationFlags, and AppliesTo of the access and audit cmdlets are declared as object + /// (Decision 27, fix B), so that the binder does not silently skip a piped property it cannot convert and keep + /// the parameter at its default; this class gives the cmdlet the same conversion, with a message it can report + /// as its own error instead of a binding error. This class writes nothing; the cmdlet writes the error. + /// + internal static class ParameterConversion + { + /// + /// Converts the raw value of a parameter to T, the way the PowerShell binder converts an argument for a + /// parameter of that type: names in any case, a comma-separated list for a flags enumeration, a number, or + /// an enumeration instance. An enumeration that is not a flags enumeration takes one value: PowerShell + /// combines the numbers of a list for it bit by bit, so that one of the values wins silently or a value that + /// the caller did not name is chosen, so a list is rejected for it, in whatever kind of collection it comes. + /// A collection of one element converts as that element. + /// + /// + /// The raw value of the parameter. Not null: the caller keeps the default of the parameter for null instead + /// of calling this method, because null means that the parameter was not specified. + /// + /// The name of the parameter, without the leading dash, for the message. + /// The converted value. + /// + /// The value is blank (an empty or a white-space-only string, the usual blank CSV cell), is a list for an + /// enumeration that is not a flags enumeration, or the conversion failed. The message names the value, unless + /// it is blank, the parameter, and the valid names of T; the inner exception of a failed conversion is the + /// error that LanguagePrimitives.ConvertTo raised. + /// + internal static T Convert(object value, string parameterName) + { + var convertible = value; + + if (!typeof(T).IsDefined(typeof(FlagsAttribute), false)) + { + bool isList; + convertible = TakeOneValue(value, out isList); + if (isList) + { + throw new ArgumentException(string.Format( + "The value '{0}' for parameter -{1} is not valid: the parameter takes one value, not a list. {2}", + Describe(value), parameterName, DescribeValidValues(typeof(T)))); + } + } + + var text = convertible as string; + if (text != null && string.IsNullOrWhiteSpace(text)) + { + throw new ArgumentException(string.Format( + "The value for parameter -{0} is blank. {1}", parameterName, DescribeValidValues(typeof(T)))); + } + + try + { + return (T)LanguagePrimitives.ConvertTo(convertible, typeof(T)); + } + catch (Exception ex) + { + throw new ArgumentException( + string.Format("The value '{0}' for parameter -{1} is not valid. {2}", Describe(value), parameterName, DescribeValidValues(typeof(T))), + ex); + } + } + + /// + /// Builds the message for a parameter whose value is explicitly null although it was bound on the command + /// line (a literal $null, a variable that holds $null, or a null entry of $PSDefaultParameterValues): "The + /// value for parameter -AccessType is null. Valid values: Allow, Deny." A piped property that is null is + /// not an error there: it means the property was not specified and the parameter's own default applies; + /// only the cmdlet knows, from MyInvocation.BoundParameters, whether a null came from such a named value. + /// + /// The name of the parameter, without the leading dash, for the message. + internal static string DescribeNull(string parameterName) + { + return string.Format("The value for parameter -{0} is null. {1}", parameterName, DescribeValidValues(typeof(T))); + } + + // The most levels of nested collections that the conversion looks into, counting the argument itself: a value + // that is wrapped in more of them counts as a list. Enough for any real argument, and a stop for a collection + // that holds itself. + private const int MaxNesting = 32; + + // The one value that the conversion takes from the argument of an enumeration that is not a flags enumeration: + // the argument itself, or the only element of a collection of any kind (a wrapped PSObject counts as what it + // wraps, and a collection that holds a collection is opened too). isList is true when the argument holds more + // than one value: a string that holds a comma, a collection of several elements (two are enough to know, so + // a long or endless sequence is not read to its end), a collection of one element that holds such a list, or + // collections nested deeper than MaxNesting; the returned value is then not used. A dictionary, an empty + // collection, or a collection that cannot be enumerated is returned as it is, for the conversion to reject or + // convert with its own error. A failure to release the enumerator after the elements were read changes nothing. + private static object TakeOneValue(object value, out bool isList) + { + isList = false; + var current = value; + + for (var depth = 0; depth < MaxNesting; depth++) + { + var unwrapped = current is PSObject ? ((PSObject)current).BaseObject : current; + + var text = unwrapped as string; + if (text != null) + { + isList = text.IndexOf(',') >= 0; + return unwrapped; + } + + var collection = unwrapped as IEnumerable; + if (collection == null || unwrapped is IDictionary) + { + return unwrapped; + } + + var count = 0; + object only = null; + IEnumerator enumerator = null; + try + { + enumerator = collection.GetEnumerator(); + while (enumerator.MoveNext()) + { + count++; + if (count > 1) + { + break; + } + + only = enumerator.Current; + } + } + catch (Exception) + { + return value; + } + finally + { + Release(enumerator); + } + + if (count == 1) + { + current = only; + continue; + } + + isList = count > 1; + return value; + } + + isList = true; + return value; + } + + // Releasing the enumerator of a collection that was read can fail (only a custom enumerator does), and that does + // not change what was read. + private static void Release(IEnumerator enumerator) + { + var disposable = enumerator as IDisposable; + if (disposable == null) + { + return; + } + + try + { + disposable.Dispose(); + } + catch (Exception) + { + } + } + + // The value as a message names it: a collection of one element by that element (also when it is a collection + // itself), a collection of several elements by its first ten, anything else by its text. The message is built + // inside a catch block, so this method must not fail whatever the text or the enumeration of a value does; a + // value that cannot be formatted is named by its type, in the short form of Type.ToString. + private static string Describe(object value) + { + try + { + return DescribeValue(value); + } + catch (Exception) + { + var unwrapped = value is PSObject ? ((PSObject)value).BaseObject : value; + return unwrapped == null ? string.Empty : unwrapped.GetType().ToString(); + } + } + + private static string DescribeValue(object value) + { + var current = value; + + for (var depth = 0; depth < MaxNesting; depth++) + { + var unwrapped = current is PSObject ? ((PSObject)current).BaseObject : current; + var collection = unwrapped as IEnumerable; + + if (unwrapped is string || collection == null || unwrapped is IDictionary) + { + return string.Format("{0}", current); + } + + var shown = new System.Collections.Generic.List(); + var more = false; + foreach (var item in collection) + { + if (shown.Count == 10) + { + more = true; + break; + } + + shown.Add(item); + } + + if (shown.Count == 1 && !more) + { + current = shown[0]; + continue; + } + + if (shown.Count == 0) + { + return string.Format("{0}", current); + } + + return string.Join(", ", shown.ConvertAll(item => string.Format("{0}", item))) + (more ? ", ..." : string.Empty); + } + + return string.Format("{0}", value); + } + + // The valid names of the enumeration, in declaration order, with a note for a flags enumeration that a + // combination of them is written separated with commas. + private static string DescribeValidValues(Type enumType) + { + var names = string.Join(", ", Enum.GetNames(enumType)); + + if (enumType.IsDefined(typeof(FlagsAttribute), false)) + { + return string.Format("Valid values: {0}. Combine several with a comma.", names); + } + + return string.Format("Valid values: {0}.", names); + } + } +} diff --git a/NTFSSecurity/SecurityDescriptorCmdlets/SetSecurityDescriptor.cs b/NTFSSecurity/SecurityDescriptorCmdlets/SetSecurityDescriptor.cs index 86b4a83..3d1a456 100644 --- a/NTFSSecurity/SecurityDescriptorCmdlets/SetSecurityDescriptor.cs +++ b/NTFSSecurity/SecurityDescriptorCmdlets/SetSecurityDescriptor.cs @@ -11,7 +11,7 @@ namespace NTFSSecurity { private SwitchParameter passThru; - [Parameter(Mandatory = true, Position = 2, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] public FileSystemSecurity2[] SecurityDescriptor { get { return securityDescriptors.ToArray(); } @@ -61,6 +61,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "WriteSdError", ErrorCategory.WriteError, sd.Item)); continue; } diff --git a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml index 93948b7..7a17a77 100644 --- a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml +++ b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml @@ -12,7 +12,7 @@ Adds an access control entry (ACE) to the discretionary access control list (DACL) of a file or a folder. Every account in `-Account` receives the rights in `-AccessRights`, either as an `Allow` or as a `Deny` entry. `-AccessRights` accepts the basic rights such as `Read`, `Modify`, and `FullControl` as well as the granular rights such as `CreateFiles` or `WriteAttributes`, and several values can be combined, for example `-AccessRights ReadData, WriteData, Delete`. For the mapping between the values of this module, the rights that Windows displays, and the entries of the advanced security dialog, see Concepts (../Concepts.md). - The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`; both describe the same ACE flags. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo` that PowerShell can't convert to a scope, such as the blank cell of a CSV file or a misspelled name, is ignored, and the object is handled as one without `AppliesTo`: it uses its own `InheritanceFlags` and `PropagationFlags`, or the default scope if it has none. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. + The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`; both describe the same ACE flags. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a piped value was silently ignored, and the object was handled as one without it. A script block is a plain value for these parameters, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type; before 5.0.0, a script block was evaluated once for each piped object. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When `-AccessType`, `-AppliesTo`, `-InheritanceFlags`, and `-PropagationFlags` are omitted, the cmdlet adds an `Allow` ACE that applies to this folder, subfolders, and files, which corresponds to the inheritance flags `ContainerInherit, ObjectInherit` and no propagation flags. An `Allow` ACE always receives the `Synchronize` right in addition to the requested rights, inheritance and propagation flags are ignored on files, and rights for an account that already has an ACE with the same access type and the same flags are merged into that ACE. The cmdlet writes no output unless `-PassThru` is used, and a failure on one item is reported as a non-terminating error while the remaining items are processed. `-Path` accepts pipeline input by value and by property name through its alias `FullName`, so output of `Get-ChildItem`, `Get-ChildItem2`, and `Get-Item2` can be piped in. `-Account`, `-AccessRights`, `-AccessType`, `-InheritanceFlags`, and `-PropagationFlags` bind by property name as well, which lets you pipe `Security2.FileSystemAccessRule2` objects, or rows imported from a CSV file created from them, directly into the cmdlet. @@ -88,15 +88,15 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. Allow Deny - AccessControlType + Object - AccessControlType + Object Allow @@ -104,7 +104,7 @@ AppliesTo - Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. + Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -121,9 +121,9 @@ SubfoldersOnlyOneLevel FilesOnlyOneLevel - ApplyTo + Object - ApplyTo + Object None @@ -212,15 +212,15 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. Allow Deny - AccessControlType + Object - AccessControlType + Object Allow @@ -228,7 +228,7 @@ AppliesTo - Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. + Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -245,9 +245,9 @@ SubfoldersOnlyOneLevel FilesOnlyOneLevel - ApplyTo + Object - ApplyTo + Object None @@ -335,15 +335,15 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. Allow Deny - AccessControlType + Object - AccessControlType + Object Allow @@ -351,7 +351,7 @@ InheritanceFlags - Specifies which kind of child objects inherit the ACE. `ContainerInherit` passes the ACE on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the ACE on the item itself. The default is `ContainerInherit, ObjectInherit`. Inheritance flags have no effect on files, where the ACE is always created with `None`. + Specifies which kind of child objects inherit the ACE. `ContainerInherit` passes the ACE on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the ACE on the item itself. The default is `ContainerInherit, ObjectInherit`. Inheritance flags have no effect on files, where the ACE is always created with `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. For details about the flags, see the .NET documentation of the InheritanceFlags Enum (https://learn.microsoft.com/en-us/dotnet/api/system.security.accesscontrol.inheritanceflags). @@ -359,9 +359,9 @@ ContainerInherit ObjectInherit - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -380,16 +380,16 @@ PropagationFlags - Specifies how the ACE is propagated to child objects. `None` propagates the ACE to all levels that the inheritance flags allow, `InheritOnly` keeps the ACE from applying to the item it is defined on, and `NoPropagateInherit` limits inheritance to the direct children of the folder. The default is `None`, and propagation flags only have an effect in combination with `-InheritanceFlags`. + Specifies how the ACE is propagated to child objects. `None` propagates the ACE to all levels that the inheritance flags allow, `InheritOnly` keeps the ACE from applying to the item it is defined on, and `NoPropagateInherit` limits inheritance to the direct children of the folder. The default is `None`, and propagation flags only have an effect in combination with `-InheritanceFlags`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None NoPropagateInherit InheritOnly - PropagationFlags + Object - PropagationFlags + Object None @@ -467,15 +467,15 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. Allow Deny - AccessControlType + Object - AccessControlType + Object Allow @@ -483,7 +483,7 @@ InheritanceFlags - Specifies which kind of child objects inherit the ACE. `ContainerInherit` passes the ACE on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the ACE on the item itself. The default is `ContainerInherit, ObjectInherit`. Inheritance flags have no effect on files, where the ACE is always created with `None`. + Specifies which kind of child objects inherit the ACE. `ContainerInherit` passes the ACE on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the ACE on the item itself. The default is `ContainerInherit, ObjectInherit`. Inheritance flags have no effect on files, where the ACE is always created with `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. For details about the flags, see the .NET documentation of the InheritanceFlags Enum (https://learn.microsoft.com/en-us/dotnet/api/system.security.accesscontrol.inheritanceflags). @@ -491,9 +491,9 @@ ContainerInherit ObjectInherit - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -512,16 +512,16 @@ PropagationFlags - Specifies how the ACE is propagated to child objects. `None` propagates the ACE to all levels that the inheritance flags allow, `InheritOnly` keeps the ACE from applying to the item it is defined on, and `NoPropagateInherit` limits inheritance to the direct children of the folder. The default is `None`, and propagation flags only have an effect in combination with `-InheritanceFlags`. + Specifies how the ACE is propagated to child objects. `None` propagates the ACE to all levels that the inheritance flags allow, `InheritOnly` keeps the ACE from applying to the item it is defined on, and `NoPropagateInherit` limits inheritance to the direct children of the folder. The default is `None`, and propagation flags only have an effect in combination with `-InheritanceFlags`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None NoPropagateInherit InheritOnly - PropagationFlags + Object - PropagationFlags + Object None @@ -544,11 +544,11 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. - AccessControlType + Object - AccessControlType + Object Allow @@ -568,11 +568,11 @@ AppliesTo - Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. + Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. - ApplyTo + Object - ApplyTo + Object None @@ -580,12 +580,12 @@ InheritanceFlags - Specifies which kind of child objects inherit the ACE. `ContainerInherit` passes the ACE on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the ACE on the item itself. The default is `ContainerInherit, ObjectInherit`. Inheritance flags have no effect on files, where the ACE is always created with `None`. + Specifies which kind of child objects inherit the ACE. `ContainerInherit` passes the ACE on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the ACE on the item itself. The default is `ContainerInherit, ObjectInherit`. Inheritance flags have no effect on files, where the ACE is always created with `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. For details about the flags, see the .NET documentation of the InheritanceFlags Enum (https://learn.microsoft.com/en-us/dotnet/api/system.security.accesscontrol.inheritanceflags). - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -617,11 +617,11 @@ PropagationFlags - Specifies how the ACE is propagated to child objects. `None` propagates the ACE to all levels that the inheritance flags allow, `InheritOnly` keeps the ACE from applying to the item it is defined on, and `NoPropagateInherit` limits inheritance to the direct children of the folder. The default is `None`, and propagation flags only have an effect in combination with `-InheritanceFlags`. + Specifies how the ACE is propagated to child objects. `None` propagates the ACE to all levels that the inheritance flags allow, `InheritOnly` keeps the ACE from applying to the item it is defined on, and `NoPropagateInherit` limits inheritance to the direct children of the folder. The default is `None`, and propagation flags only have an effect in combination with `-InheritanceFlags`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. - PropagationFlags + Object - PropagationFlags + Object None @@ -796,7 +796,7 @@ The `Add-NTFSAudit` cmdlet adds an audit entry to the system access control list (SACL) of a file or folder. Windows then writes an event to the security log when the audited account uses one of the audited access rights on the item. `-AuditFlags Success` audits successful attempts, `-AuditFlags Failure` audits failed attempts, and the default audits both. For what the individual access rights permit, see Concepts (../Concepts.md). In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path`, adds the entry, and writes the descriptor back right away. In the `SDSimple` and `SDComplex` parameter sets it adds the entry to an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned; that change only reaches the file system when you pass the object to `Set-NTFSSecurityDescriptor`. The simple sets describe the scope of the entry with the single `-AppliesTo` parameter, the complex sets with `-InheritanceFlags` and `-PropagationFlags`. - A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo` that PowerShell can't convert to a scope, such as the blank cell of a CSV file or a misspelled name, is ignored, and the object is handled as one without `AppliesTo`: it uses its own `InheritanceFlags` and `PropagationFlags`, or the default scope if it has none. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. + A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a piped value was silently ignored, and the object was handled as one without it. A script block is a plain value for these parameters, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type; before 5.0.0, a script block was evaluated once for each piped object. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When you omit them, `-AuditFlags` is `Success, Failure`, `-InheritanceFlags` is `ContainerInherit, ObjectInherit`, `-PropagationFlags` is `None`, and `-AppliesTo` is `ThisFolderSubfoldersAndFiles`, so both the simple and the complex set audit the item, its subfolders, and its files by default. Inheritance applies to folders only: when the item is a file, the cmdlet stores the entry without inheritance and propagation flags. `-Path` accepts pipeline input by value and by property name through its `FullName` alias, so the output of `Get-ChildItem`, `Get-ChildItem2`, and `Get-Item2` binds to it, and the remaining parameters bind by property name. The cmdlet writes no object unless you use `-PassThru`. @@ -872,7 +872,7 @@ AppliesTo - Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. + Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -889,9 +889,9 @@ SubfoldersOnlyOneLevel FilesOnlyOneLevel - ApplyTo + Object - ApplyTo + Object None @@ -899,16 +899,16 @@ AuditFlags - Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. + Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None Success Failure - AuditFlags + Object - AuditFlags + Object Success, Failure @@ -996,7 +996,7 @@ AppliesTo - Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. + Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -1013,9 +1013,9 @@ SubfoldersOnlyOneLevel FilesOnlyOneLevel - ApplyTo + Object - ApplyTo + Object None @@ -1023,16 +1023,16 @@ AuditFlags - Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. + Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None Success Failure - AuditFlags + Object - AuditFlags + Object Success, Failure @@ -1120,16 +1120,16 @@ AuditFlags - Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. + Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None Success Failure - AuditFlags + Object - AuditFlags + Object Success, Failure @@ -1137,16 +1137,16 @@ InheritanceFlags - Specifies which child items inherit the audit entry. `ContainerInherit` passes the entry on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the entry on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. Use `-PropagationFlags` to control whether the entry also applies to the item itself and how far it propagates. + Specifies which child items inherit the audit entry. `ContainerInherit` passes the entry on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the entry on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. Use `-PropagationFlags` to control whether the entry also applies to the item itself and how far it propagates. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None ContainerInherit ObjectInherit - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -1165,16 +1165,16 @@ PropagationFlags - Specifies how the inheritance selected with `-InheritanceFlags` propagates. `None` applies the entry to the item itself and to all inheriting child items, `InheritOnly` applies it to the inheriting child items but not to the item itself, and `NoPropagateInherit` limits inheritance to the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. The parameter has no effect when `-InheritanceFlags` is `None`. + Specifies how the inheritance selected with `-InheritanceFlags` propagates. `None` applies the entry to the item itself and to all inheriting child items, `InheritOnly` applies it to the inheriting child items but not to the item itself, and `NoPropagateInherit` limits inheritance to the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. The parameter has no effect when `-InheritanceFlags` is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None NoPropagateInherit InheritOnly - PropagationFlags + Object - PropagationFlags + Object None @@ -1251,16 +1251,16 @@ AuditFlags - Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. + Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None Success Failure - AuditFlags + Object - AuditFlags + Object Success, Failure @@ -1268,16 +1268,16 @@ InheritanceFlags - Specifies which child items inherit the audit entry. `ContainerInherit` passes the entry on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the entry on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. Use `-PropagationFlags` to control whether the entry also applies to the item itself and how far it propagates. + Specifies which child items inherit the audit entry. `ContainerInherit` passes the entry on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the entry on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. Use `-PropagationFlags` to control whether the entry also applies to the item itself and how far it propagates. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None ContainerInherit ObjectInherit - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -1296,16 +1296,16 @@ PropagationFlags - Specifies how the inheritance selected with `-InheritanceFlags` propagates. `None` applies the entry to the item itself and to all inheriting child items, `InheritOnly` applies it to the inheriting child items but not to the item itself, and `NoPropagateInherit` limits inheritance to the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. The parameter has no effect when `-InheritanceFlags` is `None`. + Specifies how the inheritance selected with `-InheritanceFlags` propagates. `None` applies the entry to the item itself and to all inheriting child items, `InheritOnly` applies it to the inheriting child items but not to the item itself, and `NoPropagateInherit` limits inheritance to the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. The parameter has no effect when `-InheritanceFlags` is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None NoPropagateInherit InheritOnly - PropagationFlags + Object - PropagationFlags + Object None @@ -1340,11 +1340,11 @@ AppliesTo - Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. + Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. - ApplyTo + Object - ApplyTo + Object None @@ -1352,11 +1352,11 @@ AuditFlags - Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. + Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. - AuditFlags + Object - AuditFlags + Object Success, Failure @@ -1364,11 +1364,11 @@ InheritanceFlags - Specifies which child items inherit the audit entry. `ContainerInherit` passes the entry on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the entry on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. Use `-PropagationFlags` to control whether the entry also applies to the item itself and how far it propagates. + Specifies which child items inherit the audit entry. `ContainerInherit` passes the entry on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the entry on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. Use `-PropagationFlags` to control whether the entry also applies to the item itself and how far it propagates. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -1400,11 +1400,11 @@ PropagationFlags - Specifies how the inheritance selected with `-InheritanceFlags` propagates. `None` applies the entry to the item itself and to all inheriting child items, `InheritOnly` applies it to the inheriting child items but not to the item itself, and `NoPropagateInherit` limits inheritance to the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. The parameter has no effect when `-InheritanceFlags` is `None`. + Specifies how the inheritance selected with `-InheritanceFlags` propagates. `None` applies the entry to the item itself and to all inheriting child items, `InheritOnly` applies it to the inheriting child items but not to the item itself, and `NoPropagateInherit` limits inheritance to the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. The parameter has no effect when `-InheritanceFlags` is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. - PropagationFlags + Object - PropagationFlags + Object None @@ -3497,7 +3497,7 @@ PS C:\> Disable-Privileges The `Get-ChildItem2` cmdlet lists the files and folders in the folders that you specify with `-Path`. It returns an `Alphaleonis.Win32.Filesystem.FileInfo` object for every file and an `Alphaleonis.Win32.Filesystem.DirectoryInfo` object for every folder. The cmdlet is the long-path counterpart of the built-in `Get-ChildItem` cmdlet: it enumerates the file system through the AlphaFS library (`Alphaleonis.Win32.Filesystem`) instead of `System.IO`, so it also returns items whose path is longer than the 260-character `MAX_PATH` limit. - If you omit `-Path`, the cmdlet lists the current location. Relative paths and the `.` and `..` notations are resolved against the current location. Each path must name a folder, and wildcard characters are not supported. The parameter accepts pipeline input by value and by the property name `FullName`, so you can pipe folders from `Get-ChildItem2` or `Get-Item2` into another `Get-ChildItem2` call, and you can pipe the result into `Get-NTFSAccess` and the other NTFSSecurity cmdlets. + If you omit `-Path`, the cmdlet lists the current location. Relative paths and the `.` and `..` notations are resolved against the current location. Each path must name an existing folder or file, and wildcard characters are not supported. The parameter accepts pipeline input by value and by the property name `FullName`, so you can pipe folders from `Get-ChildItem2` or `Get-Item2` into another `Get-ChildItem2` call, and you can pipe the result into `Get-NTFSAccess` and the other NTFSSecurity cmdlets. By default the cmdlet returns the immediate content of each folder and omits hidden items. Use `-Recurse` to walk the whole tree, `-Depth` to limit how deep the recursion goes, `-Filter` to restrict the result by name, `-Directory` or `-File` to restrict it by item type, and `-Force`, `-Hidden`, `-System`, `-ReadOnly`, or `-Attributes` to restrict it by file attributes. Two settings in the `PrivateData` section of the module manifest change the objects that this cmdlet emits. `GetFileSystemModeProperty` adds the `Mode` property, which shows the directory, archive, read-only, hidden, and system attributes in `darhs` notation. `IdentifyHardLinks` adds a `HardLinkCount` property to every file object. Both are `$true` by default and are read once when the cmdlet starts. A folder that the cmdlet cannot read produces a non-terminating error, and the enumeration continues with the next folder. If a folder cannot be opened while `-Recurse` looks for subfolders, the cmdlet reports the problem as a verbose message instead, so run the command with the `-Verbose` common parameter if you need to know which branches were skipped. @@ -3520,7 +3520,12 @@ PS C:\> Disable-Privileges Filter - Specifies a name pattern that an item must match to be returned. The pattern supports the asterisk and the question mark as wildcard characters, an asterisk for any number of characters and a question mark for exactly one, and the match ignores case. Any other character stands for itself; a bracket is an ordinary character, so `Report[1].txt` returns the file of that name. As for `Get-ChildItem`, a pattern of an asterisk, a dot, and an asterisk returns every item, also an item without a dot in its name. The default value is `*`, which returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder. + Specifies a name pattern that an item must match to be returned. The pattern follows the wildcard rules that Windows PowerShell 5.1 applies to `Get-ChildItem -Filter`, in both editions, on the real name of the item, not its 8.3 short name. An asterisk stands for a run of any characters, and a question mark stands for one character that is not a dot, or for nothing where a dot or the end of the name follows. A dot stands for a literal dot, or for nothing where only wildcards or the end of the pattern follow. The match ignores case and is not affected by the culture of the session. Any other character stands for itself, including a bracket, so `Report[1].txt` returns the file of that name. + For example, `Report.*` also returns the item `Report`, which has no dot. + A pattern of an asterisk, a dot, and an asterisk returns every item, also an item without a dot in its name, as `Get-ChildItem` does for this pattern. + `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions, so the result does not depend on the edition. The built-in `Get-ChildItem -Filter` of PowerShell 7 differs for some patterns, for example for a pattern that ends in a dot, for which it returns nothing unless an asterisk comes right before the dot (the pattern of a single dot returns every item), and for a pattern in which an asterisk is followed by a dot and question marks, for which it ignores the dot and the question marks. + Two kinds of pattern are not like Windows PowerShell 5.1: blanks at the end of the pattern, also before a final dot, are part of it, where Windows PowerShell 5.1 drops them, and a pattern that ends in an even number of dots, such as two, is accepted, where Windows PowerShell 5.1 stops with an error. + The default value is `*`, which returns every item, and an empty value also returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder. String @@ -3724,7 +3729,12 @@ PS C:\> Disable-Privileges Filter - Specifies a name pattern that an item must match to be returned. The pattern supports the asterisk and the question mark as wildcard characters, an asterisk for any number of characters and a question mark for exactly one, and the match ignores case. Any other character stands for itself; a bracket is an ordinary character, so `Report[1].txt` returns the file of that name. As for `Get-ChildItem`, a pattern of an asterisk, a dot, and an asterisk returns every item, also an item without a dot in its name. The default value is `*`, which returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder. + Specifies a name pattern that an item must match to be returned. The pattern follows the wildcard rules that Windows PowerShell 5.1 applies to `Get-ChildItem -Filter`, in both editions, on the real name of the item, not its 8.3 short name. An asterisk stands for a run of any characters, and a question mark stands for one character that is not a dot, or for nothing where a dot or the end of the name follows. A dot stands for a literal dot, or for nothing where only wildcards or the end of the pattern follow. The match ignores case and is not affected by the culture of the session. Any other character stands for itself, including a bracket, so `Report[1].txt` returns the file of that name. + For example, `Report.*` also returns the item `Report`, which has no dot. + A pattern of an asterisk, a dot, and an asterisk returns every item, also an item without a dot in its name, as `Get-ChildItem` does for this pattern. + `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions, so the result does not depend on the edition. The built-in `Get-ChildItem -Filter` of PowerShell 7 differs for some patterns, for example for a pattern that ends in a dot, for which it returns nothing unless an asterisk comes right before the dot (the pattern of a single dot returns every item), and for a pattern in which an asterisk is followed by a dot and question marks, for which it ignores the dot and the question marks. + Two kinds of pattern are not like Windows PowerShell 5.1: blanks at the end of the pattern, also before a final dot, are part of it, where Windows PowerShell 5.1 drops them, and a pattern that ends in an even number of dots, such as two, is accepted, where Windows PowerShell 5.1 stops with an error. + The default value is `*`, which returns every item, and an empty value also returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder. String @@ -3867,7 +3877,9 @@ PS C:\> Disable-Privileges A folder that cannot be read produces a non-terminating error with the ID `DirUnauthorizedAccessError` for an access denial or `DirUnspecifiedError` for any other failure, and a path that does not exist produces the error `FileNotFound`. In each case the cmdlet continues with the next path. Failures that occur while `-Recurse` collects the subfolders of a folder are reported as verbose messages only, not as errors. Before 5.0.0, a `-Path` value that points to a file stopped the cmdlet with an `InvalidCastException`, `-Attributes` returned only the items that had all the listed attributes, and an empty `-Attributes` value returned every item, also the hidden ones. Earlier builds, including the 5.0.0 prereleases, could also omit the first hidden item with `-Hidden` unless `-Force` was explicitly supplied. Before 5.0.0, `-Filter` read a bracket as the start of a character class, so a file with brackets in its name, such as `Report[1].txt`, was not returned for its name, and a pattern of an asterisk, a dot, and an asterisk dropped the items without a dot in their names, most folders among them. - The names are matched twice, by the enumeration of the AlphaFS library and by the cmdlet, which reads a dot as an ordinary character, and their rules for a dot can differ from those of `Get-ChildItem`: a pattern such as `Report.*` does not return the file `Report`, which has no dot, a pattern that ends in a dot returns nothing, and an empty value returns nothing. Only the pattern of an asterisk, a dot, and an asterisk is treated as a single asterisk. + The cmdlet enumerates broadly and matches each name once, with the Win32 "DOS" wildcard rules that Windows PowerShell 5.1 applies to `Get-ChildItem -Filter`, in both editions: a pattern that ends in a dot returns the names without a dot, and an empty value returns every item, the same as a plain asterisk. + For example, `Report.*` also returns the item `Report`, which has no dot. A star in front of a dot token may cross every dot in the name but the last one, so a pattern such as a star followed by a dot also reaches the final segment of a name with two or more dots, for example `archive.tar.gz`. + PowerShell 7 itself differs from Windows PowerShell 5.1 here: its own `Get-ChildItem -Filter` returns nothing for a pattern that ends in a dot, unless the character right before the dot is an asterisk (the pattern of a single dot returns every item), and it ignores the dot and the question marks of a pattern in which an asterisk is followed by a dot and question marks. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead of the edition it runs in, so this is a deliberate difference from the built-in `Get-ChildItem` in PowerShell 7 for these and some similar patterns. Before 5.0.0, a `break`, a `continue`, or a `throw` in a later command of the pipeline did not end the cmdlet for an item below the first folder, also when the later command took the error of a folder that the cmdlet cannot read, for example with `2>&1`. @@ -5170,6 +5182,7 @@ PS C:\> Get-NTFSAudit -SecurityDescriptor $sd Before 5.0.0-rc7, the warning about a computer that couldn't be reached didn't name the computer, and the cmdlet warned for every name of this computer except `localhost` in lowercase, such as `.`, `LOCALHOST`, or the computer name. Before 5.0.0-rc7, a user who wasn't an administrator got the error "Access is denied" and no result on a computer in a domain, also for a name of this computer, such as the default `localhost`. Before 5.0.0-rc8, the cmdlet returned no access and no error when Windows refused to start the authorization manager for the computer of `-ServerName`, for example for a name of 32,718 characters or more, so that the result looked like an account without any rights. The cmdlet now writes a non-terminating `GetEffectiveAccessError` with the error that Windows reported and returns no result for the item, as it does for every other failure of the calculation. It warns that the result is based on this computer only when it calculated a result; before 5.0.0-rc8, the warning also came before an error when the computer couldn't be reached. + A user who has no domain credentials, such as a local standard user, still gets the error "Access is denied" from Windows when it asks for the effective access of a user account of the domain, also for a name of this computer; the cmdlet writes it as a non-terminating `GetEffectiveAccessError` and returns no result for that item. Accounts of this computer and groups such as `Everyone` or `Domain Users` give a result. @@ -5760,6 +5773,7 @@ PS C:\> Get-NTFSAudit -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. If the ACL of an item cannot be read because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. Reading the owner, which that fallback needs first, requires the same Read Permissions right as reading the ACL, so the cmdlet then writes a non-terminating `ReadSecurityError` and continues with the next item. Before 5.0.0, it reported that error as an `AddAceError`. + A read failure that is not an access denial is reported the same way, as a non-terminating `ReadSecurityError`, categorized as `OpenError` and targeting the path; before 5.0.0-rc9, the cmdlet wrote a warning instead (`Could not read item {0}. The error was: {1}`) and returned no entry for the item, without an error to say why. Before 5.0.0, the cmdlet ignored `-Account` and `-SecurityDescriptor`, and after a path whose ACL could not be read, it returned the orphaned entries of the previous item again. @@ -7724,7 +7738,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor Removes the rights in `-AccessRights` from the access control entries (ACEs) of a file or a folder. An entry is addressed by the account in `-Account`, the access type in `-AccessType`, and the inheritance and propagation flags, which are given either as `-AppliesTo` or as `-InheritanceFlags` and `-PropagationFlags`. Only the specified rights are taken away: when an entry grants more than `-AccessRights` names, the remaining rights stay in place, and the entry disappears only when all of its rights are removed. An `Allow` entry is always matched with the `Synchronize` right added to the specified rights. The flags must describe the entry as it exists on the item; when they do not, Windows splits the entry instead of removing the rights, so use the values that `Get-NTFSAccess` reports for the entry you want to change. With `-RemoveSpecific`, the cmdlet removes only an entry that matches exactly. Inherited entries cannot be removed from the item that inherits them. Remove them from the folder named in the `InheritedFrom` property, or run `Disable-NTFSAccessInheritance` on the item first, which copies the inherited entries into it as explicit ones that this cmdlet can then remove. - The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo` that PowerShell can't convert to a scope, such as the blank cell of a CSV file or a misspelled name, is ignored, and the object is handled as one without `AppliesTo`: it uses its own `InheritanceFlags` and `PropagationFlags`, or the default scope if it has none. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. All relevant parameters bind by property name, so the output of `Get-NTFSAccess` and `Get-NTFSOrphanedAccess` can be piped directly into this cmdlet. The cmdlet writes no output unless `-PassThru` is used. + The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a piped value was silently ignored, and the object was handled as one without it. A script block is a plain value for these parameters, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type; before 5.0.0, a script block was evaluated once for each piped object. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. All relevant parameters bind by property name, so the output of `Get-NTFSAccess` and `Get-NTFSOrphanedAccess` can be piped directly into this cmdlet. The cmdlet writes no output unless `-PassThru` is used. @@ -7798,15 +7812,15 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. Allow Deny - AccessControlType + Object - AccessControlType + Object Allow @@ -7814,7 +7828,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. + Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -7831,9 +7845,9 @@ PS C:\Data> Get-NTFSSecurityDescriptor SubfoldersOnlyOneLevel FilesOnlyOneLevel - ApplyTo + Object - ApplyTo + Object None @@ -7933,15 +7947,15 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. Allow Deny - AccessControlType + Object - AccessControlType + Object Allow @@ -7949,7 +7963,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. + Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -7966,9 +7980,9 @@ PS C:\Data> Get-NTFSSecurityDescriptor SubfoldersOnlyOneLevel FilesOnlyOneLevel - ApplyTo + Object - ApplyTo + Object None @@ -8067,15 +8081,15 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. Allow Deny - AccessControlType + Object - AccessControlType + Object Allow @@ -8083,16 +8097,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor InheritanceFlags - Specifies the inheritance flags of the entry that is addressed. `ContainerInherit` marks an entry that child folders inherit, `ObjectInherit` marks an entry that child files inherit, and `None` marks an entry that is not inherited at all. The default is `ContainerInherit, ObjectInherit`, which is the scope `ThisFolderSubfoldersAndFiles`. Entries on files always carry `None`. + Specifies the inheritance flags of the entry that is addressed. `ContainerInherit` marks an entry that child folders inherit, `ObjectInherit` marks an entry that child files inherit, and `None` marks an entry that is not inherited at all. The default is `ContainerInherit, ObjectInherit`, which is the scope `ThisFolderSubfoldersAndFiles`. Entries on files always carry `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None ContainerInherit ObjectInherit - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -8111,16 +8125,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor PropagationFlags - Specifies the propagation flags of the entry that is addressed. `None` marks an entry that is inherited by all levels allowed by its inheritance flags, `InheritOnly` marks an entry that does not apply to the item it is defined on, and `NoPropagateInherit` marks an entry that is only inherited by the direct children of the folder. The default is `None`. + Specifies the propagation flags of the entry that is addressed. `None` marks an entry that is inherited by all levels allowed by its inheritance flags, `InheritOnly` marks an entry that does not apply to the item it is defined on, and `NoPropagateInherit` marks an entry that is only inherited by the direct children of the folder. The default is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None NoPropagateInherit InheritOnly - PropagationFlags + Object - PropagationFlags + Object None @@ -8209,15 +8223,15 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. Allow Deny - AccessControlType + Object - AccessControlType + Object Allow @@ -8225,16 +8239,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor InheritanceFlags - Specifies the inheritance flags of the entry that is addressed. `ContainerInherit` marks an entry that child folders inherit, `ObjectInherit` marks an entry that child files inherit, and `None` marks an entry that is not inherited at all. The default is `ContainerInherit, ObjectInherit`, which is the scope `ThisFolderSubfoldersAndFiles`. Entries on files always carry `None`. + Specifies the inheritance flags of the entry that is addressed. `ContainerInherit` marks an entry that child folders inherit, `ObjectInherit` marks an entry that child files inherit, and `None` marks an entry that is not inherited at all. The default is `ContainerInherit, ObjectInherit`, which is the scope `ThisFolderSubfoldersAndFiles`. Entries on files always carry `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None ContainerInherit ObjectInherit - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -8253,16 +8267,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor PropagationFlags - Specifies the propagation flags of the entry that is addressed. `None` marks an entry that is inherited by all levels allowed by its inheritance flags, `InheritOnly` marks an entry that does not apply to the item it is defined on, and `NoPropagateInherit` marks an entry that is only inherited by the direct children of the folder. The default is `None`. + Specifies the propagation flags of the entry that is addressed. `None` marks an entry that is inherited by all levels allowed by its inheritance flags, `InheritOnly` marks an entry that does not apply to the item it is defined on, and `NoPropagateInherit` marks an entry that is only inherited by the direct children of the folder. The default is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None NoPropagateInherit InheritOnly - PropagationFlags + Object - PropagationFlags + Object None @@ -8296,11 +8310,11 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. - AccessControlType + Object - AccessControlType + Object Allow @@ -8320,11 +8334,11 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. + Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. - ApplyTo + Object - ApplyTo + Object None @@ -8332,11 +8346,11 @@ PS C:\Data> Get-NTFSSecurityDescriptor InheritanceFlags - Specifies the inheritance flags of the entry that is addressed. `ContainerInherit` marks an entry that child folders inherit, `ObjectInherit` marks an entry that child files inherit, and `None` marks an entry that is not inherited at all. The default is `ContainerInherit, ObjectInherit`, which is the scope `ThisFolderSubfoldersAndFiles`. Entries on files always carry `None`. + Specifies the inheritance flags of the entry that is addressed. `ContainerInherit` marks an entry that child folders inherit, `ObjectInherit` marks an entry that child files inherit, and `None` marks an entry that is not inherited at all. The default is `ContainerInherit, ObjectInherit`, which is the scope `ThisFolderSubfoldersAndFiles`. Entries on files always carry `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -8368,11 +8382,11 @@ PS C:\Data> Get-NTFSSecurityDescriptor PropagationFlags - Specifies the propagation flags of the entry that is addressed. `None` marks an entry that is inherited by all levels allowed by its inheritance flags, `InheritOnly` marks an entry that does not apply to the item it is defined on, and `NoPropagateInherit` marks an entry that is only inherited by the direct children of the folder. The default is `None`. + Specifies the propagation flags of the entry that is addressed. `None` marks an entry that is inherited by all levels allowed by its inheritance flags, `InheritOnly` marks an entry that does not apply to the item it is defined on, and `NoPropagateInherit` marks an entry that is only inherited by the direct children of the folder. The default is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. - PropagationFlags + Object - PropagationFlags + Object None @@ -8563,7 +8577,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor The `Remove-NTFSAudit` cmdlet removes an audit entry from the system access control list (SACL) of a file or folder. The cmdlet builds an audit entry from `-Account`, `-AccessRights`, `-AuditFlags`, and the inheritance and propagation flags, and removes that entry from the SACL. The audit entries of the account are matched by their inheritance and propagation flags, and the requested access rights and audit flags are then taken away from them: an entry that audits further rights keeps those rights and disappears only when nothing is left. To remove an entry completely, pass the same values that `Get-NTFSAudit` reports for it. With `-RemoveSpecific`, the cmdlet removes only an entry that matches exactly. Because the inheritance and propagation flags take part in the match, they must describe the entry you want to remove. `-AppliesTo ThisFolderOnly` removes an entry that is not inherited by child items, which is also the shape of every audit entry on a file, while the default of the complex parameter sets removes an entry that applies to the folder, its subfolders, and its files. An entry that an item inherits from a parent folder is stored on that parent, so remove it there, or use `Clear-NTFSAudit` with `-DisableInheritance` to drop the inherited entries on the item. - In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path` and writes it back right away. In the `SDSimple` and `SDComplex` parameter sets it changes an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, and the change reaches the file system only when you pass the object to `Set-NTFSSecurityDescriptor`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo` that PowerShell can't convert to a scope, such as the blank cell of a CSV file or a misspelled name, is ignored, and the object is handled as one without `AppliesTo`: it uses its own `InheritanceFlags` and `PropagationFlags`, or the default scope if it has none. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. + In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path` and writes it back right away. In the `SDSimple` and `SDComplex` parameter sets it changes an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, and the change reaches the file system only when you pass the object to `Set-NTFSSecurityDescriptor`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a piped value was silently ignored, and the object was handled as one without it. A script block is a plain value for these parameters, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type; before 5.0.0, a script block was evaluated once for each piped object. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When you omit them, `-AuditFlags` is `Success, Failure`, `-InheritanceFlags` is `ContainerInherit, ObjectInherit`, `-PropagationFlags` is `None`. All parameters bind by property name, and `-Path` also binds by value and through its `FullName` alias, so you can pipe the output of `Get-NTFSAudit`, `Get-ChildItem`, `Get-ChildItem2`, and `Get-Item2` into the cmdlet. The cmdlet writes no object unless you use `-PassThru`. @@ -8638,7 +8652,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. + Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -8655,9 +8669,9 @@ PS C:\Data> Get-NTFSSecurityDescriptor SubfoldersOnlyOneLevel FilesOnlyOneLevel - ApplyTo + Object - ApplyTo + Object None @@ -8665,16 +8679,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor AuditFlags - Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. + Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None Success Failure - AuditFlags + Object - AuditFlags + Object Success, Failure @@ -8773,7 +8787,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. + Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -8790,9 +8804,9 @@ PS C:\Data> Get-NTFSSecurityDescriptor SubfoldersOnlyOneLevel FilesOnlyOneLevel - ApplyTo + Object - ApplyTo + Object None @@ -8800,16 +8814,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor AuditFlags - Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. + Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None Success Failure - AuditFlags + Object - AuditFlags + Object Success, Failure @@ -8908,16 +8922,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor AuditFlags - Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. + Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None Success Failure - AuditFlags + Object - AuditFlags + Object Success, Failure @@ -8925,16 +8939,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor InheritanceFlags - Specifies the inheritance flags of the audit entry to remove. `ContainerInherit` addresses an entry that child folders inherit, `ObjectInherit` an entry that child files inherit, and `None` an entry that stays on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. The flags must match the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. + Specifies the inheritance flags of the audit entry to remove. `ContainerInherit` addresses an entry that child folders inherit, `ObjectInherit` an entry that child files inherit, and `None` an entry that stays on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. The flags must match the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None ContainerInherit ObjectInherit - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -8953,16 +8967,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor PropagationFlags - Specifies the propagation flags of the audit entry to remove. `None` addresses an entry that applies to the item itself and to all inheriting child items, `InheritOnly` an entry that applies to the child items only, and `NoPropagateInherit` an entry whose inheritance stops at the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. + Specifies the propagation flags of the audit entry to remove. `None` addresses an entry that applies to the item itself and to all inheriting child items, `InheritOnly` an entry that applies to the child items only, and `NoPropagateInherit` an entry whose inheritance stops at the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None NoPropagateInherit InheritOnly - PropagationFlags + Object - PropagationFlags + Object None @@ -9050,16 +9064,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor AuditFlags - Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. + Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None Success Failure - AuditFlags + Object - AuditFlags + Object Success, Failure @@ -9067,16 +9081,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor InheritanceFlags - Specifies the inheritance flags of the audit entry to remove. `ContainerInherit` addresses an entry that child folders inherit, `ObjectInherit` an entry that child files inherit, and `None` an entry that stays on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. The flags must match the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. + Specifies the inheritance flags of the audit entry to remove. `ContainerInherit` addresses an entry that child folders inherit, `ObjectInherit` an entry that child files inherit, and `None` an entry that stays on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. The flags must match the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None ContainerInherit ObjectInherit - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -9095,16 +9109,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor PropagationFlags - Specifies the propagation flags of the audit entry to remove. `None` addresses an entry that applies to the item itself and to all inheriting child items, `InheritOnly` an entry that applies to the child items only, and `NoPropagateInherit` an entry whose inheritance stops at the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. + Specifies the propagation flags of the audit entry to remove. `None` addresses an entry that applies to the item itself and to all inheriting child items, `InheritOnly` an entry that applies to the child items only, and `NoPropagateInherit` an entry whose inheritance stops at the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None NoPropagateInherit InheritOnly - PropagationFlags + Object - PropagationFlags + Object None @@ -9150,11 +9164,11 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. + Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. - ApplyTo + Object - ApplyTo + Object None @@ -9162,11 +9176,11 @@ PS C:\Data> Get-NTFSSecurityDescriptor AuditFlags - Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. + Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. - AuditFlags + Object - AuditFlags + Object Success, Failure @@ -9174,11 +9188,11 @@ PS C:\Data> Get-NTFSSecurityDescriptor InheritanceFlags - Specifies the inheritance flags of the audit entry to remove. `ContainerInherit` addresses an entry that child folders inherit, `ObjectInherit` an entry that child files inherit, and `None` an entry that stays on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. The flags must match the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. + Specifies the inheritance flags of the audit entry to remove. `ContainerInherit` addresses an entry that child folders inherit, `ObjectInherit` an entry that child files inherit, and `None` an entry that stays on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. The flags must match the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -9210,11 +9224,11 @@ PS C:\Data> Get-NTFSSecurityDescriptor PropagationFlags - Specifies the propagation flags of the audit entry to remove. `None` addresses an entry that applies to the item itself and to all inheriting child items, `InheritOnly` an entry that applies to the child items only, and `NoPropagateInherit` an entry whose inheritance stops at the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. + Specifies the propagation flags of the audit entry to remove. `None` addresses an entry that applies to the item itself and to all inheriting child items, `InheritOnly` an entry that applies to the child items only, and `NoPropagateInherit` an entry whose inheritance stops at the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. - PropagationFlags + Object - PropagationFlags + Object None @@ -9949,7 +9963,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd Set-NTFSSecurityDescriptor - + SecurityDescriptor Specifies one or more security descriptors that `Get-NTFSSecurityDescriptor` returned. Each descriptor is written to the file or folder it was read from, including every change that was made to it in memory. @@ -9987,7 +10001,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd False - + SecurityDescriptor Specifies one or more security descriptors that `Get-NTFSSecurityDescriptor` returned. Each descriptor is written to the file or folder it was read from, including every change that was made to it in memory. diff --git a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.AddFileSystemAccessRules.cs b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.AddFileSystemAccessRules.cs index cd8f067..d5ba957 100644 --- a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.AddFileSystemAccessRules.cs +++ b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.AddFileSystemAccessRules.cs @@ -87,17 +87,27 @@ namespace Security2 return ace; } + /// + /// Adds one access entry per account to the item at the path, and returns them in the order of + /// . Before 5.0.0, this overload was an iterator (a yield return in the loop + /// below), so nothing happened until the caller enumerated the result; called as a statement, or assigned to + /// a variable that is never read, it changed nothing. It now builds the list and writes the entries before + /// returning, like its FileSystemInfo and FileSystemSecurity2 siblings; an exception that adding an entry + /// raises is now thrown by this call, not by the later enumeration. + /// public static IEnumerable AddFileSystemAccessRule(string path, List accounts, FileSystemRights2 rights, AccessControlType type, InheritanceFlags inheritanceFlags, PropagationFlags propagationFlags) { if (type == AccessControlType.Allow) rights = rights | FileSystemRights2.Synchronize; + var aces = new List(); + if (File.Exists(path)) { var item = new FileInfo(path); foreach (var account in accounts) { - yield return AddFileSystemAccessRule(item, account, rights, type, inheritanceFlags, propagationFlags); + aces.Add(AddFileSystemAccessRule(item, account, rights, type, inheritanceFlags, propagationFlags)); } } else @@ -105,9 +115,11 @@ namespace Security2 var item = new DirectoryInfo(path); foreach (var account in accounts) { - yield return AddFileSystemAccessRule(item, account, rights, type, inheritanceFlags, propagationFlags); + aces.Add(AddFileSystemAccessRule(item, account, rights, type, inheritanceFlags, propagationFlags)); } } + + return aces; } public static void AddFileSystemAccessRule(FileSystemAccessRule2 rule) diff --git a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.GetFileSystemAccessRules.cs b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.GetFileSystemAccessRules.cs index 8946af0..0408646 100644 --- a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.GetFileSystemAccessRules.cs +++ b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.GetFileSystemAccessRules.cs @@ -19,7 +19,14 @@ namespace Security2 List aceList = new List(); List inheritedFrom = null; - if (getInheritedFrom) + // The source of an inherited entry is found from the path of the item that the descriptor was read from; + // a descriptor that a caller built directly, such as the conversion from FileSecurity, has none. Such an + // entry gets no source, the same as one whose source Windows could not name: a cmdlet caller has no + // parameter to turn getInheritedFrom off (the module setting GetInheritedFrom is true by default), so an + // exception here would stop a cmdlet that a descriptor without an item was never meant to reach. + var findInheritedFrom = getInheritedFrom && sd.Item != null; + + if (findInheritedFrom) { inheritedFrom = Win32.GetInheritedFrom(sd.Item, sd.SecurityDescriptor, false); } @@ -33,15 +40,16 @@ namespace Security2 foreach (FileSystemAccessRule ace in acl) { - var source = getInheritedFrom && aceCounter < inheritedFrom.Count ? inheritedFrom[aceCounter] : null; + var source = findInheritedFrom && aceCounter < inheritedFrom.Count ? inheritedFrom[aceCounter] : null; aceCounter++; if (ace.IsInherited ? !includeInherited : !includeExplicit) { continue; } - var ace2 = new FileSystemAccessRule2(ace) { FullName = sd.Item.FullName, InheritanceEnabled = !sd.SecurityDescriptor.AreAccessRulesProtected }; - if (getInheritedFrom && inheritedFrom.Count > 0) + // sd.FullName, not sd.Item.FullName: null without an item instead of a NullReferenceException. + var ace2 = new FileSystemAccessRule2(ace) { FullName = sd.FullName, InheritanceEnabled = !sd.SecurityDescriptor.AreAccessRulesProtected }; + if (findInheritedFrom && inheritedFrom.Count > 0) { // Windows names a folder with a trailing backslash; the text for an unknown parent has none. ace2.inheritedFrom = string.IsNullOrEmpty(source) ? "" : source.TrimEnd('\\'); diff --git a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.RemoveFileSystemAccessRulesAll.cs b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.RemoveFileSystemAccessRulesAll.cs index bcc1c27..45cd7e0 100644 --- a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.RemoveFileSystemAccessRulesAll.cs +++ b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.RemoveFileSystemAccessRulesAll.cs @@ -8,16 +8,20 @@ namespace Security2 { public partial class FileSystemAccessRule2 { + /// + /// Removes the explicit access entries of the descriptor; with a non-null , only + /// the entries whose account is in the list (compared like IdentityReference2.Equals), leaving the rest of + /// the descriptor untouched. Before 5.0.0, the list was never applied: the filtered result was discarded, and + /// every explicit entry was removed regardless of the list, including with an empty list. + /// public static void RemoveFileSystemAccessRuleAll(FileSystemSecurity2 sd, List accounts = null) { var acl = sd.SecurityDescriptor.GetAccessRules(true, false, typeof(SecurityIdentifier)); - if (accounts != null) - { - acl.OfType().Where(ace => (accounts.Where(account => account == (IdentityReference2)ace.IdentityReference).Count() > 1)); - } + var entriesToRemove = accounts == null ? acl.OfType() : + acl.OfType().Where(ace => accounts.Any(account => account == (IdentityReference2)ace.IdentityReference)); - foreach (FileSystemAccessRule ace in acl) + foreach (FileSystemAccessRule ace in entriesToRemove.ToList()) { sd.SecurityDescriptor.RemoveAccessRuleSpecific(ace); } diff --git a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.cs b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.cs index 4b1a200..fdeb63e 100644 --- a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.cs +++ b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.cs @@ -31,7 +31,9 @@ namespace Security2 public FileSystemAccessRule2(FileSystemAccessRule fileSystemAccessRule, FileSystemInfo item) { this.fileSystemAccessRule = fileSystemAccessRule; - this.fullName = item.FullName; + // Null without an item, such as when EffectiveAccess.GetEffectiveAccess builds this from a + // FileSystemSecurity2 that has none, instead of a NullReferenceException. + this.fullName = item != null ? item.FullName : null; } public FileSystemAccessRule2(FileSystemAccessRule fileSystemAccessRule, string path) diff --git a/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.AddFileSystemAuditRules.cs b/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.AddFileSystemAuditRules.cs index 59e1b9b..5ed1c8a 100644 --- a/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.AddFileSystemAuditRules.cs +++ b/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.AddFileSystemAuditRules.cs @@ -78,14 +78,24 @@ namespace Security2 return aces; } + /// + /// Adds one audit entry per account to the item at the path, and returns them in the order of + /// . Before 5.0.0, this overload was an iterator (a yield return in the loop + /// below), so nothing happened until the caller enumerated the result; called as a statement, or assigned to + /// a variable that is never read, it changed nothing. It now builds the list and writes the entries before + /// returning, like its FileSystemInfo and FileSystemSecurity2 siblings; an exception that adding an entry + /// raises is now thrown by this call, not by the later enumeration. + /// public static IEnumerable AddFileSystemAuditRule(string path, List accounts, FileSystemRights2 rights, AuditFlags type, InheritanceFlags inheritanceFlags, PropagationFlags propagationFlags) { + var aces = new List(); + if (File.Exists(path)) { var item = new FileInfo(path); foreach (var account in accounts) { - yield return AddFileSystemAuditRule(item, account, rights, type, inheritanceFlags, propagationFlags); + aces.Add(AddFileSystemAuditRule(item, account, rights, type, inheritanceFlags, propagationFlags)); } } else @@ -93,9 +103,11 @@ namespace Security2 var item = new DirectoryInfo(path); foreach (var account in accounts) { - yield return AddFileSystemAuditRule(item, account, rights, type, inheritanceFlags, propagationFlags); + aces.Add(AddFileSystemAuditRule(item, account, rights, type, inheritanceFlags, propagationFlags)); } } + + return aces; } public static void AddFileSystemAuditRule(FileSystemAuditRule2 rule) diff --git a/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.GetFileSystemAuditRules.cs b/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.GetFileSystemAuditRules.cs index fa18d6c..5df90c9 100644 --- a/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.GetFileSystemAuditRules.cs +++ b/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.GetFileSystemAuditRules.cs @@ -19,7 +19,14 @@ namespace Security2 List aceList = new List(); List inheritedFrom = null; - if (getInheritedFrom) + // The source of an inherited entry is found from the path of the item that the descriptor was read from; + // a descriptor that a caller built directly, such as the conversion from FileSecurity, has none. Such an + // entry gets no source, the same as one whose source Windows could not name: a cmdlet caller has no + // parameter to turn getInheritedFrom off (the module setting GetInheritedFrom is true by default), so an + // exception here would stop a cmdlet that a descriptor without an item was never meant to reach. + var findInheritedFrom = getInheritedFrom && sd.Item != null; + + if (findInheritedFrom) { inheritedFrom = Win32.GetInheritedFrom(sd.Item, sd.SecurityDescriptor, true); } @@ -33,15 +40,16 @@ namespace Security2 foreach (FileSystemAuditRule ace in acl) { - var source = getInheritedFrom && aceCounter < inheritedFrom.Count ? inheritedFrom[aceCounter] : null; + var source = findInheritedFrom && aceCounter < inheritedFrom.Count ? inheritedFrom[aceCounter] : null; aceCounter++; if (ace.IsInherited ? !includeInherited : !includeExplicit) { continue; } - var ace2 = new FileSystemAuditRule2(ace) { FullName = sd.Item.FullName, InheritanceEnabled = !sd.SecurityDescriptor.AreAuditRulesProtected }; - if (getInheritedFrom && inheritedFrom.Count > 0) + // sd.FullName, not sd.Item.FullName: null without an item instead of a NullReferenceException. + var ace2 = new FileSystemAuditRule2(ace) { FullName = sd.FullName, InheritanceEnabled = !sd.SecurityDescriptor.AreAuditRulesProtected }; + if (findInheritedFrom && inheritedFrom.Count > 0) { // Windows names a folder with a trailing backslash; the text for an unknown parent has none. ace2.inheritedFrom = string.IsNullOrEmpty(source) ? "" : source.TrimEnd('\\'); diff --git a/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.RemoveFileSystemAuditRuleAll.cs b/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.RemoveFileSystemAuditRuleAll.cs index ee721f0..f1872df 100644 --- a/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.RemoveFileSystemAuditRuleAll.cs +++ b/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.RemoveFileSystemAuditRuleAll.cs @@ -8,16 +8,20 @@ namespace Security2 { public partial class FileSystemAuditRule2 { + /// + /// Removes the explicit audit entries of the descriptor; with a non-null , only + /// the entries whose account is in the list (compared like IdentityReference2.Equals), leaving the rest of + /// the descriptor untouched. Before 5.0.0, the list was never applied: the filtered result was discarded, and + /// every explicit entry was removed regardless of the list, including with an empty list. + /// public static void RemoveFileSystemAuditRuleAll(FileSystemSecurity2 sd, List accounts = null) { var acl = sd.SecurityDescriptor.GetAuditRules(true, false, typeof(SecurityIdentifier)); - if (accounts != null) - { - acl.OfType().Where(ace => (accounts.Where(account => account == (IdentityReference2)ace.IdentityReference).Count() > 1)); - } + var entriesToRemove = accounts == null ? acl.OfType() : + acl.OfType().Where(ace => accounts.Any(account => account == (IdentityReference2)ace.IdentityReference)); - foreach (FileSystemAuditRule ace in acl) + foreach (FileSystemAuditRule ace in entriesToRemove.ToList()) { sd.SecurityDescriptor.RemoveAuditRuleSpecific(ace); } diff --git a/Security2/FileSystem/FileSystemInheritanceInfo.cs b/Security2/FileSystem/FileSystemInheritanceInfo.cs index 5fc9f9c..3a37496 100644 --- a/Security2/FileSystem/FileSystemInheritanceInfo.cs +++ b/Security2/FileSystem/FileSystemInheritanceInfo.cs @@ -35,9 +35,11 @@ namespace Security2 set { auditInheritanceEnabled = value; } } - public string FullName { get { return Item.FullName; } } + // Null without an item, such as for a descriptor that a conversion from FileSecurity built directly, instead + // of a NullReferenceException. + public string FullName { get { return item != null ? item.FullName : null; } } - public string Name { get { return Path.GetFileName(item.FullName); } } + public string Name { get { return item != null ? Path.GetFileName(item.FullName) : null; } } private FileSystemInheritanceInfo(FileSystemInfo item, bool? accessInheritanceEnabled, bool? auditInheritanceEnabled) { diff --git a/Security2/FileSystem/FileSystemOwner.cs b/Security2/FileSystem/FileSystemOwner.cs index 103482b..a2dfea2 100644 --- a/Security2/FileSystem/FileSystemOwner.cs +++ b/Security2/FileSystem/FileSystemOwner.cs @@ -27,7 +27,9 @@ namespace Security2 public string FullName { - get { return item.FullName; } + // Null without an item, such as for a descriptor that a conversion from FileSecurity built directly, + // instead of a NullReferenceException. + get { return item != null ? item.FullName : null; } } private FileSystemOwner(FileSystemInfo item, IdentityReference2 owner) @@ -62,7 +64,9 @@ namespace Security2 public override string ToString() { - return item.FullName; + // Empty, not null, without an item: ToString must never return null, and there is no other + // representation of this object to fall back to. + return item != null ? item.FullName : string.Empty; } } } diff --git a/Security2/FileSystem/FileSystemSecurity2.cs b/Security2/FileSystem/FileSystemSecurity2.cs index cf905ba..6e5b71e 100644 --- a/Security2/FileSystem/FileSystemSecurity2.cs +++ b/Security2/FileSystem/FileSystemSecurity2.cs @@ -22,9 +22,12 @@ namespace Security2 set { item = value; } } - public string FullName { get { return item.FullName; } } + // An object that wraps a descriptor directly, such as the conversion from FileSecurity or DirectorySecurity, + // has no item and so no path to report; null reads more naturally here than an empty string, which would look + // like the path of an item whose name happens to be empty. + public string FullName { get { return item != null ? item.FullName : null; } } - public string Name { get { return item.Name; } } + public string Name { get { return item != null ? item.Name : null; } } public bool IsFile { get { return isFile; } } @@ -39,6 +42,25 @@ namespace Security2 RememberSections(); } + // Wraps a descriptor that a caller created directly, such as the conversions from FileSecurity and + // DirectorySecurity do; the object has no item. Access, Owner, and Group are always part of such a + // descriptor; Audit is part of it only when the descriptor actually carries a SACL (the same test as + // HasSystemAcl), since there was no read to tell sections apart otherwise. Without this, a descriptor that + // Get-Acl returned without -Audit reported HasAuditSection as true, so TestAuditSection (#109) let the audit + // cmdlets go on with no audit data at all, instead of reporting that the descriptor has none. + private FileSystemSecurity2(FileSystemSecurity sd) + { + this.sd = sd; + isFile = sd is FileSecurity; + sections = AccessControlSections.Access | AccessControlSections.Owner | AccessControlSections.Group; + if (new RawSecurityDescriptor(sd.GetSecurityDescriptorBinaryForm(), 0).SystemAcl != null) + { + sections |= AccessControlSections.Audit; + } + + RememberSections(); + } + public FileSystemSecurity2(FileSystemInfo item) { this.item = item; @@ -195,6 +217,8 @@ namespace Security2 // that is read alone, and writing them back fails for an owner that the user cannot assign (#34). public void Write() { + RequireItem(); + SetSecurity(item, sd, sections); RememberSections(); @@ -204,6 +228,8 @@ namespace Security2 // unchanged owner that the user cannot assign isn't written back (#34). Without a change, it writes nothing. internal void WriteChanges() { + RequireItem(); + var changedSections = ChangedSections; if (changedSections == AccessControlSections.None) { @@ -215,6 +241,20 @@ namespace Security2 RememberSections(); } + // Write() and WriteChanges() write to the item that the descriptor was read from; an object that a conversion + // from FileSecurity or DirectorySecurity created has none. Without this check they would fail with a + // NullReferenceException deep in SetSecurity instead of saying what is missing. + private void RequireItem() + { + if (item == null) + { + throw new InvalidOperationException( + "This security descriptor has no item to write to: it was created directly from a FileSecurity or " + + "DirectorySecurity object, not read from a file or folder. Set the Item property first, or call " + + "Write(FileSystemInfo) or Write(string) to choose where to write it."); + } + } + // Writes the sections that the descriptor was read with to another item, like Write(). public void Write(FileSystemInfo item) { @@ -243,14 +283,17 @@ namespace Security2 #region Conversion // The descriptor is in sd. Before 5.0.0-rc6, these conversions returned fields that were never set, so they - // gave null, and the hash code threw a NullReferenceException. + // gave null, and the hash code threw a NullReferenceException. Before 5.0.0, the conversions the other way + // (from FileSecurity/DirectorySecurity) ignored the descriptor that was passed and returned an object built + // from an empty path, which always threw; they now keep the descriptor that was passed, see the private + // constructor above. public static implicit operator FileSecurity(FileSystemSecurity2 fs2) { return fs2.sd as FileSecurity; } public static implicit operator FileSystemSecurity2(FileSecurity fs) { - return new FileSystemSecurity2(new FileInfo("")); + return new FileSystemSecurity2((FileSystemSecurity)fs); } public static implicit operator DirectorySecurity(FileSystemSecurity2 fs2) @@ -259,7 +302,7 @@ namespace Security2 } public static implicit operator FileSystemSecurity2(DirectorySecurity fs) { - return new FileSystemSecurity2(new DirectoryInfo("")); + return new FileSystemSecurity2((FileSystemSecurity)fs); } // Like the descriptors of .NET, two objects are equal when they wrap the same descriptor; a descriptor of .NET diff --git a/Tests/Access.Tests.ps1 b/Tests/Access.Tests.ps1 index 5b70d0f..fbdbb33 100644 --- a/Tests/Access.Tests.ps1 +++ b/Tests/Access.Tests.ps1 @@ -88,6 +88,26 @@ Describe 'Get-NTFSEffectiveAccess' { $result | Should -BeNullOrEmpty } + # Each piped object names its own account, so one call calculates the access of two accounts on the same item. + It 'Should bind -Account by property name and calculate the access of the account that its own object names' { + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'EffectiveAccountByProperty' + Disable-NTFSAccessInheritance -Path $file -RemoveInheritedAccessRules -ErrorAction Stop + Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop + Add-NTFSAccess -Path $file -Account 'S-1-5-32-546' -AccessRights ReadData -AccessType Deny -ErrorAction Stop + $requests = @( + [pscustomobject]@{ Path = $file; Account = 'S-1-1-0' } + [pscustomobject]@{ Path = $file; Account = 'S-1-5-32-546' } + ) + + $result = @($requests | Get-NTFSEffectiveAccess -WarningAction SilentlyContinue -ErrorAction Stop) + + $result | Should -HaveCount 2 + $result[0].Account.Sid | Should -Be 'S-1-1-0' + $result[0].AccessRights.HasFlag([Security2.FileSystemRights2]::ReadData) | Should -BeTrue + $result[1].Account.Sid | Should -Be 'S-1-5-32-546' + $result[1].AccessRights.HasFlag([Security2.FileSystemRights2]::ReadData) | Should -BeFalse + } + It 'Should return an account with access when -ExcludeNoneAccessEntries is used' { $result = @(Get-NTFSEffectiveAccess -Path $effectiveFile -ExcludeNoneAccessEntries -WarningAction SilentlyContinue -ErrorAction Stop) diff --git a/Tests/ItemCmdlets.Tests.ps1 b/Tests/ItemCmdlets.Tests.ps1 index e6a1e4a..721a027 100644 --- a/Tests/ItemCmdlets.Tests.ps1 +++ b/Tests/ItemCmdlets.Tests.ps1 @@ -122,6 +122,23 @@ Describe 'Get-ChildItem2' { Assert-TestSandboxPath -Sandbox $sandbox -Path (@($grandchild) + @($paths)) New-Item -ItemType Directory -Path $grandchild -Force | Out-Null foreach ($path in $paths) { Set-Content -LiteralPath $path -Value 'Tree' } + + # The fixture for the -Filter truth table: 18 files and 4 folders, all 8.3-compliant (at most one dot, + # up to 8 characters of base name and 3 of extension, no spaces), so each real name is also its own + # short name and AlphaFS enumerates the same identity that the matcher compares. + $filterTruthTableFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterTruthTable' -Directory + $truthTableFiles = 'a', 'ab', 'abc', 'a.b', 'ab.c', 'abc.de', 'a.bcd', 'abcdefgh', 'abcdefgh.ijk', 'x.y', 'a1.b2', 'bca', 'cab.cd', 'ccc', 'c.c', 'bcabcabc', 'a2.b1', '1cb' + $truthTableDirs = 'bdir', 'bdir.cd', 'fold', 'fo.ld' + foreach ($name in $truthTableFiles) { + $path = Join-Path -Path $filterTruthTableFolder -ChildPath $name + Assert-TestSandboxPath -Sandbox $sandbox -Path $path + Set-Content -LiteralPath $path -Value $name + } + foreach ($name in $truthTableDirs) { + $path = Join-Path -Path $filterTruthTableFolder -ChildPath $name + Assert-TestSandboxPath -Sandbox $sandbox -Path $path + New-Item -ItemType Directory -Path $path | Out-Null + } } It 'Should return the exact tree for ' -ForEach @( @@ -146,9 +163,13 @@ Describe 'Get-ChildItem2' { @{ Filter = '*.htm'; Expected = @('Page.htm') } @{ Filter = 'Page?.html'; Expected = @('Page2.html') } @{ Filter = 'PAGE*'; Expected = @('Page.htm', 'Page2.html') } + @{ Filter = 'Page.ht?'; Expected = @('Page.htm') } + @{ Filter = 'Page2.h*'; Expected = @('Page2.html') } + @{ Filter = 'Page2.'; Expected = @() } + @{ Filter = 'LONGFI~1*'; Expected = @() } ) { $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterNames' -Directory - foreach ($name in 'Page.htm', 'Page2.html') { + foreach ($name in 'Page.htm', 'Page2.html', 'LongFileName') { $file = Join-Path -Path $folder -ChildPath $name Assert-TestSandboxPath -Sandbox $sandbox -Path $file Set-Content -LiteralPath $file -Value $name @@ -229,9 +250,12 @@ Describe 'Get-ChildItem2' { ($result.Name | Sort-Object) -join ',' | Should -BeExactly 'NoExtension,NoExtensionFolder,Page.htm' } - # The enumeration returns every item for *.*.*, as Get-ChildItem does. The cmdlet compares each name with the whole - # pattern again, so that the dots of the pattern are characters of the name, as the help says. - It 'Should return only the items that match the whole pattern for -Filter *.*.*' { + # The cmdlet now has one matcher, not two: every dot in -Filter whose single next + # character is an asterisk or a question mark, or that has nothing after it, is a DOS_DOT, and *.*.* has + # two such dots, so it returns every item, the same as *.* and a plain *. Before this change, the cmdlet + # compared the name with the pattern a second time with the dots read as ordinary characters, so only a + # name with two literal dots matched. + It 'Should return every item for -Filter *.*.*, also the ones with one dot or none' { $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterDots' -Directory foreach ($name in 'Page.htm', 'NoExtension', 'Two.dots.txt') { $file = Join-Path -Path $folder -ChildPath $name @@ -241,20 +265,20 @@ Describe 'Get-ChildItem2' { $result = @(Get-ChildItem2 -Path $folder -Filter '*.*.*' -ErrorAction Stop) - ($result.Name -join ',') | Should -BeExactly 'Two.dots.txt' + ($result.Name | Sort-Object) -join ',' | Should -BeExactly 'NoExtension,Page.htm,Two.dots.txt' } - # The names are matched twice, by the enumeration and by the cmdlet, and the rules for a dot differ from those of - # Get-ChildItem, where Report.* also returns Report. The documentation lists this as a limitation; these cases pin - # it, so that a change of the rules is a decision. Report* is the control: without a dot in the pattern, the names - # without a dot are returned. + # The names are matched once, by the cmdlet, with the Win32 "DOS" wildcard rules that Windows PowerShell 5.1 + # applies to Get-ChildItem -Filter, in both editions: Report.* also returns Report, + # which has no dot, and a trailing dot asks for the names without a dot. Report* is the control: without a + # dot in the pattern, the names with and without a dot are both returned, as before. It 'Should return for -Filter ""' -ForEach @( - @{ Filter = 'Report.*'; Expected = 'Report.txt'; Outcome = 'only the names with a dot' } + @{ Filter = 'Report.*'; Expected = 'Report,Report.txt'; Outcome = 'the names with and without a dot' } @{ Filter = 'Report*'; Expected = 'Report,Report.txt'; Outcome = 'the names with and without a dot' } - @{ Filter = 'Report.'; Expected = ''; Outcome = 'nothing' } - @{ Filter = 'Rep*.'; Expected = ''; Outcome = 'nothing' } - @{ Filter = '*.'; Expected = ''; Outcome = 'nothing' } - @{ Filter = ''; Expected = ''; Outcome = 'nothing' } + @{ Filter = 'Report.'; Expected = 'Report'; Outcome = 'only the name without a dot' } + @{ Filter = 'Rep*.'; Expected = 'Report'; Outcome = 'only the name without a dot' } + @{ Filter = '*.'; Expected = 'Other,Report'; Outcome = 'the names without a dot' } + @{ Filter = ''; Expected = 'Other,Report,Report.txt'; Outcome = 'every item, like no filter at all' } ) { $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterDotRules' -Directory foreach ($name in 'Report', 'Report.txt', 'Other') { @@ -268,11 +292,162 @@ Describe 'Get-ChildItem2' { ($result.Name | Sort-Object) -join ',' | Should -BeExactly $Expected } + # A fixed truth table, pinned with the literal result of Windows PowerShell 5.1's Get-ChildItem on 8.3-compliant + # names (at most one dot, up to 8 characters of base name and 3 of extension, no spaces), whose short name is + # its own real name, so AlphaFS and the cmdlet agree on which name they compare. The expected values are data, + # not the output of a run of Get-ChildItem: this test is its own oracle. + It 'Should match -Filter "" the way Windows PowerShell 5.1 does' -ForEach @( + @{ Filter = '*'; Expected = @('1cb', 'a', 'a.b', 'a.bcd', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk', 'bca', 'bcabcabc', 'bdir', 'bdir.cd', 'c.c', 'cab.cd', 'ccc', 'fo.ld', 'fold', 'x.y') } + @{ Filter = '*.*'; Expected = @('1cb', 'a', 'a.b', 'a.bcd', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk', 'bca', 'bcabcabc', 'bdir', 'bdir.cd', 'c.c', 'cab.cd', 'ccc', 'fo.ld', 'fold', 'x.y') } + @{ Filter = '*.'; Expected = @('1cb', 'a', 'ab', 'abc', 'abcdefgh', 'bca', 'bcabcabc', 'bdir', 'ccc', 'fold') } + @{ Filter = 'a.*'; Expected = @('a', 'a.b', 'a.bcd') } + @{ Filter = 'ab.*'; Expected = @('ab', 'ab.c') } + @{ Filter = 'a.'; Expected = @('a') } + @{ Filter = 'ab.'; Expected = @('ab') } + @{ Filter = 'a*.'; Expected = @('a', 'ab', 'abc', 'abcdefgh') } + @{ Filter = 'ab*.'; Expected = @('ab', 'abc', 'abcdefgh') } + @{ Filter = 'c*.'; Expected = @('ccc') } + @{ Filter = '*.b'; Expected = @('a.b') } + @{ Filter = '*.c?'; Expected = @('ab.c', 'bdir.cd', 'c.c', 'cab.cd') } + @{ Filter = '*.b?'; Expected = @('a.b', 'a1.b2', 'a2.b1') } + @{ Filter = '*.?'; Expected = @('1cb', 'a', 'a.b', 'ab', 'ab.c', 'abc', 'abcdefgh', 'bca', 'bcabcabc', 'bdir', 'c.c', 'ccc', 'fold', 'x.y') } + @{ Filter = '*.??'; Expected = @('1cb', 'a', 'a.b', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'bca', 'bcabcabc', 'bdir', 'bdir.cd', 'c.c', 'cab.cd', 'ccc', 'fo.ld', 'fold', 'x.y') } + @{ Filter = '?'; Expected = @('a') } + @{ Filter = '??'; Expected = @('a', 'ab') } + @{ Filter = 'a?'; Expected = @('a', 'ab') } + @{ Filter = 'b?'; Expected = @() } + @{ Filter = 'ab?'; Expected = @('ab', 'abc') } + @{ Filter = 'a?.'; Expected = @('a', 'ab') } + @{ Filter = 'bc?'; Expected = @('bca') } + @{ Filter = '?.?'; Expected = @('a', 'a.b', 'c.c', 'x.y') } + @{ Filter = 'c.?'; Expected = @('c.c') } + @{ Filter = '*a*'; Expected = @('a', 'a.b', 'a.bcd', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk', 'bca', 'bcabcabc', 'cab.cd') } + @{ Filter = '*c*'; Expected = @('1cb', 'a.bcd', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk', 'bca', 'bcabcabc', 'bdir.cd', 'c.c', 'cab.cd', 'ccc') } + @{ Filter = 'a*.*'; Expected = @('a', 'a.b', 'a.bcd', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk') } + @{ Filter = 'bca*.'; Expected = @('bca', 'bcabcabc') } + @{ Filter = '*.*.*'; Expected = @('1cb', 'a', 'a.b', 'a.bcd', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk', 'bca', 'bcabcabc', 'bdir', 'bdir.cd', 'c.c', 'cab.cd', 'ccc', 'fo.ld', 'fold', 'x.y') } + @{ Filter = '.*'; Expected = @() } + @{ Filter = '*..*'; Expected = @() } + @{ Filter = 'abc'; Expected = @('abc') } + @{ Filter = 'cba'; Expected = @() } + @{ Filter = 'a.b.'; Expected = @('a.b') } + @{ Filter = 'ab.c.'; Expected = @('ab.c') } + @{ Filter = '*b.'; Expected = @('1cb', 'a.b', 'ab') } + @{ Filter = 'cab*'; Expected = @('cab.cd') } + @{ Filter = 'a??'; Expected = @('a', 'ab', 'abc') } + @{ Filter = 'c.c'; Expected = @('c.c') } + @{ Filter = 'ca?'; Expected = @() } + ) { + $folder = $filterTruthTableFolder + + $result = @(Get-ChildItem2 -Path $folder -Filter $Filter -Force -ErrorAction Stop) + + ($result.Name | Sort-Object) -join ',' | Should -BeExactly (($Expected | Sort-Object) -join ',') + } + + # A star in front of a dot token may cross every dot in the name but the LAST one (Win32 DOS_STAR), not stop + # at the first one: with names that have two or more dots, a pattern such as "*.?" still has to reach the + # last dot before the dot token and the question mark account for the final segment. The 8.3-compliant + # names of the truth table above have at most one dot, where the first dot and the last dot are the same + # position, so they cannot tell the two rules apart; this fixture uses names with two or three dots + # instead, including realistic ones such as archive.tar.gz and jquery.min.js. Expected values are pinned + # with the real name of each item, confirmed with `fsutil file setshortname` so that a short name the volume + # might otherwise assign cannot also produce a match. + It 'Should match -Filter "" on a name with two or more dots the way Windows PowerShell 5.1 does' -ForEach @( + @{ Filter = '*.?'; Expected = @('a.b.c', 'v1.2.3', 'x.y.z.w') } + @{ Filter = '*.??'; Expected = @('a.b.c', 'a.b.cd', 'archive.tar.gz', 'index.d.ts', 'jquery.min.js', 'my.file.tx', 'v1.2.3', 'x.y.z.w') } + @{ Filter = '*.???'; Expected = @('a.b.c', 'a.b.cd', 'archive.tar.gz', 'index.d.ts', 'jquery.min.js', 'my.file.name.txt', 'my.file.tx', 'v1.2.3', 'x.y.z.w') } + @{ Filter = '*.?.?'; Expected = @('a.b.c', 'v1.2.3', 'x.y.z.w') } + @{ Filter = '*.*.*'; Expected = @('a.b.c', 'a.b.cd', 'archive.tar.gz', 'index.d.ts', 'jquery.min.js', 'my.file.name.txt', 'my.file.tx', 'one.two.three', 'v1.2.3', 'x.y.z.w') } + @{ Filter = 'a*.?'; Expected = @('a.b.c') } + @{ Filter = 'a.b.?'; Expected = @('a.b.c') } + @{ Filter = 'a.b.??'; Expected = @('a.b.c', 'a.b.cd') } + @{ Filter = 'x.*.?'; Expected = @('x.y.z.w') } + ) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterMultiDot' -Directory + foreach ($name in 'a.b.c', 'a.b.cd', 'x.y.z.w', 'my.file.tx', 'one.two.three', 'archive.tar.gz', 'jquery.min.js', 'my.file.name.txt', 'v1.2.3', 'index.d.ts') { + $file = Join-Path -Path $folder -ChildPath $name + Assert-TestSandboxPath -Sandbox $sandbox -Path $file + Set-Content -LiteralPath $file -Value $name + } + + $result = @(Get-ChildItem2 -Path $folder -Filter $Filter -Force -ErrorAction Stop) + + ($result.Name | Sort-Object) -join ',' | Should -BeExactly (($Expected | Sort-Object) -join ',') + } + + It 'Should apply a -Filter at every level of a recursive listing, directories included' { + $recurseFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterRecurse' -Directory + $paths = @( + 'A.txt', 'B.log', + 'Sub\C.txt', 'Sub\D.log', + 'Sub\Sub2\E.txt', 'Sub\Sub2\F.log' + ) | ForEach-Object -Process { Join-Path -Path $recurseFolder -ChildPath $_ } + Assert-TestSandboxPath -Sandbox $sandbox -Path $paths + New-Item -ItemType Directory -Path (Join-Path -Path $recurseFolder -ChildPath 'Sub\Sub2') -Force | Out-Null + foreach ($path in $paths) { Set-Content -LiteralPath $path -Value 'Content' } + + $result = @(Get-ChildItem2 -Path $recurseFolder -Recurse -Filter '*.txt' -ErrorAction Stop) + + ($result.Name | Sort-Object) -join ',' | Should -BeExactly 'A.txt,C.txt,E.txt' + } + + It 'Should apply a dot -Filter together with -File and with -Directory' { + $folder = $filterTruthTableFolder + + $files = @(Get-ChildItem2 -Path $folder -File -Filter '*.' -Force -ErrorAction Stop) + $directories = @(Get-ChildItem2 -Path $folder -Directory -Filter '*.' -Force -ErrorAction Stop) + + ($files.Name | Sort-Object) -join ',' | Should -BeExactly '1cb,a,ab,abc,abcdefgh,bca,bcabcabc,ccc' + ($directories.Name | Sort-Object) -join ',' | Should -BeExactly 'bdir,fold' + } + + It 'Should return every item for an empty -Filter, like no filter at all' { + $folder = $filterTruthTableFolder + + $result = @(Get-ChildItem2 -Path $folder -Filter '' -Force -ErrorAction Stop) + + $result | Should -HaveCount 22 + } + + It 'Should apply -Filter to a -Path that is a file' { + $file = Join-Path -Path $tree -ChildPath 'Root.txt' + + $matching = @(Get-ChildItem2 -Path $file -Filter '*.txt' -ErrorAction Stop) + $other = @(Get-ChildItem2 -Path $file -Filter '*.log' -ErrorAction Stop) + + $matching | Should -HaveCount 1 + $matching[0].FullName | Should -Be $file + $other | Should -BeNullOrEmpty + } + It 'Should reject a null -Filter' { { Get-ChildItem2 -Path $tree -Filter $null -ErrorAction Stop } | Should -Throw -ErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' -ExpectedMessage "*'Filter'*" } + It 'Should reject a -Filter of more than 32,767 characters' { + $tooLong = '?' * 32768 + { Get-ChildItem2 -Path $tree -Filter $tooLong -ErrorAction Stop } | + Should -Throw -ErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' -ExpectedMessage "*'Filter'*" + } + + # A pattern this long is not useful, but the cmdlet must not crash the host: before this test was added, a + # pattern of this length (or a long run of asterisks) recursed deep enough in the matcher to raise an + # uncatchable StackOverflowException and terminate the process. + It 'Should accept a -Filter of exactly 32,767 characters and return the right result' { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterMaxLength' -Directory + $file = Join-Path -Path $folder -ChildPath 'a' + Assert-TestSandboxPath -Sandbox $sandbox -Path $file + Set-Content -LiteralPath $file -Value 'a' + $longest = '?' * 32767 + + $result = @(Get-ChildItem2 -Path $folder -Filter $longest -ErrorAction Stop) + + $result | Should -HaveCount 1 + $result[0].Name | Should -Be 'a' + } + It 'Should stop a recursive pipeline without recording an enumeration error' { $result = @(Get-ChildItem2 -Path $tree -Recurse -ErrorVariable childErrors -ErrorAction SilentlyContinue | Select-Object -First 1) diff --git a/Tests/ObjectApis.Tests.ps1 b/Tests/ObjectApis.Tests.ps1 index 575e34c..5b39cc9 100644 --- a/Tests/ObjectApis.Tests.ps1 +++ b/Tests/ObjectApis.Tests.ps1 @@ -395,9 +395,11 @@ Describe 'Access rule helpers that take a path' { $entries[0].FileSystemRights | Should -Be ([System.Security.AccessControl.FileSystemRights] 'ReadData, Synchronize') } - # The overload that takes a path returns an iterator, so the caller must enumerate the result to write the entries. - # The overloads that take an item write them at once; this test doesn't pin the difference. - It 'Should add the entries of several accounts to a by its path when the result is enumerated' -ForEach @( + # Before 5.0.0, the overload that takes a path was an iterator (yield return) that changed nothing until the + # caller enumerated the result, so a call as a statement, or an assignment that the caller never reads, wrote + # nothing; the overloads that take an item or a FileSystemSecurity2 were never lazy. It now writes at once, like + # its siblings, so the entries exist before $pending is ever touched. + It 'Should add the entries of several accounts to a by its path at once, before the result is ever enumerated' -ForEach @( @{ Kind = 'file'; Directory = $false } @{ Kind = 'folder'; Directory = $true } ) { @@ -408,9 +410,13 @@ Describe 'Access rule helpers that take a path' { $path, $accounts, [Security2.FileSystemRights2]::ReadData, $allow, $noInheritance, $noPropagation ) - @($pending) | Should -HaveCount 2 @(Get-ExplicitEntries -Path $path) | Should -HaveCount 1 @(Get-ExplicitEntries -Path $path -Account 'S-1-5-32-545') | Should -HaveCount 1 + + $added = @($pending) + $added | Should -HaveCount 2 + $added[0].Account.Sid | Should -BeExactly $identity.Sid + $added[1].Account.Sid | Should -BeExactly $users.Sid } It 'Should add the deny entries of several accounts without Synchronize when the result is enumerated' { @@ -589,7 +595,10 @@ Describe 'Audit rule helpers that take a path' -Skip:(-not $holdsSecurityPrivile $found[0].FullName | Should -BeExactly $path } - It 'Should add the entries of several accounts to a by its path when the result is enumerated, and remove them again' -ForEach @( + # Before 5.0.0, the overload that takes a path was an iterator (yield return) that changed nothing until the + # caller enumerated the result; it now writes at once, like its siblings, so the entries exist before $pending is + # ever touched. This overload needs the Security privilege like its neighbors, so the whole Describe skips without it. + It 'Should add the entries of several accounts to a by its path at once, before the result is ever enumerated, and remove them again' -ForEach @( @{ Kind = 'file'; Directory = $false } @{ Kind = 'folder'; Directory = $true } ) { @@ -602,9 +611,13 @@ Describe 'Audit rule helpers that take a path' -Skip:(-not $holdsSecurityPrivile $path, $accounts, [Security2.FileSystemRights2]::Delete, $success, $noInheritance, $noPropagation ) - @($pending) | Should -HaveCount 2 @(Get-AuditEntries -Path $path) | Should -HaveCount 1 @(Get-AuditEntries -Path $path -Account 'S-1-5-32-545') | Should -HaveCount 1 + $added = @($pending) + $added | Should -HaveCount 2 + $added[0].Account.Sid | Should -BeExactly $identity.Sid + $added[1].Account.Sid | Should -BeExactly $users.Sid + [Security2.FileSystemAuditRule2]::RemoveFileSystemAuditRule( $path, $identity, [Security2.FileSystemRights2]::Delete, $success, $noInheritance, $noPropagation, $true ) @@ -797,6 +810,442 @@ Describe 'Owner and descriptor objects' { } } +# Before 5.0.0, both conversions ignored the descriptor that was passed and returned a FileSystemSecurity2 that wrapped +# a FileInfo or a DirectoryInfo built from an empty path, which AlphaFS rejects, so the conversion always threw. The +# object that the fixed conversion returns has no item: FullName and Name, which have no path to report, are null, +# which reads most naturally as "nothing to name". +Describe 'The FileSystemSecurity2 conversions from FileSecurity and DirectorySecurity' { + BeforeAll { + function New-RawFileSystemSecurity { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds an in-memory descriptor only.' + )] + param ([string] $Kind) + + $raw = New-Object -TypeName "System.Security.AccessControl.$Kind" + $rule = New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList ( + $sid, [System.Security.AccessControl.FileSystemRights]::ReadData, + [System.Security.AccessControl.AccessControlType]::Allow + ) + $raw.AddAccessRule($rule) + $raw + } + + # PowerShell silently turns an exception that a property getter throws into $null, without even recording a + # non-terminating error, so a plain "$object.Property" cannot tell a getter that throws from one that + # legitimately returns null. The getter is invoked through reflection instead, so that it throws normally. + function Invoke-PropertyGetter { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Test helper that only reads a property through reflection.' + )] + param ([object] $InputObject, [string] $PropertyName) + + $InputObject.GetType().GetProperty($PropertyName).GetGetMethod().Invoke($InputObject, $null) + } + } + + It 'Should wrap the descriptor that was passed, report IsFile as , and round-trip the same instance' -ForEach @( + @{ Kind = 'FileSecurity'; IsFile = $true } + @{ Kind = 'DirectorySecurity'; IsFile = $false } + ) { + $raw = New-RawFileSystemSecurity -Kind $Kind + $targetType = [type] "System.Security.AccessControl.$Kind" + + $wrapped = [Security2.FileSystemSecurity2] $raw + + [object]::ReferenceEquals($wrapped.SecurityDescriptor, $raw) | Should -BeTrue + $wrapped.IsFile | Should -Be $IsFile + [object]::ReferenceEquals(($wrapped -as $targetType), $raw) | Should -BeTrue + # A plain property read turns a throwing getter into $null (see Invoke-PropertyGetter above), so these two + # getters are invoked through reflection: a null FullName/Name is pinned as the getter's own return value, + # not as whatever a getter that happened to throw would also look like from here. + { Invoke-PropertyGetter -InputObject $wrapped -PropertyName 'FullName' } | Should -Not -Throw + { Invoke-PropertyGetter -InputObject $wrapped -PropertyName 'Name' } | Should -Not -Throw + (Invoke-PropertyGetter -InputObject $wrapped -PropertyName 'FullName') | Should -Be $null + (Invoke-PropertyGetter -InputObject $wrapped -PropertyName 'Name') | Should -Be $null + } + + It 'Should compare two conversions of the same descriptor as equal, and of different descriptors as unequal' -ForEach @( + @{ Kind = 'FileSecurity' } + @{ Kind = 'DirectorySecurity' } + ) { + $raw = New-RawFileSystemSecurity -Kind $Kind + + $first = [Security2.FileSystemSecurity2] $raw + $second = [Security2.FileSystemSecurity2] $raw + $other = [Security2.FileSystemSecurity2] (New-RawFileSystemSecurity -Kind $Kind) + + $first.Equals($second) | Should -BeTrue + $first.GetHashCode() | Should -Be $second.GetHashCode() + $first.Equals($other) | Should -BeFalse + } + + It 'Should throw an InvalidOperationException, not a NullReferenceException, when a without an item is written' -ForEach @( + @{ Kind = 'FileSecurity' } + @{ Kind = 'DirectorySecurity' } + ) { + $wrapped = [Security2.FileSystemSecurity2] (New-RawFileSystemSecurity -Kind $Kind) + + $failure = { $wrapped.Write() } | Should -Throw -PassThru + + $failure.Exception.GetBaseException() | Should -BeOfType [System.InvalidOperationException] + $failure.Exception.GetBaseException().Message | Should -BeLike '*item*' + } + + It 'Should write a without an item once the caller sets Item' -ForEach @( + @{ Kind = 'FileSecurity'; Directory = $false } + @{ Kind = 'DirectorySecurity'; Directory = $true } + ) { + $path = New-TestSandboxItem -Sandbox $sandbox -Name 'ConvertedWriteItem' -Directory:$Directory + $wrapped = [Security2.FileSystemSecurity2] (New-RawFileSystemSecurity -Kind $Kind) + $wrapped.Item = Get-Item2 -Path $path + + $wrapped.Write() + + @((Get-Acl -LiteralPath $path).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) | + Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) | Should -HaveCount 1 + } + + It 'Should write a without an item directly to a path' -ForEach @( + @{ Kind = 'FileSecurity'; Directory = $false } + @{ Kind = 'DirectorySecurity'; Directory = $true } + ) { + $path = New-TestSandboxItem -Sandbox $sandbox -Name 'ConvertedWritePath' -Directory:$Directory + $wrapped = [Security2.FileSystemSecurity2] (New-RawFileSystemSecurity -Kind $Kind) + + $wrapped.Write([string] $path) + + @((Get-Acl -LiteralPath $path).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) | + Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) | Should -HaveCount 1 + } + + # 3.1 makes a FileSystemSecurity2 without an item reachable from a .NET caller; the readers below must not meet a + # NullReferenceException from it. Built in memory, so no privilege is needed to add the audit rule. + It 'Should return the entries of a without an item, with a null FullName' -ForEach @( + @{ Kind = 'FileSecurity'; RuleType = 'access' } + @{ Kind = 'DirectorySecurity'; RuleType = 'access' } + @{ Kind = 'FileSecurity'; RuleType = 'audit' } + @{ Kind = 'DirectorySecurity'; RuleType = 'audit' } + ) { + if ($RuleType -eq 'access') { + $sd = [Security2.FileSystemSecurity2] (New-RawFileSystemSecurity -Kind $Kind) + $found = @([Security2.FileSystemAccessRule2]::GetFileSystemAccessRules($sd, $true, $true, $false)) + } + else { + $raw = New-RawFileSystemSecurity -Kind $Kind + $raw.AddAuditRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAuditRule' -ArgumentList ( + $sid, [System.Security.AccessControl.FileSystemRights]::ReadData, + [System.Security.AccessControl.AuditFlags]::Success + ))) + $sd = [Security2.FileSystemSecurity2] $raw + $found = @([Security2.FileSystemAuditRule2]::GetFileSystemAuditRules($sd, $true, $true, $false)) + } + + $found | Should -HaveCount 1 + $found[0].FullName | Should -Be $null + $found[0].Account.Sid | Should -BeExactly 'S-1-1-0' + } + + # Before this fix, a descriptor without an item made getInheritedFrom throw an InvalidOperationException, which a + # cmdlet caller has no parameter to avoid (the module setting GetInheritedFrom is $true by default): getInheritedFrom + # is now treated as false for such a descriptor instead, the same as an entry whose source Windows cannot name. + It 'Should return the entries with no source, not throw, when getInheritedFrom is requested for a descriptor without an item' -ForEach @( + @{ RuleType = 'access' } + @{ RuleType = 'audit' } + ) { + if ($RuleType -eq 'access') { + $sd = [Security2.FileSystemSecurity2] (New-RawFileSystemSecurity -Kind 'FileSecurity') + { [Security2.FileSystemAccessRule2]::GetFileSystemAccessRules($sd, $true, $true, $true) } | Should -Not -Throw + $found = @([Security2.FileSystemAccessRule2]::GetFileSystemAccessRules($sd, $true, $true, $true)) + } + else { + $raw = New-RawFileSystemSecurity -Kind 'FileSecurity' + $raw.AddAuditRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAuditRule' -ArgumentList ( + $sid, [System.Security.AccessControl.FileSystemRights]::ReadData, + [System.Security.AccessControl.AuditFlags]::Success + ))) + $sd = [Security2.FileSystemSecurity2] $raw + { [Security2.FileSystemAuditRule2]::GetFileSystemAuditRules($sd, $true, $true, $true) } | Should -Not -Throw + $found = @([Security2.FileSystemAuditRule2]::GetFileSystemAuditRules($sd, $true, $true, $true)) + } + + $found | Should -HaveCount 1 + $found[0].InheritedFrom | Should -Be $null + } + + It 'Should return an owner object with a null FullName and an empty ToString for a without an item' -ForEach @( + @{ Kind = 'FileSecurity' } + @{ Kind = 'DirectorySecurity' } + ) { + $raw = New-RawFileSystemSecurity -Kind $Kind + $raw.SetOwner($sid) + $sd = [Security2.FileSystemSecurity2] $raw + + $owner = [Security2.FileSystemOwner]::GetOwner($sd) + + $owner.Owner.Sid | Should -BeExactly 'S-1-1-0' + { Invoke-PropertyGetter -InputObject $owner -PropertyName 'FullName' } | Should -Not -Throw + (Invoke-PropertyGetter -InputObject $owner -PropertyName 'FullName') | Should -Be $null + $owner.ToString() | Should -BeExactly '' + } + + It 'Should return inheritance info with a null FullName and Name for a without an item' -ForEach @( + @{ Kind = 'FileSecurity' } + @{ Kind = 'DirectorySecurity' } + ) { + $sd = [Security2.FileSystemSecurity2] (New-RawFileSystemSecurity -Kind $Kind) + + $info = [Security2.FileSystemInheritanceInfo]::GetFileSystemInheritanceInfo($sd) + + { Invoke-PropertyGetter -InputObject $info -PropertyName 'FullName' } | Should -Not -Throw + { Invoke-PropertyGetter -InputObject $info -PropertyName 'Name' } | Should -Not -Throw + (Invoke-PropertyGetter -InputObject $info -PropertyName 'FullName') | Should -Be $null + (Invoke-PropertyGetter -InputObject $info -PropertyName 'Name') | Should -Be $null + $info.AccessInheritanceEnabled | Should -BeTrue + } + + # The Authz access check needs a descriptor with an owner and a group, like any real file has; the minimal + # descriptor of the other tests has neither, and the call fails with "The parameter is incorrect" for either. + It 'Should return the effective access of a without an item, with a null FullName on the result' -ForEach @( + @{ Kind = 'FileSecurity' } + @{ Kind = 'DirectorySecurity' } + ) { + $raw = New-RawFileSystemSecurity -Kind $Kind + $raw.SetOwner($sid) + $raw.SetGroup($sid) + $sd = [Security2.FileSystemSecurity2] $raw + + $result = [Security2.EffectiveAccess]::GetEffectiveAccess($sd, $identity, $null) + + if ($result.OperationFailed) { + Set-ItResult -Skipped -Because "the Authz call could not run in this configuration: $($result.AuthzException.Message)" + return + } + + $result.Ace.FullName | Should -Be $null + } +} + +# PowerShell's binder uses the conversion from FileSecurity and DirectorySecurity for every -SecurityDescriptor +# parameter, so a descriptor that Get-Acl returns, never meant for these cmdlets, binds where it used to be a binding +# error. The descriptor has no item and no audit section unless Get-Acl read one. These pin a reader of the access +# entries, a reader of the audit entries, the inheritance reader, and the writer. +Describe 'Cmdlets with a security descriptor that Get-Acl returns' { + BeforeAll { + $plainFile = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAclPlain' + Add-NTFSAccess -Path $plainFile -Account 'S-1-1-0' -AccessRights ReadData -AppliesTo ThisFolderOnly + } + + It 'Should return the explicit access entries and no error for Get-NTFSAccess -SecurityDescriptor (Get-Acl ...)' { + $sd = Get-Acl -LiteralPath $plainFile + $expected = @($sd.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier])) + + $result = @(Get-NTFSAccess -SecurityDescriptor $sd -ExcludeInherited -ErrorVariable accessErrors -ErrorAction SilentlyContinue) + + $accessErrors | Should -BeNullOrEmpty + ($result.Account.Sid | Sort-Object) | Should -Be ($expected.IdentityReference.Value | Sort-Object) + } + + It 'Should write the #109 ReadSecurityError, not an empty result, for Get-NTFSAudit -SecurityDescriptor (Get-Acl ...) without -Audit' { + $sd = Get-Acl -LiteralPath $plainFile + + $result = @(Get-NTFSAudit -SecurityDescriptor $sd -ErrorVariable auditErrors -ErrorAction SilentlyContinue) + + $result | Should -BeNullOrEmpty + $auditErrors | Should -HaveCount 1 + $auditErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*' + $auditErrors[0].CategoryInfo.Category | Should -Be 'InvalidData' + $auditErrors[0].Exception.Message | Should -BeLike "*doesn't contain the audit entries*" + } + + It 'Should give a null AuditInheritanceEnabled and the right AccessInheritanceEnabled for Get-NTFSInheritance -SecurityDescriptor (Get-Acl ...)' { + $sd = Get-Acl -LiteralPath $plainFile + + $result = Get-NTFSInheritance -SecurityDescriptor $sd -ErrorAction Stop + + $result.AuditInheritanceEnabled | Should -Be $null + $result.AccessInheritanceEnabled | Should -Be (-not $sd.AreAccessRulesProtected) + } + + It 'Should write a non-terminating error naming the missing item for Set-NTFSSecurityDescriptor -SecurityDescriptor (Get-Acl ...), and change nothing' { + $sd = Get-Acl -LiteralPath $plainFile + $sd.AddAccessRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList ( + $sid, [System.Security.AccessControl.FileSystemRights]::WriteData, + [System.Security.AccessControl.AccessControlType]::Allow + ))) + $before = (Get-Acl -LiteralPath $plainFile).Sddl + + $null = Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorVariable sdErrors -ErrorAction SilentlyContinue + + $sdErrors | Should -HaveCount 1 + $sdErrors[0].FullyQualifiedErrorId | Should -BeLike 'WriteSdError,*' + $sdErrors[0].Exception.Message | Should -BeLike '*no item*' + (Get-Acl -LiteralPath $plainFile).Sddl | Should -BeExactly $before + } +} + +Describe 'RemoveFileSystemAccessRuleAll and RemoveFileSystemAuditRuleAll honor the account list' { + BeforeAll { + $allow = [System.Security.AccessControl.AccessControlType]::Allow + $success = [System.Security.AccessControl.AuditFlags]::Success + $accountOne = $identity + $accountTwo = [Security2.IdentityReference2] 'S-1-5-32-545' + $accountThree = [Security2.IdentityReference2] 'S-1-5-32-544' + $accountsByLabel = @{ One = $accountOne; Two = $accountTwo; Three = $accountThree } + + function New-AccessSecurityFor { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds an in-memory descriptor only.' + )] + param ([Security2.IdentityReference2[]] $Accounts) + + $raw = New-Object -TypeName 'System.Security.AccessControl.FileSecurity' + foreach ($account in $Accounts) { + $raw.AddAccessRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList ( + [System.Security.Principal.SecurityIdentifier] $account, + [System.Security.AccessControl.FileSystemRights]::ReadData, $allow + ))) + } + $raw + } + + function New-AuditSecurityFor { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds an in-memory descriptor only.' + )] + param ([Security2.IdentityReference2[]] $Accounts) + + $raw = New-Object -TypeName 'System.Security.AccessControl.FileSecurity' + foreach ($account in $Accounts) { + $raw.AddAuditRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAuditRule' -ArgumentList ( + [System.Security.Principal.SecurityIdentifier] $account, + [System.Security.AccessControl.FileSystemRights]::ReadData, $success + ))) + } + $raw + } + + function New-IdentityList { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Test helper that only creates a list.' + )] + param ([Security2.IdentityReference2[]] $Accounts) + + $list = New-Object -TypeName 'System.Collections.Generic.List[Security2.IdentityReference2]' + foreach ($account in $Accounts) { + $list.Add($account) + } + , $list + } + + function Get-AccessSddl { + param ([System.Security.AccessControl.FileSecurity] $Raw) + + $Raw.GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::Access) + } + + function Get-AuditSddl { + param ([System.Security.AccessControl.FileSecurity] $Raw) + + $Raw.GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::Audit) + } + + # Builds the expected result by removing the given accounts with the .NET method directly, instead of with + # a freshly built descriptor that only holds the remaining accounts: once an entry was added and removed + # again, the SACL stays present but empty, while a SACL that never held an entry is absent, and the two give + # a different SDDL for the same set of remaining entries. + function Get-ExpectedAccessSddl { + param ([Security2.IdentityReference2[]] $AccountsToRemove) + + $raw = New-AccessSecurityFor -Accounts $accountOne, $accountTwo, $accountThree + foreach ($account in $AccountsToRemove) { + $raw.RemoveAccessRuleSpecific((New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList ( + [System.Security.Principal.SecurityIdentifier] $account, + [System.Security.AccessControl.FileSystemRights]::ReadData, $allow + ))) + } + Get-AccessSddl $raw + } + + function Get-ExpectedAuditSddl { + param ([Security2.IdentityReference2[]] $AccountsToRemove) + + $raw = New-AuditSecurityFor -Accounts $accountOne, $accountTwo, $accountThree + foreach ($account in $AccountsToRemove) { + $raw.RemoveAuditRuleSpecific((New-Object -TypeName 'System.Security.AccessControl.FileSystemAuditRule' -ArgumentList ( + [System.Security.Principal.SecurityIdentifier] $account, + [System.Security.AccessControl.FileSystemRights]::ReadData, $success + ))) + } + Get-AuditSddl $raw + } + } + + It 'Should honor the account list when it removes access entries from a descriptor with three accounts, in memory, for ' -ForEach @( + @{ Scenario = 'a list with one account'; AccountsToRemove = @('One') } + @{ Scenario = 'a list with two accounts'; AccountsToRemove = @('One', 'Two') } + @{ Scenario = 'a null list'; AccountsToRemove = $null } + @{ Scenario = 'an empty list'; AccountsToRemove = @() } + ) { + $raw = New-AccessSecurityFor -Accounts $accountOne, $accountTwo, $accountThree + $toRemove = if ($null -eq $AccountsToRemove) { $accountOne, $accountTwo, $accountThree } else { $AccountsToRemove.ForEach({ $accountsByLabel[$_] }) } + $expected = Get-ExpectedAccessSddl -AccountsToRemove $toRemove + $accounts = if ($null -eq $AccountsToRemove) { $null } else { New-IdentityList -Accounts $AccountsToRemove.ForEach({ $accountsByLabel[$_] }) } + + [Security2.FileSystemAccessRule2]::RemoveFileSystemAccessRuleAll(([Security2.FileSystemSecurity2] $raw), $accounts) + + Get-AccessSddl $raw | Should -BeExactly $expected + } + + It 'Should honor the account list when it removes audit entries from a descriptor with three accounts, in memory, for ' -ForEach @( + @{ Scenario = 'a list with one account'; AccountsToRemove = @('One') } + @{ Scenario = 'a list with two accounts'; AccountsToRemove = @('One', 'Two') } + @{ Scenario = 'a null list'; AccountsToRemove = $null } + @{ Scenario = 'an empty list'; AccountsToRemove = @() } + ) { + $raw = New-AuditSecurityFor -Accounts $accountOne, $accountTwo, $accountThree + $toRemove = if ($null -eq $AccountsToRemove) { $accountOne, $accountTwo, $accountThree } else { $AccountsToRemove.ForEach({ $accountsByLabel[$_] }) } + $expected = Get-ExpectedAuditSddl -AccountsToRemove $toRemove + $accounts = if ($null -eq $AccountsToRemove) { $null } else { New-IdentityList -Accounts $AccountsToRemove.ForEach({ $accountsByLabel[$_] }) } + + [Security2.FileSystemAuditRule2]::RemoveFileSystemAuditRuleAll(([Security2.FileSystemSecurity2] $raw), $accounts) + + Get-AuditSddl $raw | Should -BeExactly $expected + } + + It 'Should remove only the listed account''s explicit access entries of a sandbox item, read back with .NET' { + $path = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveAllAccessByItem' + $item = New-Object -TypeName 'Alphaleonis.Win32.Filesystem.FileInfo' -ArgumentList $path + foreach ($account in $accountOne, $accountTwo, $accountThree) { + [void] [Security2.FileSystemAccessRule2]::AddFileSystemAccessRule( + $item, $account, [Security2.FileSystemRights2]::ReadData, $allow, + [System.Security.AccessControl.InheritanceFlags]::None, [System.Security.AccessControl.PropagationFlags]::None + ) + } + + [Security2.FileSystemAccessRule2]::RemoveFileSystemAccessRuleAll($item, (New-IdentityList -Accounts $accountOne)) + + $remaining = @((Get-Acl -LiteralPath $path).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier])) + @($remaining.IdentityReference.Value | Sort-Object) | Should -Be @(($accountTwo.Sid, $accountThree.Sid) | Sort-Object) + } + + It 'Should remove only the listed account''s explicit audit entries of a sandbox item, read back with .NET' -Skip:(-not $holdsSecurityPrivilege) { + $path = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveAllAuditByItem' + $item = New-Object -TypeName 'Alphaleonis.Win32.Filesystem.FileInfo' -ArgumentList $path + foreach ($account in $accountOne, $accountTwo, $accountThree) { + [void] [Security2.FileSystemAuditRule2]::AddFileSystemAuditRule( + $item, $account, [Security2.FileSystemRights2]::ReadData, $success, + [System.Security.AccessControl.InheritanceFlags]::None, [System.Security.AccessControl.PropagationFlags]::None + ) + } + + [Security2.FileSystemAuditRule2]::RemoveFileSystemAuditRuleAll($item, (New-IdentityList -Accounts $accountOne)) + + $remaining = @((Get-Acl -LiteralPath $path -Audit).GetAuditRules($true, $false, [System.Security.Principal.SecurityIdentifier])) + @($remaining.IdentityReference.Value | Sort-Object) | Should -Be @(($accountTwo.Sid, $accountThree.Sid) | Sort-Object) + } +} + Describe 'Generic access rights and identity errors' { It 'Should map the generic mask to the file system rights ' -ForEach @( @{ Mask = '80000000'; Expected = '00120089' } diff --git a/Tests/Owner.Tests.ps1 b/Tests/Owner.Tests.ps1 index 324f749..d923a59 100644 --- a/Tests/Owner.Tests.ps1 +++ b/Tests/Owner.Tests.ps1 @@ -237,6 +237,24 @@ Describe 'Set-NTFSOwner' { ($result.FullName -join '|') | Should -Be ($files -join '|') } + # Each piped object names its own owner; the owners are accounts that only the Restore privilege can assign, so + # that the result differs from the owner that the items have already. + It 'Should bind -Account by property name and give each piped item the owner that its own object names' -Skip:(-not $canAssignAnyOwner) { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'SetOwnerAccountByPropertyFirst' + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'SetOwnerAccountByPropertySecond' + $administrators = 'S-1-5-32-544' + $requests = @( + [pscustomobject]@{ Path = $first; Account = $trustedInstaller } + [pscustomobject]@{ Path = $second; Account = $administrators } + ) + + $result = @($requests | Set-NTFSOwner -PassThru -ErrorAction Stop) + + $result | Should -HaveCount 2 + Get-TestOwner -Path $first | Should -Be $trustedInstaller + Get-TestOwner -Path $second | Should -Be $administrators + } + It 'Should set an owner that only the Restore privilege allows' -Skip:(-not $canAssignAnyOwner) { $file = New-TestSandboxItem -Sandbox $sandbox -Name 'SetOwnerRestore' diff --git a/Tests/ParameterSets.Access.Tests.ps1 b/Tests/ParameterSets.Access.Tests.ps1 index 551cc3b..d4e9a20 100644 --- a/Tests/ParameterSets.Access.Tests.ps1 +++ b/Tests/ParameterSets.Access.Tests.ps1 @@ -4,8 +4,7 @@ found untested, with the module built in NTFSSecurity\bin\Release on files in a sandbox folder. Each test closes a gap the matrix recorded for the row it names (state, error, pipeline, continuation, or output); it does not repeat what Access.Tests.ps1, PathErrors.Tests.ps1, PermissionScopes.Tests.ps1, SecurityDescriptorSets.Tests.ps1, or - PipelineControl.Tests.ps1 already assert. The -SecurityDescriptor sets of Add-NTFSAccess and Remove-NTFSAccess have - no per-item error handler (an open item for the maintainer); no test here makes one of those two sets fail. + PipelineControl.Tests.ps1 already assert. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.' @@ -201,9 +200,10 @@ Describe 'Add-NTFSAccess' { $scopes -join '|' | Should -BeExactly 'None/None|ContainerInherit/None|ContainerInherit, ObjectInherit/InheritOnly' } - # A blank cell that Import-Csv reads is an empty string: not $null, and not a scope name. The binder cannot convert it, so - # PathSimple does not bind, and the row gets the default scope, as an object without the property does. - It 'Should give the row of a CSV file whose AppliesTo cell is blank the default scope, and the other rows their own' { + # A blank cell that Import-Csv reads is an empty string: not $null, and not a scope name. Since -AppliesTo is + # now an object parameter (Decision 27, fix B), the blank string still binds it, which selects PathSimple, + # so the row gets an InvalidParameterValue error instead of the default scope; the other rows are unaffected. + It 'Should write an InvalidParameterValue error for the row of a CSV file whose AppliesTo cell is blank, leave its folder alone, and still give the other rows their own scope' { $folders = @('AddCsvBlankFirst', 'AddCsvBlankCell', 'AddCsvBlankLast' | ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory }) $csv = Join-Path -Path $sandbox -ChildPath 'AddCsvBlank.csv' @@ -215,10 +215,15 @@ Describe 'Add-NTFSAccess' { ('"{0}",S-1-5-32-546,ReadData,SubfoldersAndFilesOnly' -f $folders[2]) ) - Import-Csv -LiteralPath $csv | Add-NTFSAccess -ErrorAction Stop + Import-Csv -LiteralPath $csv | Add-NTFSAccess -ErrorVariable addErrors -ErrorAction SilentlyContinue + $addErrors | Should -HaveCount 1 + $addErrors[0].FullyQualifiedErrorId | Should -BeLike 'InvalidParameterValue,*' + $addErrors[0].CategoryInfo.Category | Should -Be 'InvalidArgument' + $addErrors[0].TargetObject | Should -Be $folders[1] + $addErrors[0].Exception.Message | Should -Match 'AppliesTo' $scopes = $folders | ForEach-Object -Process { Get-TestAccountScope -Acl (Get-Acl -LiteralPath $_) -Account 'S-1-5-32-546' } - $scopes -join '|' | Should -BeExactly 'None/None|ContainerInherit, ObjectInherit/None|ContainerInherit, ObjectInherit/InheritOnly' + $scopes -join '|' | Should -BeExactly 'None/None||ContainerInherit, ObjectInherit/InheritOnly' } # $null cannot be bound at all (ConvertFrom-Csv makes it of a trailing blank cell): the object fails, loudly, and the cmdlet goes @@ -412,6 +417,57 @@ Describe 'Add-NTFSAccess' { (Get-Acl -LiteralPath $second).Sddl | Should -BeExactly $before[1] } } + + # Item 5 of Decision 27: a failing change to one of several descriptors (a Deny entry without rights, which .NET + # refuses to build, the same as Access.Tests.ps1:774 pins for -Path) reports an error for that descriptor and goes + # on with the next one, instead of ending the cmdlet with a terminating error. + Context 'A failing change to one of several descriptors' { + It 'Should write an AddAceError for the descriptor whose change fails in the SDSimple set, continue with the next, and return -PassThru only for the one that succeeded' { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorSimpleFirst' + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorSimpleSecond' + $descriptors = @(Get-NTFSSecurityDescriptor -Path $first, $second -ErrorAction Stop) + $before = $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') + + $rows = @( + [pscustomobject]@{ SecurityDescriptor = $descriptors[0]; Account = 'S-1-1-0'; AccessRights = 'None'; AccessType = 'Deny'; AppliesTo = 'ThisFolderOnly' } + [pscustomobject]@{ SecurityDescriptor = $descriptors[1]; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = 'Deny'; AppliesTo = 'ThisFolderOnly' } + ) + + $result = @($rows | Add-NTFSAccess -PassThru -ErrorVariable sdErrors -ErrorAction SilentlyContinue) + + $sdErrors | Should -HaveCount 1 + $sdErrors[0].FullyQualifiedErrorId | Should -BeLike 'AddAceError,*' + $sdErrors[0].CategoryInfo.Category | Should -Be 'WriteError' + $sdErrors[0].TargetObject | Should -Be $descriptors[0] + $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') | Should -BeExactly $before + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $second } + @($result | Where-Object -FilterScript { $_.Account.Sid -eq 'S-1-1-0' -and $_.AccessControlType -eq 'Deny' }) | Should -HaveCount 1 + } + + It 'Should write an AddAceError for the descriptor whose change fails in the SDComplex set, continue with the next, and return -PassThru only for the one that succeeded' { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorComplexFirst' + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorComplexSecond' + $descriptors = @(Get-NTFSSecurityDescriptor -Path $first, $second -ErrorAction Stop) + $before = $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') + + $rows = @( + [pscustomobject]@{ SecurityDescriptor = $descriptors[0]; Account = 'S-1-1-0'; AccessRights = 'None'; AccessType = 'Deny' } + [pscustomobject]@{ SecurityDescriptor = $descriptors[1]; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = 'Deny' } + ) + + $result = @($rows | Add-NTFSAccess -PassThru -ErrorVariable sdErrors -ErrorAction SilentlyContinue) + + $sdErrors | Should -HaveCount 1 + $sdErrors[0].FullyQualifiedErrorId | Should -BeLike 'AddAceError,*' + $sdErrors[0].CategoryInfo.Category | Should -Be 'WriteError' + $sdErrors[0].TargetObject | Should -Be $descriptors[0] + $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') | Should -BeExactly $before + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $second } + @($result | Where-Object -FilterScript { $_.Account.Sid -eq 'S-1-1-0' -and $_.AccessControlType -eq 'Deny' }) | Should -HaveCount 1 + } + } } Describe 'Remove-NTFSAccess' { @@ -730,6 +786,59 @@ Describe 'Remove-NTFSAccess' { } } } + + # Item 5 of Decision 27: a failing change to one of several descriptors (a Deny entry without rights, which .NET + # refuses to build, the same as Access.Tests.ps1:571 pins for -Path) reports an error for that descriptor and goes + # on with the next one, instead of ending the cmdlet with a terminating error. + Context 'A failing change to one of several descriptors' { + It 'Should write a RemoveAceError for the descriptor whose change fails in the SDSimple set, continue with the next, and return -PassThru only for the one that succeeded' { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorSimpleFirst' + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorSimpleSecond' + Add-NTFSAccess -Path $first, $second -Account 'S-1-1-0' -AccessRights ReadData -AccessType Deny -AppliesTo ThisFolderOnly -ErrorAction Stop + $descriptors = @(Get-NTFSSecurityDescriptor -Path $first, $second -ErrorAction Stop) + $before = $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') + + $rows = @( + [pscustomobject]@{ SecurityDescriptor = $descriptors[0]; Account = 'S-1-1-0'; AccessRights = 'None'; AccessType = 'Deny'; AppliesTo = 'ThisFolderOnly' } + [pscustomobject]@{ SecurityDescriptor = $descriptors[1]; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = 'Deny'; AppliesTo = 'ThisFolderOnly' } + ) + + $result = @($rows | Remove-NTFSAccess -PassThru -ErrorVariable sdErrors -ErrorAction SilentlyContinue) + + $sdErrors | Should -HaveCount 1 + $sdErrors[0].FullyQualifiedErrorId | Should -BeLike 'RemoveAceError,*' + $sdErrors[0].CategoryInfo.Category | Should -Be 'WriteError' + $sdErrors[0].TargetObject | Should -Be $descriptors[0] + $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') | Should -BeExactly $before + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $second } + @($result | Where-Object -FilterScript { $_.Account.Sid -eq 'S-1-1-0' }) | Should -BeNullOrEmpty + } + + It 'Should write a RemoveAceError for the descriptor whose change fails in the SDComplex set, continue with the next, and return -PassThru only for the one that succeeded' { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorComplexFirst' + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorComplexSecond' + Add-NTFSAccess -Path $first, $second -Account 'S-1-1-0' -AccessRights ReadData -AccessType Deny -ErrorAction Stop + $descriptors = @(Get-NTFSSecurityDescriptor -Path $first, $second -ErrorAction Stop) + $before = $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') + + $rows = @( + [pscustomobject]@{ SecurityDescriptor = $descriptors[0]; Account = 'S-1-1-0'; AccessRights = 'None'; AccessType = 'Deny' } + [pscustomobject]@{ SecurityDescriptor = $descriptors[1]; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = 'Deny' } + ) + + $result = @($rows | Remove-NTFSAccess -PassThru -ErrorVariable sdErrors -ErrorAction SilentlyContinue) + + $sdErrors | Should -HaveCount 1 + $sdErrors[0].FullyQualifiedErrorId | Should -BeLike 'RemoveAceError,*' + $sdErrors[0].CategoryInfo.Category | Should -Be 'WriteError' + $sdErrors[0].TargetObject | Should -Be $descriptors[0] + $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') | Should -BeExactly $before + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $second } + @($result | Where-Object -FilterScript { $_.Account.Sid -eq 'S-1-1-0' }) | Should -BeNullOrEmpty + } + } } Describe 'Clear-NTFSAccess' { @@ -911,6 +1020,37 @@ Describe 'Get-NTFSAccess' { @($piped | Where-Object -FilterScript { $_.FullName -eq $file2 }) | Should -HaveCount $expectedSecond } } + + Context 'Path parameter set, unreadable folder' { + BeforeAll { + # Forcing EnablePrivileges off reproduces the same denial as a basic user for an elevated session, as + # PathErrors.Tests.ps1 does for the same scenario, and as the Get-NTFSOrphanedAccess test below does. + $privateData['EnablePrivileges'] = $false + } + + AfterAll { + $privateData['EnablePrivileges'] = $enablePrivileges + } + + # Get-NTFSAccess, Get-NTFSAudit, Get-NTFSEffectiveAccess, Get-NTFSInheritance, and Get-NTFSSecurityDescriptor + # all report a failed ownership retry as ReadSecurityError categorized as WriteError (GetAccess.cs); this is + # the module's convention. A characterization test: the behavior is already correct, only not yet pinned for + # this cmdlet (6a). Unlike Get-NTFSOrphanedAccess, whose own category for the same failure is ReadError. + It 'Should write a ReadSecurityError categorized as WriteError after a failed ownership retry, and still return the next path''s entries' { + $blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAccessUnreadable' + Block-TestReadPermission -Sandbox $sandbox -Path $blocked + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAccessUnreadableNext' + Add-NTFSAccess -Path $next -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop + + $result = @(Get-NTFSAccess -Path $blocked, $next -ErrorVariable accessErrors -ErrorAction SilentlyContinue) + + $accessErrors | Should -HaveCount 1 + $accessErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*' + $accessErrors[0].CategoryInfo.Category | Should -Be 'WriteError' + $accessErrors[0].TargetObject | Should -Be $blocked + @($result | Where-Object -FilterScript { $_.FullName -eq $next -and $_.Account.Sid -eq 'S-1-1-0' -and -not $_.IsInherited }) | Should -HaveCount 1 + } + } } Describe 'Get-NTFSOrphanedAccess' { @@ -1192,6 +1332,796 @@ Describe 'Get-NTFSEffectiveAccess' { } } +# Decision 27 (fix B): AccessType, InheritanceFlags, PropagationFlags, and AppliesTo of Add-NTFSAccess and +# Remove-NTFSAccess are object parameters, converted in the cmdlet with LanguagePrimitives.ConvertTo, so that a +# piped value the binder cannot convert to the enumeration is an error instead of a silently kept default. +Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationFlags, and AppliesTo' { + BeforeDiscovery { + # A collection nested 40 levels deep is a list too: the conversion opens at most 32 levels, which also ends a + # collection that holds itself (a test with such a collection would not end if the limit were lost). + $accessDeepAccessType = 'Deny' + $accessDeepScope = 'FilesOnly' + foreach ($level in 1..40) { + $accessDeepAccessType = , $accessDeepAccessType + $accessDeepScope = , $accessDeepScope + } + + # The message of the error names the value, and building it must not fail when the text of an element does. + $accessThrowingElement = [pscustomobject]@{ Unrelated = 1 } + $accessThrowingElement | Add-Member -MemberType ScriptMethod -Name ToString -Value { throw 'ToString throws' } -Force + + $accessBadShapes = @( + @{ Shape = 'a misspelled name'; Value = 'Typoo'; ValueText = 'Typoo' } + @{ Shape = 'a blank string'; Value = ''; ValueText = $null } + @{ Shape = 'a white-space string'; Value = ' '; ValueText = $null } + @{ Shape = 'an undefined number'; Value = 99; ValueText = '99' } + @{ Shape = 'a hashtable'; Value = @{ Unrelated = 1 }; ValueText = $null } + @{ Shape = 'an array of one misspelled name'; Value = @('Typoo'); ValueText = 'Typoo' } + @{ Shape = 'an array of one object whose ToString throws'; Value = @($accessThrowingElement); ValueText = $null } + ) + $accessTypeBadShapes = @( + @{ Shape = 'a misspelled name'; Value = 'Denyy'; ValueText = 'Denyy' } + @{ Shape = 'a blank string'; Value = ''; ValueText = $null } + @{ Shape = 'a white-space string'; Value = ' '; ValueText = $null } + @{ Shape = 'an undefined number'; Value = 99; ValueText = '99' } + @{ Shape = 'a hashtable'; Value = @{ Unrelated = 1 }; ValueText = $null } + @{ Shape = 'an array of one misspelled name'; Value = @('Denyy'); ValueText = 'Denyy' } + @{ Shape = 'an array of one object whose ToString throws'; Value = @($accessThrowingElement); ValueText = $null } + @{ Shape = 'a comma list of two names'; Value = 'Allow,Deny'; ValueText = 'Allow,Deny' } + @{ Shape = 'an array of two names'; Value = @('Allow', 'Deny'); ValueText = 'Allow, Deny' } + @{ Shape = 'an array of one comma list'; Value = @('Allow,Deny'); ValueText = 'Allow,Deny' } + @{ Shape = 'an array that holds an array of two names'; Value = , @('Allow', 'Deny'); ValueText = 'Allow, Deny' } + @{ Shape = 'a hash set of two names'; Value = [System.Collections.Generic.HashSet[string]]::new([string[]]@('Allow', 'Deny')); ValueText = $null } + @{ Shape = 'the keys of a hashtable'; Value = (@{ Allow = 1; Deny = 2 }).Keys; ValueText = $null } + @{ Shape = 'an array nested 40 levels deep'; Value = $accessDeepAccessType; ValueText = $null } + ) + + # AccessType and AppliesTo name one value each, not a combination: PowerShell combines the numbers of a list bit by + # bit, so that one of the values wins silently or, for AppliesTo, a scope that the caller did not name is chosen. + # Every kind of collection counts, also a collection of one element that holds a list. + $accessAppliesToListShapes = @( + @{ Shape = 'a comma list of two names'; Value = 'ThisFolderAndSubfolders,ThisFolderAndFiles'; ValueText = 'ThisFolderAndSubfolders,ThisFolderAndFiles' } + @{ Shape = 'an array of two names'; Value = @('ThisFolderAndSubfolders', 'ThisFolderAndFiles'); ValueText = 'ThisFolderAndSubfolders, ThisFolderAndFiles' } + @{ Shape = 'an array of one comma list'; Value = @('ThisFolderAndSubfolders,ThisFolderAndFiles'); ValueText = 'ThisFolderAndSubfolders,ThisFolderAndFiles' } + @{ Shape = 'a hash set of two names'; Value = [System.Collections.Generic.HashSet[string]]::new([string[]]@('ThisFolderAndSubfolders', 'ThisFolderAndFiles')); ValueText = $null } + @{ Shape = 'the keys of a hashtable'; Value = (@{ ThisFolderAndSubfolders = 1; ThisFolderAndFiles = 2 }).Keys; ValueText = $null } + @{ Shape = 'an array nested 40 levels deep'; Value = $accessDeepScope; ValueText = $null } + ) + + # The cross product of the two cmdlets that share these parameters and every bad shape: 2 * (14 + 3 * 7 + 6) = 82 rows. + $accessInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { + foreach ($shapeCase in $accessTypeBadShapes) { + @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Shape = $shapeCase.Shape; Value = $shapeCase.Value; ValueText = $shapeCase.ValueText } + } + foreach ($parameterName in 'InheritanceFlags', 'PropagationFlags', 'AppliesTo') { + foreach ($shapeCase in $accessBadShapes) { + @{ Cmdlet = $cmdlet; Parameter = $parameterName; Shape = $shapeCase.Shape; Value = $shapeCase.Value; ValueText = $shapeCase.ValueText } + } + } + foreach ($shapeCase in $accessAppliesToListShapes) { + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Shape = $shapeCase.Shape; Value = $shapeCase.Value; ValueText = $shapeCase.ValueText } + } + } + + # The message names a list by its first ten elements: ten are named in full, eleven end with an ellipsis. The + # elements of a flags parameter are converted one by one, so these numbers fail there too (4 is not a flag). A + # value whose text cannot be built is named by its type, in the short form that Type.ToString gives. + $accessInvalidParameterCases += @( + @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'AccessType'; Shape = 'ten numbers'; Value = 1..10; ValueText = '1, 2, 3, 4, 5, 6, 7, 8, 9, 10' } + @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'AccessType'; Shape = 'eleven numbers'; Value = 1..11; ValueText = '1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...' } + @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'InheritanceFlags'; Shape = 'ten numbers'; Value = 1..10; ValueText = '1, 2, 3, 4, 5, 6, 7, 8, 9, 10' } + @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'InheritanceFlags'; Shape = 'eleven numbers'; Value = 1..11; ValueText = '1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...' } + @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'AccessType'; Shape = 'a generic list of one object whose ToString throws'; Value = [System.Collections.Generic.List[object]]::new([object[]] @($accessThrowingElement)); ValueText = 'System.Collections.Generic.List`1[System.Object]' } + @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'InheritanceFlags'; Shape = 'a generic list of one object whose ToString throws'; Value = [System.Collections.Generic.List[object]]::new([object[]] @($accessThrowingElement)); ValueText = 'System.Collections.Generic.List`1[System.Object]' } + ) + + $accessSdInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { + @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Value = 'Denyy'; ValueText = 'Denyy' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'Typoo'; ValueText = 'Typoo' } + } + + $accessNoCarryOverCases = @( + @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'AccessType'; Value = 'Denyy' } + @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'InheritanceFlags'; Value = 'Typoo' } + @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'PropagationFlags'; Value = 'Typoo' } + @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'AppliesTo'; Value = 'Typoo' } + ) + + $accessNamedInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { + @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Value = 'Denyy' } + @{ Cmdlet = $cmdlet; Parameter = 'InheritanceFlags'; Value = 'Typoo' } + @{ Cmdlet = $cmdlet; Parameter = 'PropagationFlags'; Value = 'Typoo' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'Typoo' } + @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Value = 'Allow,Deny' } + @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Value = @('Allow', 'Deny') } + @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Value = @('Allow,Deny') } + @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Value = [System.Collections.Generic.HashSet[string]]::new([string[]]@('Allow', 'Deny')) } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'ThisFolderAndSubfolders,ThisFolderAndFiles' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = @('ThisFolderAndSubfolders', 'ThisFolderAndFiles') } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = @('ThisFolderAndSubfolders,ThisFolderAndFiles') } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = [System.Collections.Generic.HashSet[string]]::new([string[]]@('ThisFolderAndSubfolders', 'ThisFolderAndFiles')) } + @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Value = $accessDeepAccessType } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = $accessDeepScope } + @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Value = @($accessThrowingElement) } + @{ Cmdlet = $cmdlet; Parameter = 'InheritanceFlags'; Value = @($accessThrowingElement) } + } + + # A named $null (a literal $null, a variable, an alias, splatting, or $PSDefaultParameterValues) must + # stay a terminating error, the same as a bad named value; only a $null that a piped object's property + # carries keeps the parameter's own default. + $accessNamedNullCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { + @{ Cmdlet = $cmdlet; Parameter = 'AccessType' } + @{ Cmdlet = $cmdlet; Parameter = 'InheritanceFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'PropagationFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo' } + } + + $accessValidAccessTypeShapeCases = @( + @{ Description = 'the exact name'; Value = 'Deny' } + @{ Description = 'a different case'; Value = 'deny' } + @{ Description = 'the enum value'; Value = [System.Security.AccessControl.AccessControlType]::Deny } + @{ Description = 'an integer'; Value = 1 } + @{ Description = 'an array of one name'; Value = @('Deny') } + @{ Description = 'a list of one name'; Value = [System.Collections.Generic.List[string]]::new([string[]]@('Deny')) } + @{ Description = 'a hash set of one name'; Value = [System.Collections.Generic.HashSet[string]]::new([string[]]@('Deny')) } + @{ Description = 'an array that holds an array of one name'; Value = , @('Deny') } + ) + + $accessValidFlagsShapeCases = @( + @{ Description = 'a comma list without spaces'; Value = 'ContainerInherit,ObjectInherit' } + @{ Description = 'a comma list with spaces'; Value = 'ContainerInherit, ObjectInherit' } + @{ Description = 'the enum value'; Value = [System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit' } + ) + + $accessAmbiguousValueCases = @( + @{ AppliesToShape = 'a string'; AppliesToIsEnum = $false; FlagShape = 'a string'; FlagIsEnum = $false } + @{ AppliesToShape = 'an enum value'; AppliesToIsEnum = $true; FlagShape = 'a string'; FlagIsEnum = $false } + @{ AppliesToShape = 'a string'; AppliesToIsEnum = $false; FlagShape = 'an enum value'; FlagIsEnum = $true } + @{ AppliesToShape = 'an enum value'; AppliesToIsEnum = $true; FlagShape = 'an enum value'; FlagIsEnum = $true } + ) + + $accessCompleterCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { + @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; EnumTypeName = 'System.Security.AccessControl.AccessControlType' } + @{ Cmdlet = $cmdlet; Parameter = 'InheritanceFlags'; EnumTypeName = 'System.Security.AccessControl.InheritanceFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'PropagationFlags'; EnumTypeName = 'System.Security.AccessControl.PropagationFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; EnumTypeName = 'Security2.ApplyTo' } + } + + # A quoted or a wildcard word completed while these parameters were typed as enumerations, so the completers + # must offer the names for either shape too. Each ExpectedName has no sibling name that also starts with it + # (ThisFolderOnly has no ThisFolderOnlyOneLevel), so the unquoted and quoted forms match it alone; the + # wildcard only needs to contain it among the matches. + $accessCompletionFormCases = @( + @{ Parameter = 'AccessType'; Word = 'D'; ExpectedName = 'Deny' } + @{ Parameter = 'InheritanceFlags'; Word = 'Container'; ExpectedName = 'ContainerInherit' } + @{ Parameter = 'PropagationFlags'; Word = 'Inh'; ExpectedName = 'InheritOnly' } + @{ Parameter = 'AppliesTo'; Word = 'ThisFolderO'; ExpectedName = 'ThisFolderOnly' } + ) + + # In Turkish, a case-insensitive comparison of the culture of the thread treats I and i as different letters, so + # a word typed in capitals completes only when the completers compare culture invariant. + $accessTurkishCompletionCases = @( + @{ Parameter = 'AppliesTo'; Word = 'THISFOLDERO'; ExpectedName = 'ThisFolderOnly' } + @{ Parameter = 'InheritanceFlags'; Word = 'CONTAINERINH'; ExpectedName = 'ContainerInherit' } + @{ Parameter = 'PropagationFlags'; Word = 'NOPROPAGATEINHERI'; ExpectedName = 'NoPropagateInherit' } + ) + + # Because the five parameters are object, PowerShell counts a $null, an empty string, or DBNull value of a + # property as present when it chooses the parameter set, the same as a real value, so AppliesTo (the Simple + # group) together with a $null/''/DBNull InheritanceFlags or PropagationFlags (the Complex group) offers + # both groups and is AmbiguousParameterSet, not InvalidParameterValue. The reverse (AppliesTo blank, a + # Complex property valid) is AmbiguousParameterSet too, except that AppliesTo is Mandatory, so a literal + # $null of it (unlike '' or DBNull) is rejected earlier by the binder's own Mandatory check. Covers the + # Path and SD sets of the access cmdlets; the audit cmdlets are pinned in ParameterSets.Audit.Tests.ps1. + $accessAmbiguousOtherGroupCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { + foreach ($targetKind in 'Path', 'SD') { + foreach ($shape in 'a $null', 'an empty string', 'DBNull') { + $blankValue = switch ($shape) { 'a $null' { $null }; 'an empty string' { '' }; 'DBNull' { [System.DBNull]::Value } } + @{ + Cmdlet = $cmdlet; TargetKind = $targetKind; Shape = $shape; BlankValue = $blankValue + Direction = 'AppliesTo valid, InheritanceFlags blank'; AppliesToIsBlank = $false + ExpectedErrorId = 'AmbiguousParameterSet' + } + @{ + Cmdlet = $cmdlet; TargetKind = $targetKind; Shape = $shape; BlankValue = $blankValue + Direction = 'AppliesTo blank, PropagationFlags valid'; AppliesToIsBlank = $true + ExpectedErrorId = $(if ($shape -eq 'a $null') { 'ParameterArgumentValidationErrorNullNotAllowed' } else { 'AmbiguousParameterSet' }) + } + } + } + } + + # With the object type, a script block is a plain value (a typed parameter evaluates it once per piped + # object), so it fails the same way any other value of the wrong shape does: a terminating + # InvalidParameterValue from ValidateNamedValue in BeginProcessing, before any piped object is processed. + $accessScriptBlockCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { + @{ Cmdlet = $cmdlet; Parameter = 'AccessType' } + @{ Cmdlet = $cmdlet; Parameter = 'InheritanceFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'PropagationFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo' } + } + } + + Context 'A bad value bound from the pipeline' { + It 'Should write one InvalidParameterValue error for - (), change nothing, and still process the next object' -ForEach $accessInvalidParameterCases { + $path = New-TestSandboxItem -Sandbox $sandbox -Name 'InvalidValue' -Directory + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'InvalidValueNext' -Directory + $before = (Get-Acl -LiteralPath $path).Sddl + $extra = @{ } + $extra[$Parameter] = $Value + + $rows = @( + [pscustomobject] (@{ Path = $path; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $extra) + [pscustomobject]@{ Path = $next; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + ) + + $result = @($rows | & $Cmdlet -PassThru -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue) + + $pipelineErrors | Should -HaveCount 1 + $pipelineErrors[0].FullyQualifiedErrorId | Should -BeLike 'InvalidParameterValue,*' + $pipelineErrors[0].CategoryInfo.Category | Should -Be 'InvalidArgument' + $pipelineErrors[0].TargetObject | Should -Be $path + $pipelineErrors[0].Exception.Message | Should -Match ([regex]::Escape($Parameter)) + if ($ValueText) { + $pipelineErrors[0].Exception.Message | Should -Match ("The value '" + [regex]::Escape($ValueText) + "'") + } + (Get-Acl -LiteralPath $path).Sddl | Should -BeExactly $before + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $next } + } + + It 'Should write one InvalidParameterValue error per descriptor for - bound to a piped descriptor, and leave the descriptors unchanged' -ForEach $accessSdInvalidParameterCases { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'InvalidSdFirst' -Directory + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'InvalidSdSecond' -Directory + $descriptors = @(Get-NTFSSecurityDescriptor -Path $first, $second -ErrorAction Stop) + $before = $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') + $extra = @{ } + $extra[$Parameter] = $Value + + $rows = @( + [pscustomobject] (@{ SecurityDescriptor = $descriptors[0]; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $extra) + [pscustomobject]@{ SecurityDescriptor = $descriptors[1]; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + ) + + $result = @($rows | & $Cmdlet -PassThru -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue) + + $pipelineErrors | Should -HaveCount 1 + $pipelineErrors[0].FullyQualifiedErrorId | Should -BeLike 'InvalidParameterValue,*' + $pipelineErrors[0].CategoryInfo.Category | Should -Be 'InvalidArgument' + $pipelineErrors[0].TargetObject | Should -Be $descriptors[0] + $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') | Should -BeExactly $before + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $second } + } + } + + Context 'Continuation and no carry-over across piped objects' { + It 'Should apply both good objects when a bad AccessType value is between them, in the order of the rows' { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinueFirst' -Directory + $middle = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinueMiddle' -Directory + $last = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinueLast' -Directory + + $rows = @( + [pscustomobject]@{ Path = $first; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = 'Deny' } + [pscustomobject]@{ Path = $middle; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = 'Denyy' } + [pscustomobject]@{ Path = $last; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = 'Deny' } + ) + + $result = @($rows | Add-NTFSAccess -PassThru -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue) + + $pipelineErrors | Should -HaveCount 1 + $pipelineErrors[0].TargetObject | Should -Be $middle + $firstRule = @((Get-Acl -LiteralPath $first).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) + $firstRule | Should -HaveCount 1 + $firstRule[0].AccessControlType | Should -Be ([System.Security.AccessControl.AccessControlType]::Deny) + $middleRule = @((Get-Acl -LiteralPath $middle).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) + $middleRule | Should -BeNullOrEmpty + $lastRule = @((Get-Acl -LiteralPath $last).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) + $lastRule | Should -HaveCount 1 + $lastRule[0].AccessControlType | Should -Be ([System.Security.AccessControl.AccessControlType]::Deny) + # The rows are applied in order: both surviving entries show up, $middle's does not, and nothing reports twice. + @($result | Where-Object -FilterScript { $_.FullName -eq $first -and $_.Account.Sid -eq 'S-1-1-0' }) | Should -HaveCount 1 + @($result | Where-Object -FilterScript { $_.FullName -eq $last -and $_.Account.Sid -eq 'S-1-1-0' }) | Should -HaveCount 1 + @($result | Where-Object -FilterScript { $_.FullName -eq $middle -and $_.Account.Sid -eq 'S-1-1-0' }) | Should -BeNullOrEmpty + } + + It 'Should give the row without AccessType the default Allow after a preceding Deny row, with no carry-over' { + $denyFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'NoCarryOverDeny' -Directory + $defaultFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'NoCarryOverDefault' -Directory + + @( + [pscustomobject]@{ Path = $denyFolder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = 'Deny' } + [pscustomobject]@{ Path = $defaultFolder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + ) | Add-NTFSAccess -ErrorAction Stop + + $denyRule = @((Get-Acl -LiteralPath $denyFolder).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) + $denyRule | Should -HaveCount 1 + $denyRule[0].AccessControlType | Should -Be ([System.Security.AccessControl.AccessControlType]::Deny) + $defaultRule = @((Get-Acl -LiteralPath $defaultFolder).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) + $defaultRule | Should -HaveCount 1 + $defaultRule[0].AccessControlType | Should -Be ([System.Security.AccessControl.AccessControlType]::Allow) + } + + It 'Should give the row without its own default after a preceding row with an invalid value, with no carry-over' -ForEach $accessNoCarryOverCases { + $badFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'NoCarryOverBad' -Directory + $defaultFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'NoCarryOverAfterBad' -Directory + $extra = @{ } + $extra[$Parameter] = $Value + + @( + [pscustomobject] (@{ Path = $badFolder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $extra) + [pscustomobject]@{ Path = $defaultFolder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + ) | & $Cmdlet -ErrorAction SilentlyContinue + + $defaultRule = @((Get-Acl -LiteralPath $defaultFolder).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) + $defaultRule | Should -HaveCount 1 + $defaultRule[0].AccessControlType | Should -Be ([System.Security.AccessControl.AccessControlType]::Allow) + $defaultRule[0].InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit') + $defaultRule[0].PropagationFlags | Should -Be ([System.Security.AccessControl.PropagationFlags]::None) + } + } + + Context 'Valid values are unchanged for every shape' { + It 'Should add a Deny entry for AccessType given as ' -ForEach $accessValidAccessTypeShapeCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ValidAccessType' -Directory + + [pscustomobject]@{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = $Value } | Add-NTFSAccess -ErrorAction Stop + + $rule = @((Get-Acl -LiteralPath $folder).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) + $rule | Should -HaveCount 1 + $rule[0].AccessControlType | Should -Be ([System.Security.AccessControl.AccessControlType]::Deny) + } + + It 'Should keep the default AccessType Allow for a $null value' { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ValidAccessTypeNull' -Directory + + [pscustomobject]@{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = $null } | Add-NTFSAccess -ErrorAction Stop + + $rule = @((Get-Acl -LiteralPath $folder).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) + $rule | Should -HaveCount 1 + $rule[0].AccessControlType | Should -Be ([System.Security.AccessControl.AccessControlType]::Allow) + } + + It 'Should scope the entry with InheritanceFlags given as ' -ForEach $accessValidFlagsShapeCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ValidInheritanceFlags' -Directory + + [pscustomobject]@{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; InheritanceFlags = $Value; PropagationFlags = 'None' } | Add-NTFSAccess -ErrorAction Stop + + $rule = @((Get-Acl -LiteralPath $folder).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) + $rule | Should -HaveCount 1 + $rule[0].InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit') + } + + It 'Should keep the default flags for $null InheritanceFlags and PropagationFlags' { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ValidFlagsNull' -Directory + + [pscustomobject]@{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; InheritanceFlags = $null; PropagationFlags = $null } | Add-NTFSAccess -ErrorAction Stop + + $rule = @((Get-Acl -LiteralPath $folder).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) + $rule | Should -HaveCount 1 + $rule[0].InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit') + $rule[0].PropagationFlags | Should -Be ([System.Security.AccessControl.PropagationFlags]::None) + } + } + + Context 'A bad value named on the command line' { + It 'Should throw a terminating InvalidParameterValue error for -, catchable with try/catch, and change nothing' -ForEach $accessNamedInvalidParameterCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedInvalid' -Directory + $before = (Get-Acl -LiteralPath $folder).Sddl + $namedArguments = @{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $namedArguments[$Parameter] = $Value + + $caughtId = $null + try { + & $Cmdlet @namedArguments -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + } + + $caughtId | Should -BeLike 'InvalidParameterValue,*' + (Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before + } + + It 'Should stop before the first piped path when -AccessType is named and bad' { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedBadFirst' -Directory + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedBadSecond' -Directory + $before = @((Get-Acl -LiteralPath $first).Sddl, (Get-Acl -LiteralPath $second).Sddl) + + $caughtId = $null + try { + $first, $second | Add-NTFSAccess -Account 'S-1-1-0' -AccessRights ReadData -AccessType Denyy -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + } + + $caughtId | Should -BeLike 'InvalidParameterValue,*' + (Get-Acl -LiteralPath $first).Sddl | Should -BeExactly $before[0] + (Get-Acl -LiteralPath $second).Sddl | Should -BeExactly $before[1] + } + } + + # The conversion reads at most two elements of a sequence to know that it is a list, and the message names ten, so a + # sequence that is long (or has no end) is not read to its end. + Context 'A long sequence of values' { + It 'Should read only the first elements of a long sequence given as -AccessType, and name its first ten in the error' -ForEach @( + @{ Mode = 'named' } + @{ Mode = 'piped' } + ) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'LongSequence' -Directory + $before = (Get-Acl -LiteralPath $folder).Sddl + $counter = @{ Reads = 0 } + $count = { $counter.Reads++; 'Allow' }.GetNewClosure() + $sequence = [System.Linq.Enumerable]::Select([int[]] (1..100000), [Func[int, string]] $count) + + $message = $null + if ($Mode -eq 'named') { + try { + Add-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -AccessType $sequence -ErrorAction Stop + } + catch { + $message = $_.Exception.Message + } + } + else { + [pscustomobject]@{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = $sequence } | Add-NTFSAccess -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue + $message = @($pipelineErrors)[0].Exception.Message + } + + $message | Should -Match ('The value ''' + ('Allow, ' * 10) + '\.\.\.''') + $counter.Reads | Should -BeLessThan 50 + (Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before + } + } + + # A list is rejected when the conversion has read its second element. The loop that reads it then releases the + # enumerator, and an enumerator that fails there must not turn the list into a value that PowerShell converts. + Context 'A sequence whose enumerator fails when it is released' { + BeforeAll { + if (-not ('NtfsSecurityTests.SequenceWhoseEnumeratorFailsOnDispose' -as [type])) { + Add-Type -TypeDefinition @' +namespace NtfsSecurityTests +{ + public sealed class SequenceWhoseEnumeratorFailsOnDispose : System.Collections.IEnumerable + { + private readonly object[] items; + + public SequenceWhoseEnumeratorFailsOnDispose(params object[] items) + { + this.items = items; + } + + public System.Collections.IEnumerator GetEnumerator() + { + return new Enumerator(this.items); + } + + private sealed class Enumerator : System.Collections.IEnumerator, System.IDisposable + { + private readonly object[] items; + private int index = -1; + + public Enumerator(object[] items) + { + this.items = items; + } + + public object Current + { + get { return this.items[this.index]; } + } + + public bool MoveNext() + { + this.index++; + return this.index < this.items.Length; + } + + public void Reset() + { + this.index = -1; + } + + public void Dispose() + { + throw new System.InvalidOperationException("The enumerator fails when it is released."); + } + } + } +} +'@ + } + } + + It 'Should reject for given as , and change nothing' -ForEach @( + foreach ($mode in 'named', 'piped') { + @{ Parameter = 'AccessType'; Mode = $mode; Shape = 'a sequence of two values'; Values = @('Allow', 'Deny') } + @{ Parameter = 'AccessType'; Mode = $mode; Shape = 'a sequence of one comma list'; Values = @('Allow,Deny') } + @{ Parameter = 'AccessType'; Mode = $mode; Shape = 'a sequence of one array of two values'; Values = , @('Allow', 'Deny') } + @{ Parameter = 'AppliesTo'; Mode = $mode; Shape = 'a sequence of two values'; Values = @('SubfoldersOnly', 'FilesOnly') } + @{ Parameter = 'AppliesTo'; Mode = $mode; Shape = 'a sequence of one comma list'; Values = @('ThisFolderOnly,FilesOnly') } + @{ Parameter = 'AppliesTo'; Mode = $mode; Shape = 'a sequence of one array of two values'; Values = , @('SubfoldersOnly', 'FilesOnly') } + } + ) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ReleaseFails' -Directory + $before = (Get-Acl -LiteralPath $folder).Sddl + $sequence = [NtfsSecurityTests.SequenceWhoseEnumeratorFailsOnDispose]::new([object[]] $Values) + + $caughtId = $null + if ($Mode -eq 'named') { + $namedArguments = @{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $namedArguments[$Parameter] = $sequence + try { + Add-NTFSAccess @namedArguments -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + } + } + else { + $properties = @{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $properties[$Parameter] = $sequence + [pscustomobject] $properties | Add-NTFSAccess -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue + $caughtId = @($pipelineErrors)[0].FullyQualifiedErrorId + } + + $caughtId | Should -BeLike 'InvalidParameterValue,*' + (Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before + } + + # What was read is what counts: a failing release does not turn one valid value into an error. + It 'Should still accept a sequence of one valid value for -AccessType given as ' -ForEach @( + @{ Mode = 'named' } + @{ Mode = 'piped' } + ) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ReleaseFailsValid' -Directory + $sequence = [NtfsSecurityTests.SequenceWhoseEnumeratorFailsOnDispose]::new([object[]] @('Deny')) + + if ($Mode -eq 'named') { + Add-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -AccessType $sequence -ErrorAction Stop + } + else { + [pscustomobject]@{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = $sequence } | Add-NTFSAccess -ErrorAction Stop + } + + $rule = @((Get-Acl -LiteralPath $folder).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) + $rule | Should -HaveCount 1 + $rule[0].AccessControlType | Should -Be ([System.Security.AccessControl.AccessControlType]::Deny) + } + } + + # A typed parameter ends a named $null (for example a variable that misspells a property, such as -AccessType + # $rule.AccessTpye) with a terminating ParameterBindingFailed; the object-typed parameter accepts it, so the + # cmdlet must not keep the default silently. A named $null is reported the same way as a bad named value. + Context 'A named $null is a terminating error' { + It 'Should throw a terminating error for a named $null of -, and change nothing' -ForEach $accessNamedNullCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedNull' -Directory + $before = (Get-Acl -LiteralPath $folder).Sddl + $namedArguments = @{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $namedArguments[$Parameter] = $null + + $caughtId = $null + $caughtMessage = $null + try { + & $Cmdlet @namedArguments -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + $caughtMessage = $_.Exception.Message + } + + if ($Parameter -eq 'AppliesTo') { + # AppliesTo is Mandatory: PowerShell's own binder already rejects a named $null before the cmdlet + # runs, with its own error id, unrelated to this fix; characterized here so that the row is not + # silently missing from the table. + $caughtId | Should -BeLike 'ParameterArgumentValidationErrorNullNotAllowed,*' + } + else { + $caughtId | Should -BeLike 'InvalidParameterValue,*' + $caughtMessage | Should -Match ([regex]::Escape($Parameter)) + $caughtMessage | Should -Match 'is null' + } + (Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before + } + + It 'Should throw a terminating InvalidParameterValue error for a named $null of -AccessControlType (the alias), and change nothing' -ForEach @( + @{ Cmdlet = 'Add-NTFSAccess' } + @{ Cmdlet = 'Remove-NTFSAccess' } + ) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedNullAlias' -Directory + $before = (Get-Acl -LiteralPath $folder).Sddl + + $caughtId = $null + try { + & $Cmdlet -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -AccessControlType $null -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + } + + $caughtId | Should -BeLike 'InvalidParameterValue,*' + (Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before + } + + It 'Should throw a terminating InvalidParameterValue error for a $null of -AccessType from $PSDefaultParameterValues, and change nothing' { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedNullDefault' -Directory + $before = (Get-Acl -LiteralPath $folder).Sddl + $PSDefaultParameterValues['Add-NTFSAccess:AccessType'] = $null + + $caughtId = $null + try { + Add-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + } + finally { + $PSDefaultParameterValues.Remove('Add-NTFSAccess:AccessType') + } + + $caughtId | Should -BeLike 'InvalidParameterValue,*' + (Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before + } + + It 'Should still give - its own default for a piped $null, unaffected by the named $null fix' -ForEach $accessNamedNullCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'PipedNullUnaffected' -Directory + $extra = @{ } + $extra[$Parameter] = $null + + [pscustomobject] (@{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $extra) | + & $Cmdlet -ErrorVariable pipedErrors -ErrorAction SilentlyContinue + + if ($Parameter -eq 'AppliesTo') { + # AppliesTo is Mandatory: a piped $null is the pre-existing binder validation error, unrelated to this fix. + $pipedErrors | Should -HaveCount 1 + $pipedErrors[0].FullyQualifiedErrorId | Should -BeLike 'ParameterArgumentValidationErrorNullNotAllowed,*' + } + else { + $pipedErrors | Should -BeNullOrEmpty + } + } + } + + Context 'Choosing the parameter set with a mix of string and enum values' { + It 'Should reject a piped object whose AppliesTo is combined with InheritanceFlags as with an AmbiguousParameterSet error' -ForEach $accessAmbiguousValueCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousPipedMix' -Directory + $before = (Get-Acl -LiteralPath $folder).Sddl + $appliesToValue = if ($AppliesToIsEnum) { [Security2.ApplyTo]::ThisFolderOnly } else { 'ThisFolderOnly' } + $flagValue = if ($FlagIsEnum) { [System.Security.AccessControl.InheritanceFlags]::None } else { 'None' } + $object = [pscustomobject]@{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AppliesTo = $appliesToValue; InheritanceFlags = $flagValue } + + { $object | Add-NTFSAccess -ErrorAction Stop } | Should -Throw -ErrorId 'AmbiguousParameterSet*' + + (Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before + } + + It 'Should reject a named call whose -AppliesTo is combined with -PropagationFlags as with an AmbiguousParameterSet error' -ForEach $accessAmbiguousValueCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousNamedMix' -Directory + $before = (Get-Acl -LiteralPath $folder).Sddl + $appliesToValue = if ($AppliesToIsEnum) { [Security2.ApplyTo]::ThisFolderOnly } else { 'ThisFolderOnly' } + $flagValue = if ($FlagIsEnum) { [System.Security.AccessControl.InheritanceFlags]::None } else { 'None' } + + { Add-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -AppliesTo $appliesToValue -PropagationFlags $flagValue -ErrorAction Stop } | + Should -Throw -ErrorId 'AmbiguousParameterSet*' + + (Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before + } + } + + # A typed parameter skips a $null, an empty string, or DBNull value of a property, and the entry was created for + # every shape below; the object-typed parameters count such a value of a property of the other group as present + # too, so PowerShell cannot resolve the parameter set and nothing is changed. + Context 'A blank value of a property of the other group' { + It 'Should reject () for () with the error the binder gives, change nothing, and still process the next object' -ForEach $accessAmbiguousOtherGroupCases { + $bad = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousOtherGroupBad' -Directory + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousOtherGroupNext' -Directory + $before = (Get-Acl -LiteralPath $bad).Sddl + + $badTarget = if ($TargetKind -eq 'Path') { @{ Path = $bad } } else { @{ SecurityDescriptor = (Get-NTFSSecurityDescriptor -Path $bad -ErrorAction Stop) } } + $nextTarget = if ($TargetKind -eq 'Path') { @{ Path = $next } } else { @{ SecurityDescriptor = (Get-NTFSSecurityDescriptor -Path $next -ErrorAction Stop) } } + + $badExtra = @{ Account = 'S-1-1-0'; AccessRights = 'ReadData' } + if ($AppliesToIsBlank) { + $badExtra['AppliesTo'] = $BlankValue + $badExtra['PropagationFlags'] = 'None' + } + else { + $badExtra['AppliesTo'] = 'ThisFolderOnly' + $badExtra['InheritanceFlags'] = $BlankValue + } + + $rows = @( + [pscustomobject] ($badTarget + $badExtra) + [pscustomobject] ($nextTarget + @{ Account = 'S-1-1-0'; AccessRights = 'ReadData' }) + ) + + $result = @($rows | & $Cmdlet -PassThru -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue) + + $pipelineErrors | Should -HaveCount 1 + $pipelineErrors[0].FullyQualifiedErrorId | Should -BeLike "$ExpectedErrorId,*" + (Get-Acl -LiteralPath $bad).Sddl | Should -BeExactly $before + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $next } + } + } + + # With the object type, a script block is a plain value (a typed parameter evaluates it once per piped object), + # so ValidateNamedValue in BeginProcessing rejects it the same as any other bad value, before any piped object + # is processed. + Context 'A named script block is a terminating error' { + It 'Should throw a terminating InvalidParameterValue error for a named script block value of -, and change nothing' -ForEach $accessScriptBlockCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedScriptBlock' -Directory + $before = (Get-Acl -LiteralPath $folder).Sddl + $namedArguments = @{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $namedArguments[$Parameter] = { 'Deny' } + + $caughtId = $null + try { + & $Cmdlet @namedArguments -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + } + + $caughtId | Should -BeLike 'InvalidParameterValue,*' + (Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before + } + } + + Context 'Tab completion and the parameter type' { + It ' - should complete exactly the names of its enumeration' -ForEach $accessCompleterCases { + $inputScript = '{0} -{1} ' -f $Cmdlet, $Parameter + $enumType = [type] $EnumTypeName + + $completion = [System.Management.Automation.CommandCompletion]::CompleteInput($inputScript, $inputScript.Length, $null) + + @($completion.CompletionMatches | ForEach-Object -Process { $_.CompletionText }) | Should -BeExactly ([Enum]::GetNames($enumType)) + } + + It ' should declare - as [object]' -ForEach $accessCompleterCases { + (Get-Command -Name $Cmdlet).Parameters[$Parameter].ParameterType | Should -Be ([object]) + } + + It 'Should complete Add-NTFSAccess - for the unquoted, single-quoted, double-quoted, and wildcard forms of ' -ForEach $accessCompletionFormCases { + $base = 'Add-NTFSAccess -Path C:\x -Account a -AccessRights Read -{0} ' -f $Parameter + + $unquoted = $base + $Word + $r1 = [System.Management.Automation.CommandCompletion]::CompleteInput($unquoted, $unquoted.Length, $null) + @($r1.CompletionMatches.CompletionText) | Should -Contain $ExpectedName + + $singleQuoted = $base + "'" + $Word + $r2 = [System.Management.Automation.CommandCompletion]::CompleteInput($singleQuoted, $singleQuoted.Length, $null) + @($r2.CompletionMatches.CompletionText) | Should -Contain ("'" + $ExpectedName + "'") + + $doubleQuoted = $base + '"' + $Word + $r3 = [System.Management.Automation.CommandCompletion]::CompleteInput($doubleQuoted, $doubleQuoted.Length, $null) + @($r3.CompletionMatches.CompletionText) | Should -Contain ('"' + $ExpectedName + '"') + + $wildcard = $base + '*' + $ExpectedName.Substring($ExpectedName.Length - 3) + $r4 = [System.Management.Automation.CommandCompletion]::CompleteInput($wildcard, $wildcard.Length, $null) + @($r4.CompletionMatches.CompletionText) | Should -Contain $ExpectedName + } + + It 'Should complete Add-NTFSAccess - for the word while the culture of the thread is Turkish' -ForEach $accessTurkishCompletionCases { + $inputScript = 'Add-NTFSAccess -Path C:\x -Account a -AccessRights Read -{0} {1}' -f $Parameter, $Word + $original = [System.Threading.Thread]::CurrentThread.CurrentCulture + try { + [System.Threading.Thread]::CurrentThread.CurrentCulture = [System.Globalization.CultureInfo]::GetCultureInfo('tr-TR') + 'I'.ToLower() | Should -Be ([string][char]0x131) -Because 'the culture of the thread has to be Turkish for the test to mean anything' + + $completion = [System.Management.Automation.CommandCompletion]::CompleteInput($inputScript, $inputScript.Length, $null) + $names = @($completion.CompletionMatches | ForEach-Object -Process { $_.CompletionText }) + } + finally { + [System.Threading.Thread]::CurrentThread.CurrentCulture = $original + } + + $names | Should -Contain $ExpectedName + } + } +} + Describe 'Declared output types and parameter set boundaries' { It ' should declare as its output type' -ForEach $declaredOutputTypes { @((Get-Command -Name $Name).OutputType.Name) | Should -Be $Types diff --git a/Tests/ParameterSets.Audit.Tests.ps1 b/Tests/ParameterSets.Audit.Tests.ps1 index a22289a..6c4aab4 100644 --- a/Tests/ParameterSets.Audit.Tests.ps1 +++ b/Tests/ParameterSets.Audit.Tests.ps1 @@ -3,11 +3,7 @@ the audit cmdlets: pipeline binding by value and by property name, continuation past a failing path or descriptor, and state or output assertions beyond -PassThru, with the module built in NTFSSecurity\bin\Release on files in a sandbox folder. Reading and changing audit entries needs the Security privilege; tests that need - it skip without it and run in CI, whose runners are elevated, like Audit.Tests.ps1. The -SecurityDescriptor sets - of Add-NTFSAudit and Remove-NTFSAudit have no per-item handler around the change itself (an open maintainer - item), so no test here pins what happens when that change throws for one of several descriptors; the only - failure the -SecurityDescriptor scenarios below exercise is the already-handled one of a descriptor that was - read without its audit section. + it skip without it and run in CI, whose runners are elevated, like Audit.Tests.ps1. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.' @@ -276,9 +272,10 @@ Describe 'Add-NTFSAudit' { $scopes -join '|' | Should -BeExactly 'None/None|ContainerInherit/None|ContainerInherit, ObjectInherit/InheritOnly' } - # A blank cell that Import-Csv reads is an empty string, not $null: the row gets the default scope (ParameterSets.Access.Tests.ps1 - # has the same rows and says why). - It 'Should give the row of a CSV file whose AppliesTo cell is blank the default scope, and the other rows their own' -Skip:(-not $canReadAudit) { + # A blank cell that Import-Csv reads is an empty string, not $null. Since -AppliesTo is now an object + # parameter (Decision 27, fix B), the blank string still binds it, which selects PathSimple, so the row gets + # an InvalidParameterValue error instead of the default scope (ParameterSets.Access.Tests.ps1 has the same rows). + It 'Should write an InvalidParameterValue error for the row of a CSV file whose AppliesTo cell is blank, leave its folder alone, and still give the other rows their own scope' -Skip:(-not $canReadAudit) { $folders = @('AddCsvAuditBlankFirst', 'AddCsvAuditBlankCell', 'AddCsvAuditBlankLast' | ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory }) $csv = Join-Path -Path $sandbox -ChildPath 'AddCsvAuditBlank.csv' @@ -290,10 +287,14 @@ Describe 'Add-NTFSAudit' { ('"{0}",{1},ReadData,SubfoldersAndFilesOnly' -f $folders[2], $probe) ) - Import-Csv -LiteralPath $csv | Add-NTFSAudit -ErrorAction Stop + Import-Csv -LiteralPath $csv | Add-NTFSAudit -ErrorVariable auditErrors -ErrorAction SilentlyContinue + $auditErrors | Should -HaveCount 1 + $auditErrors[0].FullyQualifiedErrorId | Should -BeLike 'InvalidParameterValue,*' + $auditErrors[0].CategoryInfo.Category | Should -Be 'InvalidArgument' + $auditErrors[0].TargetObject | Should -Be $folders[1] $scopes = $folders | ForEach-Object -Process { Get-TestAuditScope -Acl (Get-Acl -LiteralPath $_ -Audit) } - $scopes -join '|' | Should -BeExactly 'None/None|ContainerInherit, ObjectInherit/None|ContainerInherit, ObjectInherit/InheritOnly' + $scopes -join '|' | Should -BeExactly 'None/None||ContainerInherit, ObjectInherit/InheritOnly' } # $null cannot be bound at all: the object fails, loudly, and the cmdlet goes on with the next one. @@ -352,6 +353,55 @@ Describe 'Add-NTFSAudit' { (Get-TestAccountAuditRule -Acl $sd.SecurityDescriptor -Account $probe) | Should -HaveCount 1 } } + + # Item 5 of Decision 27: a failing change to one of several descriptors (an audit entry without rights, which + # .NET refuses to build, the same as Audit.Tests.ps1:141 pins for -Path) reports an error for that descriptor and + # goes on with the next one, instead of ending the cmdlet with a terminating error. + Context 'A failing change to one of several descriptors' { + It 'Should write an AddAceError for the descriptor whose change fails in the SDSimple set, continue with the next, and return -PassThru only for the one that succeeded' -Skip:(-not $canReadAudit) { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorSimpleFirst' + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorSimpleSecond' + $descriptors = @(Get-NTFSSecurityDescriptor -Path $first, $second -ErrorAction Stop) + $before = $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Audit') + + $rows = @( + [pscustomobject]@{ SecurityDescriptor = $descriptors[0]; Account = $probe; AccessRights = 'None'; AuditFlags = 'Success'; AppliesTo = 'ThisFolderOnly' } + [pscustomobject]@{ SecurityDescriptor = $descriptors[1]; Account = $probe; AccessRights = 'ReadData'; AuditFlags = 'Success'; AppliesTo = 'ThisFolderOnly' } + ) + + $result = @($rows | Add-NTFSAudit -PassThru -ErrorVariable sdErrors -ErrorAction SilentlyContinue) + + $sdErrors | Should -HaveCount 1 + $sdErrors[0].FullyQualifiedErrorId | Should -BeLike 'AddAceError,*' + $sdErrors[0].CategoryInfo.Category | Should -Be 'WriteError' + $sdErrors[0].TargetObject | Should -Be $descriptors[0] + $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Audit') | Should -BeExactly $before + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $second } + } + + It 'Should write an AddAceError for the descriptor whose change fails in the SDComplex set, continue with the next, and return -PassThru only for the one that succeeded' -Skip:(-not $canReadAudit) { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorComplexFirst' + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorComplexSecond' + $descriptors = @(Get-NTFSSecurityDescriptor -Path $first, $second -ErrorAction Stop) + $before = $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Audit') + + $rows = @( + [pscustomobject]@{ SecurityDescriptor = $descriptors[0]; Account = $probe; AccessRights = 'None'; AuditFlags = 'Success' } + [pscustomobject]@{ SecurityDescriptor = $descriptors[1]; Account = $probe; AccessRights = 'ReadData'; AuditFlags = 'Success' } + ) + + $result = @($rows | Add-NTFSAudit -PassThru -ErrorVariable sdErrors -ErrorAction SilentlyContinue) + + $sdErrors | Should -HaveCount 1 + $sdErrors[0].FullyQualifiedErrorId | Should -BeLike 'AddAceError,*' + $sdErrors[0].CategoryInfo.Category | Should -Be 'WriteError' + $sdErrors[0].TargetObject | Should -Be $descriptors[0] + $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Audit') | Should -BeExactly $before + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $second } + } + } } Describe 'Remove-NTFSAudit' { @@ -600,6 +650,38 @@ Describe 'Remove-NTFSAudit' { Get-TestDaclSddl -Path $file | Should -BeExactly $daclBefore } } + + # Item 5 of Decision 27: a failing change to one of several descriptors (an audit entry without rights, which + # .NET refuses to build, the same as Audit.Tests.ps1:141 pins for -Path) reports an error for that descriptor and + # goes on with the next one, instead of ending the cmdlet with a terminating error. Only the SDComplex set is + # listed as open in Quality-Gate-Test-Round-2026-10-20-ParameterSets.csv (row 26); SDSimple is already closed. + Context 'A failing change to one of several descriptors' { + It 'Should write a RemoveAceError for the descriptor whose change fails in the SDComplex set, continue with the next, and return -PassThru only for the one that succeeded' -Skip:(-not $canReadAudit) { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorComplexFirst' + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorComplexSecond' + # A keeper entry for a different account on both items: audit entries are not inherited by default, so + # without one, -PassThru of the descriptor that succeeds would show nothing once its only entry is gone. + Add-NTFSAudit -Path $first, $second -Account $probe -AccessRights ReadData -AuditFlags Success -ErrorAction Stop + Add-NTFSAudit -Path $first, $second -Account $keeper -AccessRights Delete -AuditFlags Success -ErrorAction Stop + $descriptors = @(Get-NTFSSecurityDescriptor -Path $first, $second -ErrorAction Stop) + $before = $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Audit') + + $rows = @( + [pscustomobject]@{ SecurityDescriptor = $descriptors[0]; Account = $probe; AccessRights = 'None'; AuditFlags = 'Success' } + [pscustomobject]@{ SecurityDescriptor = $descriptors[1]; Account = $probe; AccessRights = 'ReadData'; AuditFlags = 'Success' } + ) + + $result = @($rows | Remove-NTFSAudit -PassThru -ErrorVariable sdErrors -ErrorAction SilentlyContinue) + + $sdErrors | Should -HaveCount 1 + $sdErrors[0].FullyQualifiedErrorId | Should -BeLike 'RemoveAceError,*' + $sdErrors[0].CategoryInfo.Category | Should -Be 'WriteError' + $sdErrors[0].TargetObject | Should -Be $descriptors[0] + $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Audit') | Should -BeExactly $before + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $second } + } + } } Describe 'Clear-NTFSAudit' { @@ -914,3 +996,614 @@ Describe 'Get-NTFSOrphanedAudit' { } } } + +# Decision 27 (fix B): AuditFlags, InheritanceFlags, PropagationFlags, and AppliesTo of Add-NTFSAudit and +# Remove-NTFSAudit are object parameters, converted in the cmdlet with LanguagePrimitives.ConvertTo, so that a piped +# value the binder cannot convert to the enumeration is an error instead of a silently kept default. A bad value is +# caught by the conversion before the cmdlet reads or writes the SACL, so none of these tests need the Security +# privilege, unlike most of the tests above. +Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationFlags, and AppliesTo' { + BeforeDiscovery { + # A collection nested 40 levels deep is a list too: the conversion opens at most 32 levels, which also ends a + # collection that holds itself. + $auditDeepScope = 'FilesOnly' + foreach ($level in 1..40) { + $auditDeepScope = , $auditDeepScope + } + + # The message of the error names the value, and building it must not fail when the text of an element does. + $auditThrowingElement = [pscustomobject]@{ Unrelated = 1 } + $auditThrowingElement | Add-Member -MemberType ScriptMethod -Name ToString -Value { throw 'ToString throws' } -Force + + $auditBadShapes = @( + @{ Shape = 'a misspelled name'; Value = 'Typoo'; ValueText = 'Typoo' } + @{ Shape = 'a blank string'; Value = ''; ValueText = $null } + @{ Shape = 'a white-space string'; Value = ' '; ValueText = $null } + @{ Shape = 'an undefined number'; Value = 99; ValueText = '99' } + @{ Shape = 'a hashtable'; Value = @{ Unrelated = 1 }; ValueText = $null } + @{ Shape = 'an array of one misspelled name'; Value = @('Typoo'); ValueText = 'Typoo' } + @{ Shape = 'an array of one object whose ToString throws'; Value = @($auditThrowingElement); ValueText = $null } + ) + $auditFlagsBadShapes = @( + @{ Shape = 'a misspelled name'; Value = 'Failurre'; ValueText = 'Failurre' } + @{ Shape = 'a blank string'; Value = ''; ValueText = $null } + @{ Shape = 'a white-space string'; Value = ' '; ValueText = $null } + @{ Shape = 'an undefined number'; Value = 99; ValueText = '99' } + @{ Shape = 'a hashtable'; Value = @{ Unrelated = 1 }; ValueText = $null } + @{ Shape = 'an array of one misspelled name'; Value = @('Failurre'); ValueText = 'Failurre' } + @{ Shape = 'an array of one object whose ToString throws'; Value = @($auditThrowingElement); ValueText = $null } + ) + + # The cross product of the two cmdlets that share these parameters and every bad shape: 2 * (7 + 3 * 7 + 6) = 68 rows. + # AppliesTo names one value, not a combination: PowerShell combines the numbers of a list bit by bit, so that one + # of the values wins silently or a scope that the caller did not name is chosen. AuditFlags and the two flags + # parameters take a list. Every kind of collection counts, also a collection of one element that holds a list. + $auditAppliesToListShapes = @( + @{ Shape = 'a comma list of two names'; Value = 'ThisFolderAndSubfolders,ThisFolderAndFiles'; ValueText = 'ThisFolderAndSubfolders,ThisFolderAndFiles' } + @{ Shape = 'an array of two names'; Value = @('ThisFolderAndSubfolders', 'ThisFolderAndFiles'); ValueText = 'ThisFolderAndSubfolders, ThisFolderAndFiles' } + @{ Shape = 'an array of one comma list'; Value = @('ThisFolderAndSubfolders,ThisFolderAndFiles'); ValueText = 'ThisFolderAndSubfolders,ThisFolderAndFiles' } + @{ Shape = 'a hash set of two names'; Value = [System.Collections.Generic.HashSet[string]]::new([string[]]@('ThisFolderAndSubfolders', 'ThisFolderAndFiles')); ValueText = $null } + @{ Shape = 'the keys of a hashtable'; Value = (@{ ThisFolderAndSubfolders = 1; ThisFolderAndFiles = 2 }).Keys; ValueText = $null } + @{ Shape = 'an array nested 40 levels deep'; Value = $auditDeepScope; ValueText = $null } + ) + + $auditInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAudit', 'Remove-NTFSAudit') { + foreach ($shapeCase in $auditFlagsBadShapes) { + @{ Cmdlet = $cmdlet; Parameter = 'AuditFlags'; Shape = $shapeCase.Shape; Value = $shapeCase.Value; ValueText = $shapeCase.ValueText } + } + foreach ($parameterName in 'InheritanceFlags', 'PropagationFlags', 'AppliesTo') { + foreach ($shapeCase in $auditBadShapes) { + @{ Cmdlet = $cmdlet; Parameter = $parameterName; Shape = $shapeCase.Shape; Value = $shapeCase.Value; ValueText = $shapeCase.ValueText } + } + } + foreach ($shapeCase in $auditAppliesToListShapes) { + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Shape = $shapeCase.Shape; Value = $shapeCase.Value; ValueText = $shapeCase.ValueText } + } + } + + $auditSdInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAudit', 'Remove-NTFSAudit') { + @{ Cmdlet = $cmdlet; Parameter = 'AuditFlags'; Value = 'Failurre'; ValueText = 'Failurre' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'Typoo'; ValueText = 'Typoo' } + } + + $auditNoCarryOverCases = @( + @{ Parameter = 'AuditFlags'; Value = 'Failurre' } + @{ Parameter = 'InheritanceFlags'; Value = 'Typoo' } + @{ Parameter = 'PropagationFlags'; Value = 'Typoo' } + @{ Parameter = 'AppliesTo'; Value = 'Typoo' } + ) + + $auditNamedInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAudit', 'Remove-NTFSAudit') { + @{ Cmdlet = $cmdlet; Parameter = 'AuditFlags'; Value = 'Failurre' } + @{ Cmdlet = $cmdlet; Parameter = 'InheritanceFlags'; Value = 'Typoo' } + @{ Cmdlet = $cmdlet; Parameter = 'PropagationFlags'; Value = 'Typoo' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'Typoo' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'ThisFolderAndSubfolders,ThisFolderAndFiles' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = @('ThisFolderAndSubfolders', 'ThisFolderAndFiles') } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = @('ThisFolderAndSubfolders,ThisFolderAndFiles') } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = [System.Collections.Generic.HashSet[string]]::new([string[]]@('ThisFolderAndSubfolders', 'ThisFolderAndFiles')) } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = $auditDeepScope } + @{ Cmdlet = $cmdlet; Parameter = 'AuditFlags'; Value = @($auditThrowingElement) } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = @($auditThrowingElement) } + } + + # A named $null (a literal $null, a variable, splatting, or $PSDefaultParameterValues) must stay a + # terminating error, the same as a bad named value; only a $null that a piped object's property carries + # keeps the parameter's own default. + $auditNamedNullCases = foreach ($cmdlet in 'Add-NTFSAudit', 'Remove-NTFSAudit') { + @{ Cmdlet = $cmdlet; Parameter = 'AuditFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'InheritanceFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'PropagationFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo' } + } + + $auditValidAuditFlagsShapeCases = @( + @{ Description = 'the exact name'; Value = 'Failure' } + @{ Description = 'a different case'; Value = 'failure' } + @{ Description = 'a comma list without spaces'; Value = 'Success,Failure' } + @{ Description = 'a comma list with spaces'; Value = 'Success, Failure' } + @{ Description = 'the enum value'; Value = [System.Security.AccessControl.AuditFlags]::Failure } + @{ Description = 'an integer'; Value = 2 } + ) + + $auditValidFlagsShapeCases = @( + @{ Description = 'a comma list without spaces'; Value = 'ContainerInherit,ObjectInherit' } + @{ Description = 'a comma list with spaces'; Value = 'ContainerInherit, ObjectInherit' } + @{ Description = 'the enum value'; Value = [System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit' } + ) + + $auditAmbiguousValueCases = @( + @{ AppliesToShape = 'a string'; AppliesToIsEnum = $false; FlagShape = 'a string'; FlagIsEnum = $false } + @{ AppliesToShape = 'an enum value'; AppliesToIsEnum = $true; FlagShape = 'a string'; FlagIsEnum = $false } + @{ AppliesToShape = 'a string'; AppliesToIsEnum = $false; FlagShape = 'an enum value'; FlagIsEnum = $true } + @{ AppliesToShape = 'an enum value'; AppliesToIsEnum = $true; FlagShape = 'an enum value'; FlagIsEnum = $true } + ) + + $auditCompleterCases = foreach ($cmdlet in 'Add-NTFSAudit', 'Remove-NTFSAudit') { + @{ Cmdlet = $cmdlet; Parameter = 'AuditFlags'; EnumTypeName = 'System.Security.AccessControl.AuditFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'InheritanceFlags'; EnumTypeName = 'System.Security.AccessControl.InheritanceFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'PropagationFlags'; EnumTypeName = 'System.Security.AccessControl.PropagationFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; EnumTypeName = 'Security2.ApplyTo' } + } + + # AuditFlags is the one completer that ParameterSets.Access.Tests.ps1 does not already cover with the + # quoted and wildcard forms (it shares the same completer code for InheritanceFlags, PropagationFlags, and + # AppliesTo, so those are pinned there). + $auditCompletionFormCases = @( + @{ Parameter = 'AuditFlags'; Word = 'F'; ExpectedName = 'Failure' } + ) + + # In Turkish, a case-insensitive comparison of the culture of the thread treats I and i as different letters, so + # a word typed in capitals completes only when the completers compare culture invariant. + $auditTurkishCompletionCases = @( + @{ Parameter = 'AuditFlags'; Word = 'FAIL'; ExpectedName = 'Failure' } + ) + + # Because the five parameters are object, PowerShell counts a $null, an empty string, or DBNull value of a + # property as present when it chooses the parameter set, the same as a real value, so AppliesTo (the Simple + # group) together with a $null/''/DBNull InheritanceFlags or PropagationFlags (the Complex group) offers + # both groups and is AmbiguousParameterSet, not InvalidParameterValue. The reverse (AppliesTo blank, a + # Complex property valid) is AmbiguousParameterSet too, except that AppliesTo is Mandatory, so a literal + # $null of it (unlike '' or DBNull) is rejected earlier by the binder's own Mandatory check. Covers the + # Path and SD sets of the audit cmdlets; the access cmdlets are pinned in ParameterSets.Access.Tests.ps1. + $auditAmbiguousOtherGroupCases = foreach ($cmdlet in 'Add-NTFSAudit', 'Remove-NTFSAudit') { + foreach ($targetKind in 'Path', 'SD') { + foreach ($shape in 'a $null', 'an empty string', 'DBNull') { + $blankValue = switch ($shape) { 'a $null' { $null }; 'an empty string' { '' }; 'DBNull' { [System.DBNull]::Value } } + @{ + Cmdlet = $cmdlet; TargetKind = $targetKind; Shape = $shape; BlankValue = $blankValue + Direction = 'AppliesTo valid, InheritanceFlags blank'; AppliesToIsBlank = $false + ExpectedErrorId = 'AmbiguousParameterSet' + } + @{ + Cmdlet = $cmdlet; TargetKind = $targetKind; Shape = $shape; BlankValue = $blankValue + Direction = 'AppliesTo blank, PropagationFlags valid'; AppliesToIsBlank = $true + ExpectedErrorId = $(if ($shape -eq 'a $null') { 'ParameterArgumentValidationErrorNullNotAllowed' } else { 'AmbiguousParameterSet' }) + } + } + } + } + + # With the object type, a script block is a plain value (a typed parameter evaluates it once per piped + # object), so it fails the same way any other value of the wrong shape does: a terminating + # InvalidParameterValue from ValidateNamedValue in BeginProcessing, before any piped object is processed. + $auditScriptBlockCases = foreach ($cmdlet in 'Add-NTFSAudit', 'Remove-NTFSAudit') { + @{ Cmdlet = $cmdlet; Parameter = 'AuditFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'InheritanceFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'PropagationFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo' } + } + } + + Context 'A bad value bound from the pipeline' { + # Writing an audit entry needs the Security privilege, so without it the "good" object right next to the bad + # one fails too, with its own (unrelated) privilege error; that part of the test runs only with the privilege, + # the same way most of Audit.Tests.ps1 does. The conversion failure that this test pins happens before the + # cmdlet ever touches the SACL, so the core assertions below run in every configuration. + It 'Should write one InvalidParameterValue error for - (), change nothing, and still process the next object' -ForEach $auditInvalidParameterCases { + $path = New-TestSandboxItem -Sandbox $sandbox -Name 'InvalidValue' -Directory + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'InvalidValueNext' -Directory + $before = if ($canReadAudit) { Get-TestAuditSddl -Path $path } + $extra = @{ } + $extra[$Parameter] = $Value + + $rows = @( + [pscustomobject] (@{ FullName = $path; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $extra) + [pscustomobject]@{ FullName = $next; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + ) + + $result = @($rows | & $Cmdlet -PassThru -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue) + + $invalidValueErrors = @($pipelineErrors | Where-Object -FilterScript { $_.TargetObject -eq $path }) + $invalidValueErrors | Should -HaveCount 1 + $invalidValueErrors[0].FullyQualifiedErrorId | Should -BeLike 'InvalidParameterValue,*' + $invalidValueErrors[0].CategoryInfo.Category | Should -Be 'InvalidArgument' + $invalidValueErrors[0].Exception.Message | Should -Match ([regex]::Escape($Parameter)) + if ($ValueText) { + $invalidValueErrors[0].Exception.Message | Should -Match ("The value '" + [regex]::Escape($ValueText) + "'") + } + if ($canReadAudit) { + Get-TestAuditSddl -Path $path | Should -BeExactly $before + $pipelineErrors | Should -HaveCount 1 + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $next } + } + } + + # A descriptor that Get-NTFSSecurityDescriptor read without the Security privilege has no audit section, so + # without the privilege the "good" descriptor also gets its own (unrelated) ReadSecurityError; see above. + It 'Should write one InvalidParameterValue error per descriptor for - bound to a piped descriptor, and leave the descriptors unchanged' -ForEach $auditSdInvalidParameterCases { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'InvalidSdFirst' -Directory + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'InvalidSdSecond' -Directory + $descriptors = @(Get-NTFSSecurityDescriptor -Path $first, $second -ErrorAction Stop) + $before = $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') + $extra = @{ } + $extra[$Parameter] = $Value + + $rows = @( + [pscustomobject] (@{ SecurityDescriptor = $descriptors[0]; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $extra) + [pscustomobject]@{ SecurityDescriptor = $descriptors[1]; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + ) + + $result = @($rows | & $Cmdlet -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue) + + $invalidValueErrors = @($pipelineErrors | Where-Object -FilterScript { $_.TargetObject -eq $descriptors[0] }) + $invalidValueErrors | Should -HaveCount 1 + $invalidValueErrors[0].FullyQualifiedErrorId | Should -BeLike 'InvalidParameterValue,*' + $invalidValueErrors[0].CategoryInfo.Category | Should -Be 'InvalidArgument' + $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') | Should -BeExactly $before + if ($canReadAudit) { + $pipelineErrors | Should -HaveCount 1 + } + } + } + + Context 'Continuation and no carry-over across piped objects' { + It 'Should apply both good objects when a bad AuditFlags value is between them, in the order of the rows' -Skip:(-not $canReadAudit) { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinueFirst' -Directory + $middle = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinueMiddle' -Directory + $last = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinueLast' -Directory + + $rows = @( + [pscustomobject]@{ FullName = $first; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AuditFlags = 'Failure' } + [pscustomobject]@{ FullName = $middle; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AuditFlags = 'Failurre' } + [pscustomobject]@{ FullName = $last; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AuditFlags = 'Failure' } + ) + + $rows | Add-NTFSAudit -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue + + $pipelineErrors | Should -HaveCount 1 + $pipelineErrors[0].TargetObject | Should -Be $middle + $firstRule = Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $first -Audit) -Account 'S-1-1-0' + $firstRule | Should -HaveCount 1 + $firstRule[0].AuditFlags | Should -Be ([System.Security.AccessControl.AuditFlags]::Failure) + $middleRule = Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $middle -Audit) -Account 'S-1-1-0' + $middleRule | Should -BeNullOrEmpty + $lastRule = Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $last -Audit) -Account 'S-1-1-0' + $lastRule | Should -HaveCount 1 + $lastRule[0].AuditFlags | Should -Be ([System.Security.AccessControl.AuditFlags]::Failure) + } + + It 'Should give the row without AuditFlags the default Success and Failure after a preceding Failure-only row, with no carry-over' -Skip:(-not $canReadAudit) { + $failureFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'NoCarryOverFailure' -Directory + $defaultFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'NoCarryOverDefault' -Directory + + @( + [pscustomobject]@{ FullName = $failureFolder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AuditFlags = 'Failure' } + [pscustomobject]@{ FullName = $defaultFolder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + ) | Add-NTFSAudit -ErrorAction Stop + + $failureRule = Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $failureFolder -Audit) -Account 'S-1-1-0' + $failureRule | Should -HaveCount 1 + $failureRule[0].AuditFlags | Should -Be ([System.Security.AccessControl.AuditFlags]::Failure) + $defaultRule = Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $defaultFolder -Audit) -Account 'S-1-1-0' + $defaultRule | Should -HaveCount 1 + $defaultRule[0].AuditFlags | Should -Be ([System.Security.AccessControl.AuditFlags] 'Success, Failure') + } + + It 'Should give the row without its own default after a preceding row with an invalid value, with no carry-over' -ForEach $auditNoCarryOverCases -Skip:(-not $canReadAudit) { + $badFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'NoCarryOverBad' -Directory + $defaultFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'NoCarryOverAfterBad' -Directory + $extra = @{ } + $extra[$Parameter] = $Value + + @( + [pscustomobject] (@{ FullName = $badFolder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $extra) + [pscustomobject]@{ FullName = $defaultFolder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + ) | Add-NTFSAudit -ErrorAction SilentlyContinue + + $defaultRule = Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $defaultFolder -Audit) -Account 'S-1-1-0' + $defaultRule | Should -HaveCount 1 + $defaultRule[0].AuditFlags | Should -Be ([System.Security.AccessControl.AuditFlags] 'Success, Failure') + $defaultRule[0].InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit') + $defaultRule[0].PropagationFlags | Should -Be ([System.Security.AccessControl.PropagationFlags]::None) + } + } + + Context 'Valid values are unchanged for every shape' { + It 'Should add a Failure-only entry for AuditFlags given as ' -ForEach $auditValidAuditFlagsShapeCases -Skip:(-not $canReadAudit) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ValidAuditFlags' -Directory + + [pscustomobject]@{ FullName = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AuditFlags = $Value } | Add-NTFSAudit -ErrorAction Stop + + $rule = Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $folder -Audit) -Account 'S-1-1-0' + $rule | Should -HaveCount 1 + $rule[0].AuditFlags.HasFlag([System.Security.AccessControl.AuditFlags]::Failure) | Should -BeTrue + } + + It 'Should keep the default AuditFlags Success and Failure for a $null value' -Skip:(-not $canReadAudit) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ValidAuditFlagsNull' -Directory + + [pscustomobject]@{ FullName = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AuditFlags = $null } | Add-NTFSAudit -ErrorAction Stop + + $rule = Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $folder -Audit) -Account 'S-1-1-0' + $rule | Should -HaveCount 1 + $rule[0].AuditFlags | Should -Be ([System.Security.AccessControl.AuditFlags] 'Success, Failure') + } + + It 'Should scope the entry with InheritanceFlags given as ' -ForEach $auditValidFlagsShapeCases -Skip:(-not $canReadAudit) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ValidInheritanceFlags' -Directory + + [pscustomobject]@{ FullName = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; InheritanceFlags = $Value; PropagationFlags = 'None' } | Add-NTFSAudit -ErrorAction Stop + + $rule = Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $folder -Audit) -Account 'S-1-1-0' + $rule | Should -HaveCount 1 + $rule[0].InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit') + } + + It 'Should keep the default flags for $null InheritanceFlags and PropagationFlags' -Skip:(-not $canReadAudit) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ValidFlagsNull' -Directory + + [pscustomobject]@{ FullName = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; InheritanceFlags = $null; PropagationFlags = $null } | Add-NTFSAudit -ErrorAction Stop + + $rule = Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $folder -Audit) -Account 'S-1-1-0' + $rule | Should -HaveCount 1 + $rule[0].InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit') + $rule[0].PropagationFlags | Should -Be ([System.Security.AccessControl.PropagationFlags]::None) + } + } + + Context 'A bad value named on the command line' { + It 'Should throw a terminating InvalidParameterValue error for -, catchable with try/catch, and change nothing' -ForEach $auditNamedInvalidParameterCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedInvalid' -Directory + $before = if ($canReadAudit) { Get-TestAuditSddl -Path $folder } + $namedArguments = @{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $namedArguments[$Parameter] = $Value + + $caughtId = $null + try { + & $Cmdlet @namedArguments -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + } + + $caughtId | Should -BeLike 'InvalidParameterValue,*' + if ($canReadAudit) { + Get-TestAuditSddl -Path $folder | Should -BeExactly $before + } + } + + It 'Should stop before the first piped path when -AuditFlags is named and bad' { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedBadFirst' -Directory + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedBadSecond' -Directory + $before = if ($canReadAudit) { @((Get-TestAuditSddl -Path $first), (Get-TestAuditSddl -Path $second)) } + + $caughtId = $null + try { + $first, $second | Add-NTFSAudit -Account 'S-1-1-0' -AccessRights ReadData -AuditFlags Failurre -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + } + + $caughtId | Should -BeLike 'InvalidParameterValue,*' + if ($canReadAudit) { + Get-TestAuditSddl -Path $first | Should -BeExactly $before[0] + Get-TestAuditSddl -Path $second | Should -BeExactly $before[1] + } + } + } + + # A typed parameter ends a named $null with a terminating ParameterBindingFailed; the object-typed parameter + # accepts it, so the cmdlet must not keep the default silently. A named $null is reported the same way as a bad + # named value. + Context 'A named $null is a terminating error' { + It 'Should throw a terminating error for a named $null of -, and change nothing' -ForEach $auditNamedNullCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedNull' -Directory + $before = if ($canReadAudit) { Get-TestAuditSddl -Path $folder } + $namedArguments = @{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $namedArguments[$Parameter] = $null + + $caughtId = $null + $caughtMessage = $null + try { + & $Cmdlet @namedArguments -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + $caughtMessage = $_.Exception.Message + } + + if ($Parameter -eq 'AppliesTo') { + # AppliesTo is Mandatory: PowerShell's own binder already rejects a named $null before the cmdlet + # runs, with its own error id, unrelated to this fix; characterized here so that the row is not + # silently missing from the table. + $caughtId | Should -BeLike 'ParameterArgumentValidationErrorNullNotAllowed,*' + } + else { + $caughtId | Should -BeLike 'InvalidParameterValue,*' + $caughtMessage | Should -Match ([regex]::Escape($Parameter)) + $caughtMessage | Should -Match 'is null' + } + if ($canReadAudit) { + Get-TestAuditSddl -Path $folder | Should -BeExactly $before + } + } + + It 'Should throw a terminating InvalidParameterValue error for a $null of -AuditFlags from $PSDefaultParameterValues, and change nothing' { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedNullDefault' -Directory + $before = if ($canReadAudit) { Get-TestAuditSddl -Path $folder } + $PSDefaultParameterValues['Add-NTFSAudit:AuditFlags'] = $null + + $caughtId = $null + try { + Add-NTFSAudit -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + } + finally { + $PSDefaultParameterValues.Remove('Add-NTFSAudit:AuditFlags') + } + + $caughtId | Should -BeLike 'InvalidParameterValue,*' + if ($canReadAudit) { + Get-TestAuditSddl -Path $folder | Should -BeExactly $before + } + } + + It 'Should still give - its own default for a piped $null, unaffected by the named $null fix' -ForEach $auditNamedNullCases -Skip:(-not $canReadAudit) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'PipedNullUnaffected' -Directory + $extra = @{ } + $extra[$Parameter] = $null + + [pscustomobject] (@{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $extra) | + & $Cmdlet -ErrorVariable pipedErrors -ErrorAction SilentlyContinue + + if ($Parameter -eq 'AppliesTo') { + # AppliesTo is Mandatory: a piped $null is the pre-existing binder validation error, unrelated to this fix. + $pipedErrors | Should -HaveCount 1 + $pipedErrors[0].FullyQualifiedErrorId | Should -BeLike 'ParameterArgumentValidationErrorNullNotAllowed,*' + } + else { + $pipedErrors | Should -BeNullOrEmpty + } + } + } + + Context 'Choosing the parameter set with a mix of string and enum values' { + It 'Should reject a piped object whose AppliesTo is combined with InheritanceFlags as with an AmbiguousParameterSet error' -ForEach $auditAmbiguousValueCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousPipedMix' -Directory + $appliesToValue = if ($AppliesToIsEnum) { [Security2.ApplyTo]::ThisFolderOnly } else { 'ThisFolderOnly' } + $flagValue = if ($FlagIsEnum) { [System.Security.AccessControl.InheritanceFlags]::None } else { 'None' } + $object = [pscustomobject]@{ FullName = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AppliesTo = $appliesToValue; InheritanceFlags = $flagValue } + + { $object | Add-NTFSAudit -ErrorAction Stop } | Should -Throw -ErrorId 'AmbiguousParameterSet*' + } + + It 'Should reject a named call whose -AppliesTo is combined with -PropagationFlags as with an AmbiguousParameterSet error' -ForEach $auditAmbiguousValueCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousNamedMix' -Directory + $appliesToValue = if ($AppliesToIsEnum) { [Security2.ApplyTo]::ThisFolderOnly } else { 'ThisFolderOnly' } + $flagValue = if ($FlagIsEnum) { [System.Security.AccessControl.InheritanceFlags]::None } else { 'None' } + + { Add-NTFSAudit -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -AppliesTo $appliesToValue -PropagationFlags $flagValue -ErrorAction Stop } | + Should -Throw -ErrorId 'AmbiguousParameterSet*' + } + } + + # A typed parameter skips a $null, an empty string, or DBNull value of a property, and the entry was created for + # every shape below; the object-typed parameters count such a value of a property of the other group as present + # too, so PowerShell cannot resolve the parameter set and nothing is changed. The conversion/parameter-set + # failure happens before the cmdlet ever touches the SACL, so the core assertion (the error id, and that the bad + # object's audit entries are unchanged) runs in every configuration; only the "good next object succeeded" part + # needs the Security privilege, the same way the rest of this file does. + Context 'A blank value of a property of the other group' { + It 'Should reject () for () with the error the binder gives, change nothing, and still process the next object' -ForEach $auditAmbiguousOtherGroupCases { + $bad = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousOtherGroupBad' -Directory + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousOtherGroupNext' -Directory + $before = if ($canReadAudit) { Get-TestAuditSddl -Path $bad } + + $badTarget = if ($TargetKind -eq 'Path') { @{ FullName = $bad } } else { @{ SecurityDescriptor = (Get-NTFSSecurityDescriptor -Path $bad -ErrorAction Stop) } } + $nextTarget = if ($TargetKind -eq 'Path') { @{ FullName = $next } } else { @{ SecurityDescriptor = (Get-NTFSSecurityDescriptor -Path $next -ErrorAction Stop) } } + + $badExtra = @{ Account = 'S-1-1-0'; AccessRights = 'ReadData' } + if ($AppliesToIsBlank) { + $badExtra['AppliesTo'] = $BlankValue + $badExtra['PropagationFlags'] = 'None' + } + else { + $badExtra['AppliesTo'] = 'ThisFolderOnly' + $badExtra['InheritanceFlags'] = $BlankValue + } + + $rows = @( + [pscustomobject] ($badTarget + $badExtra) + [pscustomobject] ($nextTarget + @{ Account = 'S-1-1-0'; AccessRights = 'ReadData' }) + ) + + $result = @($rows | & $Cmdlet -PassThru -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue) + + $badErrors = @($pipelineErrors | Where-Object -FilterScript { + ($TargetKind -eq 'Path' -and $_.TargetObject.FullName -eq $bad) -or + ($TargetKind -eq 'SD' -and $_.TargetObject.SecurityDescriptor -eq $badTarget['SecurityDescriptor']) + }) + $badErrors | Should -HaveCount 1 + $badErrors[0].FullyQualifiedErrorId | Should -BeLike "$ExpectedErrorId,*" + if ($canReadAudit) { + Get-TestAuditSddl -Path $bad | Should -BeExactly $before + $pipelineErrors | Should -HaveCount 1 + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $next } + } + } + } + + # With the object type, a script block is a plain value (a typed parameter evaluates it once per piped object), + # so ValidateNamedValue in BeginProcessing rejects it the same as any other bad value, before any piped object + # is processed and before the cmdlet ever touches the SACL. The terminating error itself needs no Security + # privilege; reading the SACL back to confirm nothing changed does, the same way the rest of this file does. + Context 'A named script block is a terminating error' { + It 'Should throw a terminating InvalidParameterValue error for a named script block value of -, and change nothing' -ForEach $auditScriptBlockCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedScriptBlock' -Directory + $before = if ($canReadAudit) { Get-TestAuditSddl -Path $folder } + $namedArguments = @{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $namedArguments[$Parameter] = { 'Deny' } + + $caughtId = $null + try { + & $Cmdlet @namedArguments -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + } + + $caughtId | Should -BeLike 'InvalidParameterValue,*' + if ($canReadAudit) { + Get-TestAuditSddl -Path $folder | Should -BeExactly $before + } + } + } + + Context 'Tab completion and the parameter type' { + It ' - should complete exactly the names of its enumeration' -ForEach $auditCompleterCases { + $inputScript = '{0} -{1} ' -f $Cmdlet, $Parameter + $enumType = [type] $EnumTypeName + + $completion = [System.Management.Automation.CommandCompletion]::CompleteInput($inputScript, $inputScript.Length, $null) + + @($completion.CompletionMatches | ForEach-Object -Process { $_.CompletionText }) | Should -BeExactly ([Enum]::GetNames($enumType)) + } + + It ' should declare - as [object]' -ForEach $auditCompleterCases { + (Get-Command -Name $Cmdlet).Parameters[$Parameter].ParameterType | Should -Be ([object]) + } + + It 'Should complete Add-NTFSAudit - for the unquoted, single-quoted, double-quoted, and wildcard forms of ' -ForEach $auditCompletionFormCases { + $base = 'Add-NTFSAudit -Path C:\x -Account a -AccessRights Read -{0} ' -f $Parameter + + $unquoted = $base + $Word + $r1 = [System.Management.Automation.CommandCompletion]::CompleteInput($unquoted, $unquoted.Length, $null) + @($r1.CompletionMatches.CompletionText) | Should -Contain $ExpectedName + + $singleQuoted = $base + "'" + $Word + $r2 = [System.Management.Automation.CommandCompletion]::CompleteInput($singleQuoted, $singleQuoted.Length, $null) + @($r2.CompletionMatches.CompletionText) | Should -Contain ("'" + $ExpectedName + "'") + + $doubleQuoted = $base + '"' + $Word + $r3 = [System.Management.Automation.CommandCompletion]::CompleteInput($doubleQuoted, $doubleQuoted.Length, $null) + @($r3.CompletionMatches.CompletionText) | Should -Contain ('"' + $ExpectedName + '"') + + $wildcard = $base + '*' + $ExpectedName.Substring($ExpectedName.Length - 3) + $r4 = [System.Management.Automation.CommandCompletion]::CompleteInput($wildcard, $wildcard.Length, $null) + @($r4.CompletionMatches.CompletionText) | Should -Contain $ExpectedName + } + + It 'Should complete Add-NTFSAudit - for the word while the culture of the thread is Turkish' -ForEach $auditTurkishCompletionCases { + $inputScript = 'Add-NTFSAudit -Path C:\x -Account a -AccessRights Read -{0} {1}' -f $Parameter, $Word + $original = [System.Threading.Thread]::CurrentThread.CurrentCulture + try { + [System.Threading.Thread]::CurrentThread.CurrentCulture = [System.Globalization.CultureInfo]::GetCultureInfo('tr-TR') + 'I'.ToLower() | Should -Be ([string][char]0x131) -Because 'the culture of the thread has to be Turkish for the test to mean anything' + + $completion = [System.Management.Automation.CommandCompletion]::CompleteInput($inputScript, $inputScript.Length, $null) + $names = @($completion.CompletionMatches | ForEach-Object -Process { $_.CompletionText }) + } + finally { + [System.Threading.Thread]::CurrentThread.CurrentCulture = $original + } + + $names | Should -Contain $ExpectedName + } + } +} diff --git a/Tests/ParameterSets.Items.Tests.ps1 b/Tests/ParameterSets.Items.Tests.ps1 index 7805650..1e4ae31 100644 --- a/Tests/ParameterSets.Items.Tests.ps1 +++ b/Tests/ParameterSets.Items.Tests.ps1 @@ -1206,6 +1206,48 @@ Describe 'Get-FileHash2 binds -Algorithm by property name' { } } +Describe 'Copy-Item2 and Move-Item2 bind -Destination by property name' { + # A piped object binds -Destination by its own property, so two objects can go to two destinations in one call. + It ' should put each piped item in the destination that its own object names' -ForEach @( + @{ Command = 'Copy-Item2' } + @{ Command = 'Move-Item2' } + ) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name "DestinationByProperty$Command" -Directory + $destinationOne = Join-Path -Path $folder -ChildPath 'One' + $destinationTwo = Join-Path -Path $folder -ChildPath 'Two' + $first = Join-Path -Path $folder -ChildPath 'First.txt' + $second = Join-Path -Path $folder -ChildPath 'Second.txt' + Assert-TestSandboxPath -Sandbox $sandbox -Path $destinationOne, $destinationTwo, $first, $second + New-Item -ItemType Directory -Path $destinationOne, $destinationTwo | Out-Null + Set-Content -LiteralPath $first -Value 'First' + Set-Content -LiteralPath $second -Value 'Second' + $requests = @( + [pscustomobject]@{ Path = $first; Destination = $destinationOne } + [pscustomobject]@{ Path = $second; Destination = $destinationTwo } + ) + + $result = @($requests | & $Command -PassThru $true -ErrorAction Stop) + + $copiedFirst = Join-Path -Path $destinationOne -ChildPath 'First.txt' + $copiedSecond = Join-Path -Path $destinationTwo -ChildPath 'Second.txt' + $result | Should -HaveCount 2 + $result[0].FullName | Should -Be $copiedFirst + $result[1].FullName | Should -Be $copiedSecond + Get-Content -LiteralPath $copiedFirst | Should -Be 'First' + Get-Content -LiteralPath $copiedSecond | Should -Be 'Second' + (Join-Path -Path $destinationOne -ChildPath 'Second.txt') | Should -Not -Exist + (Join-Path -Path $destinationTwo -ChildPath 'First.txt') | Should -Not -Exist + if ($Command -eq 'Copy-Item2') { + $first | Should -Exist + $second | Should -Exist + } + else { + $first | Should -Not -Exist + $second | Should -Not -Exist + } + } +} + Describe 'Get-DiskSpace free-space values and validation error' { It 'Should report free-space values close to an independent .NET read for the system drive' { $before = New-Object -TypeName 'System.IO.DriveInfo' -ArgumentList $env:SystemDrive diff --git a/Tests/PipelineControl.Tests.ps1 b/Tests/PipelineControl.Tests.ps1 index 96a035c..68c4c7b 100644 --- a/Tests/PipelineControl.Tests.ps1 +++ b/Tests/PipelineControl.Tests.ps1 @@ -636,3 +636,184 @@ namespace NtfsSecurityTests { public class StandInForBreak : System.Management.A $isEnd.Invoke($null, @($Exception)) | Should -BeFalse } } + +# InvokeAsOwner (and WriteChangesAsOwner of Set-NTFSSecurityDescriptor) take ownership, run the action, and in a +# finally restore the previous owner, writing a RestoreOwnerError through WriteError when that restore fails. +# WriteError runs the later commands of the pipeline and notes what they raise (IsFromLaterCommand, above); the catch +# around the call must pass that on instead of reporting it as the item's own error and going on, or the caller never +# sees it. Each row denies the current user the right its own action needs (a change denial for the writers, a read +# denial on Everyone for Get-FileHash2, which needs file data, not permissions) and gives the item an owner that only +# the Restore privilege can set back, which the tests turn off; the cmdlet then takes ownership, runs the action, and +# fails to restore the owner. Eight of the seventeen handlers can be pinned this way. The scenario and the "without a +# later command" assertion already exist for most of them in the test files of their cmdlets +# (ParameterSets.Inheritance.Tests.ps1, PathErrors.Tests.ps1, SecurityDescriptor.Tests.ps1, FileHash.Tests.ps1), and +# Add-NTFSAccess and Remove-NTFSAccess use the scenario of the inheritance cmdlets, so this table only adds the +# later-command assertion. +# +# The other nine have no row, for one Windows rule: taking ownership only bypasses a denial of +# ReadPermissions/ChangePermissions, never of a different right. Get-NTFSAccess, Get-NTFSOrphanedAccess, +# Get-NTFSEffectiveAccess, and Get-NTFSInheritance read the security descriptor, and so does the first statement of +# InvokeAsOwner (FileSystemOwner.GetOwner), which needs the same ReadPermissions right: a denial that fails one fails +# the other, so the owner-restore step is never reached (denying ReadPermissions reports a ReadSecurityError with the +# plain UnauthorizedAccessException, and the owner is never taken). Clear-NTFSAudit, Disable-NTFSAuditInheritance, +# Enable-NTFSAuditInheritance, Add-NTFSAudit, and Remove-NTFSAudit write the SACL, which Windows grants to this +# elevated session whatever the DACL says and whatever the EnablePrivileges setting is, so the action does not fail +# to begin with (the SACL is written and the owner is left unchanged). The handlers of all seventeen carry the check. +Describe 'A later command that throws at the RestoreOwnerError record' { + BeforeDiscovery { + $holdsRestorePrivilege = Test-PrivilegeHeld -Name 'SeRestorePrivilege' + $laterCommandRestoreOwnerNames = @( + 'Clear-NTFSAccess', 'Disable-NTFSAccessInheritance', 'Enable-NTFSAccessInheritance', 'Set-NTFSInheritance', + 'Set-NTFSSecurityDescriptor', 'Get-FileHash2', 'Add-NTFSAccess', 'Remove-NTFSAccess' + ) + } + + BeforeAll { + # An owner that the user can assign only with the Restore privilege, which EnablePrivileges = $false below + # keeps disabled, matching the existing RestoreOwnerError tests of the other files. + $trustedInstaller = 'S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464' + $enablePrivileges = $privateData['EnablePrivileges'] + $privateData['EnablePrivileges'] = $false + + $laterCommandRestoreOwnerCases = @{ + 'Clear-NTFSAccess' = @{ + Prepare = { + $blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterClearBlocked' + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterClearNext' + Set-TestOwner -Sandbox $sandbox -Path $blocked -Sid 'S-1-5-32-544' + Add-NTFSAccess -Path $blocked, $next -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop + Add-TestDenyRule -Sandbox $sandbox -Path $blocked -Rights @{ 'S-1-3-4' = 'ChangePermissions' } + @{ Blocked = $blocked; Next = $next } + } + Run = { param ($Context) Clear-NTFSAccess -Path $Context.Blocked, $Context.Next -DisableInheritance -ErrorAction Continue } + ItemErrorId = 'ClearAclError' + Untouched = { param ($Context, $Emitted) (Test-ExplicitEntry -Path $Context.Next -Account 'S-1-1-0') -and -not (Get-Acl -LiteralPath $Context.Next).AreAccessRulesProtected -and $Emitted.Count -eq 0 } + } + 'Disable-NTFSAccessInheritance' = @{ + Prepare = { + $blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterDisableBlocked' + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterDisableNext' + Add-TestDenyRule -Sandbox $sandbox -Path $blocked -Rights @{ $currentUser = 'ChangePermissions' } + Set-TestOwner -Sandbox $sandbox -Path $blocked -Sid $trustedInstaller + @{ Blocked = $blocked; Next = $next } + } + Run = { param ($Context) Disable-NTFSAccessInheritance -Path $Context.Blocked, $Context.Next -ErrorAction Continue } + ItemErrorId = 'ModifySdError' + Untouched = { param ($Context, $Emitted) -not (Get-Acl -LiteralPath $Context.Next).AreAccessRulesProtected -and $Emitted.Count -eq 0 } + } + 'Enable-NTFSAccessInheritance' = @{ + Prepare = { + $blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterEnableBlocked' + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterEnableNext' + Disable-NTFSAccessInheritance -Path $blocked, $next -ErrorAction Stop + Add-TestDenyRule -Sandbox $sandbox -Path $blocked -Rights @{ $currentUser = 'ChangePermissions' } + Set-TestOwner -Sandbox $sandbox -Path $blocked -Sid $trustedInstaller + @{ Blocked = $blocked; Next = $next } + } + Run = { param ($Context) Enable-NTFSAccessInheritance -Path $Context.Blocked, $Context.Next -ErrorAction Continue } + ItemErrorId = 'ModifySdError' + Untouched = { param ($Context, $Emitted) (Get-Acl -LiteralPath $Context.Next).AreAccessRulesProtected -and $Emitted.Count -eq 0 } + } + 'Set-NTFSInheritance' = @{ + Prepare = { + $blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterSetInheritanceBlocked' + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterSetInheritanceNext' + Add-TestDenyRule -Sandbox $sandbox -Path $blocked -Rights @{ $currentUser = 'ChangePermissions' } + Set-TestOwner -Sandbox $sandbox -Path $blocked -Sid $trustedInstaller + @{ Blocked = $blocked; Next = $next } + } + Run = { param ($Context) Set-NTFSInheritance -Path $Context.Blocked, $Context.Next -AccessInheritanceEnabled $false -ErrorAction Continue } + ItemErrorId = 'ModifySdError' + Untouched = { param ($Context, $Emitted) -not (Get-Acl -LiteralPath $Context.Next).AreAccessRulesProtected -and $Emitted.Count -eq 0 } + } + 'Set-NTFSSecurityDescriptor' = @{ + Prepare = { + $blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterSdBlocked' + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterSdNext' + $descriptors = @(Get-NTFSSecurityDescriptor -Path $blocked, $next -ErrorAction Stop) + Add-NTFSAccess -SecurityDescriptor $descriptors -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop + Add-TestDenyRule -Sandbox $sandbox -Path $blocked -Rights @{ $currentUser = 'ChangePermissions' } + Set-TestOwner -Sandbox $sandbox -Path $blocked -Sid $trustedInstaller + @{ Descriptors = $descriptors; Next = $next } + } + Run = { param ($Context) Set-NTFSSecurityDescriptor -SecurityDescriptor $Context.Descriptors -ErrorAction Continue } + ItemErrorId = 'WriteSdError' + Untouched = { param ($Context, $Emitted) -not (Test-ExplicitEntry -Path $Context.Next -Account 'S-1-1-0') -and $Emitted.Count -eq 0 } + } + 'Get-FileHash2' = @{ + Prepare = { + $blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterHashBlocked' + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterHashNext' + Add-TestDenyRule -Sandbox $sandbox -Path $blocked -Rights @{ 'S-1-1-0' = 'ReadData' } + Set-TestOwner -Sandbox $sandbox -Path $blocked -Sid $trustedInstaller + @{ Blocked = $blocked; Next = $next } + } + Run = { param ($Context) Get-FileHash2 -Path $Context.Blocked, $Context.Next -ErrorAction Continue } + ItemErrorId = 'GetHashError' + # Get-FileHash2 has no ACL state of its own to read back, so the oracle is that the next path still + # exists, untouched, and that the pipeline produced no hash at all: the next path's hash would be the + # only object a later-command block ever sees, since neither -PassThru nor an output object exists + # for the blocked path in either state of the fix. Not @($Emitted): PowerShell's array operator + # raises ArgumentException for an empty generic List(Of T). + Untouched = { param ($Context, $Emitted) (Test-Path -LiteralPath $Context.Next) -and $Emitted.Count -eq 0 } + } + 'Add-NTFSAccess' = @{ + Prepare = { + $blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterAddBlocked' + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterAddNext' + Add-TestDenyRule -Sandbox $sandbox -Path $blocked -Rights @{ $currentUser = 'ChangePermissions' } + Set-TestOwner -Sandbox $sandbox -Path $blocked -Sid $trustedInstaller + @{ Blocked = $blocked; Next = $next } + } + Run = { param ($Context) Add-NTFSAccess -Path $Context.Blocked, $Context.Next -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Continue } + ItemErrorId = 'AddAceError' + Untouched = { param ($Context, $Emitted) -not (Test-ExplicitEntry -Path $Context.Next -Account 'S-1-1-0') -and $Emitted.Count -eq 0 } + } + 'Remove-NTFSAccess' = @{ + Prepare = { + $blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterRemoveBlocked' + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterRemoveNext' + Add-NTFSAccess -Path $blocked, $next -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop + Add-TestDenyRule -Sandbox $sandbox -Path $blocked -Rights @{ $currentUser = 'ChangePermissions' } + Set-TestOwner -Sandbox $sandbox -Path $blocked -Sid $trustedInstaller + @{ Blocked = $blocked; Next = $next } + } + Run = { param ($Context) Remove-NTFSAccess -Path $Context.Blocked, $Context.Next -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Continue } + ItemErrorId = 'RemoveAceError' + Untouched = { param ($Context, $Emitted) (Test-ExplicitEntry -Path $Context.Next -Account 'S-1-1-0') -and $Emitted.Count -eq 0 } + } + } + } + + AfterAll { + $privateData['EnablePrivileges'] = $enablePrivileges + } + + It '<_> should pass on what a later command raises at the RestoreOwnerError record instead of reporting it as the item''s own error, and leave the next path untouched' -Skip:(-not $holdsRestorePrivilege) -ForEach $laterCommandRestoreOwnerNames { + $case = $laterCommandRestoreOwnerCases[$_] + $context = & $case.Prepare + $Error.Clear() + $caught = $null + $emitted = New-Object -TypeName 'System.Collections.Generic.List[object]' + + try { + & $case.Run $context 2>&1 | ForEach-Object -Process { + if ($_ -is [System.Management.Automation.ErrorRecord] -and $_.FullyQualifiedErrorId -like 'RestoreOwnerError,*') { + throw 'later' + } + $emitted.Add($_) + $_ + } + } + catch { + $caught = $_ + } + + $caught.Exception.Message | Should -BeLike '*later*' + @($Error | Where-Object -FilterScript { $_.FullyQualifiedErrorId -like ('{0},*' -f $case.ItemErrorId) }) | Should -BeNullOrEmpty + (& $case.Untouched $context $emitted) | Should -BeTrue + } + + # Without a later command, the existing behavior stays: RestoreOwnerError is reported and the cmdlet goes on to + # the next path, already pinned by the tests named above for all six rows; not duplicated here. +} diff --git a/Tests/Privileges.Tests.ps1 b/Tests/Privileges.Tests.ps1 index 8d5e219..812e44f 100644 --- a/Tests/Privileges.Tests.ps1 +++ b/Tests/Privileges.Tests.ps1 @@ -663,3 +663,40 @@ Describe 'The PrivilegeControl class' { Get-BackupPrivilegeState | Should -Be 'Disabled' } } + +# Before 5.0.0, a module setting that was not a Boolean (a typo such as the string '$true', or a missing key) made +# every cmdlet derived from BaseCmdletWithPrivControl fail in BeginProcessing with an InvalidCastException or a +# NullReferenceException before anything useful was said, and Enable-Privileges had its own try/catch for the same +# cast that never ran, because BeginProcessing always failed first. One EnablePrivilegesSettingInvalid error now +# names the setting instead, for a cmdlet and for Enable-Privileges alike. The test needs no elevation. +Describe 'A malformed EnablePrivileges setting' { + BeforeAll { + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'MalformedSetting' + } + + It 'Should stop a cmdlet and Enable-Privileges with one EnablePrivilegesSettingInvalid error for , not an InvalidCastException or a NullReferenceException' -ForEach @( + @{ Case = 'a string value'; Setting = 'maybe' } + @{ Case = 'a missing value'; Setting = $null } + ) { + $privateData['EnablePrivileges'] = $Setting + + try { + $cmdletFailure = { Get-NTFSInheritance -Path $file -ErrorAction Stop } | Should -Throw -PassThru + $enableFailure = { Enable-Privileges -ErrorAction Stop } | Should -Throw -PassThru + } + finally { + $privateData['EnablePrivileges'] = $enablePrivileges + } + + foreach ($failure in $cmdletFailure, $enableFailure) { + $failure.FullyQualifiedErrorId | Should -BeLike 'EnablePrivilegesSettingInvalid,*' + $failure.Exception.Message | Should -BeLike '*EnablePrivileges*' + $failure.Exception | Should -Not -BeOfType [System.InvalidCastException] + $failure.Exception | Should -Not -BeOfType [System.NullReferenceException] + } + + # Restoring the setting must leave the session usable again, for this test and the ones that follow it. + $privateData['EnablePrivileges'] | Should -Be $enablePrivileges + { Get-NTFSInheritance -Path $file -ErrorAction Stop } | Should -Not -Throw + } +} diff --git a/Tests/Repository.Tests.ps1 b/Tests/Repository.Tests.ps1 index b0efc64..622c652 100644 --- a/Tests/Repository.Tests.ps1 +++ b/Tests/Repository.Tests.ps1 @@ -97,7 +97,7 @@ Describe 'Release metadata' { # The PowerShell Gallery doesn't accept a version twice. Add every published version to this list # (Docs/Contributing/05-Releasing.md). It 'Should not reuse a version that the PowerShell Gallery already has' { - $publishedVersions = '4.0', '4.2.2', '4.2.3', '4.2.4', '4.2.5', '4.2.6', '5.0.0-rc1', '5.0.0-rc2', '5.0.0-rc3', '5.0.0-rc4', '5.0.0-rc5', '5.0.0-rc6', '5.0.0-rc7' + $publishedVersions = '4.0', '4.2.2', '4.2.3', '4.2.4', '4.2.5', '4.2.6', '5.0.0-rc1', '5.0.0-rc2', '5.0.0-rc3', '5.0.0-rc4', '5.0.0-rc5', '5.0.0-rc6', '5.0.0-rc7', '5.0.0-rc8' $publishedVersions | Should -Not -Contain $version } diff --git a/Tests/SecurityDescriptor.Tests.ps1 b/Tests/SecurityDescriptor.Tests.ps1 index 32ad555..e2c1156 100644 --- a/Tests/SecurityDescriptor.Tests.ps1 +++ b/Tests/SecurityDescriptor.Tests.ps1 @@ -73,6 +73,24 @@ Describe 'Get-NTFSSecurityDescriptor' { } Describe 'Set-NTFSSecurityDescriptor' { + Context 'Parameter declaration' { + # Its siblings (Get-NTFSSecurityDescriptor and the cmdlets with a -SecurityDescriptor parameter set) take + # their first positional parameter at 1; -SecurityDescriptor was the only one at 2 (6c), cosmetic only: a + # positional call with one argument already bound to it, because it was the only positional parameter. + It 'Should declare -SecurityDescriptor at position 1, and still accept it positionally' { + (Get-Command -Name 'Set-NTFSSecurityDescriptor').Parameters['SecurityDescriptor'].Attributes.Position | Should -Be 1 + + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'PositionalWrite' + Assert-TestSandboxPath -Sandbox $sandbox -Path $file + $sd = Get-NTFSSecurityDescriptor -Path $file + Add-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData + + Set-NTFSSecurityDescriptor $sd + + @(Get-EveryoneRule -Path $file) | Should -HaveCount 1 + } + } + Context 'When a cmdlet added an access entry to the descriptor' { It 'Should write the entry as the only explicit entry of the item' { $file = New-TestSandboxItem -Sandbox $sandbox -Name 'AddedEntry'