Browse Source

fix(effective-access): answer a user who isn't an administrator for this computer

On a computer in a domain, Get-NTFSEffectiveAccess wrote "Access is denied"
and no result for every user who wasn't an administrator of the computer,
also for the default -ServerName localhost and for every other name of this
computer. The remote interface of the authorization manager of a computer
answers only its administrators and the members of Access Control Assistance
Operators, and a computer in a domain offers that interface to every caller.
On a computer outside a domain the interface is not reachable, so the cmdlet
already used the local authorization manager there, which is why the tests
passed on the development host and on the CI runners.

For a name of this computer, the cmdlet now uses the local authorization
manager when the remote one refuses the user. That manager is the one the name
asks for, and it answered correctly in every probe on Windows Server 2019,
2022, and 2025 and on Windows 11: for a standard domain user, a local standard
user, and an administrator with a filtered token, for the user's own account,
Everyone, the Administrator of the computer, and the Administrator and Domain
Users of the domain. For the name of another computer, the denial stays an
error, as the cmdlet page and the live test of the delegated account describe.

Twenty tests of the suite failed in the basic-user mode on every domain-joined
machine of the operating-system matrix, with the published 5.0.0-rc7 code and
with the code before this change, and pass with it (Windows Server 2019: basic
user 782 and 780 passed, 0 failed, in Windows PowerShell and PowerShell 7). A
new live test runs the case as the administrator of the file server, who isn't
an administrator of the client.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
pull/119/head
Raimund Andree 2 days ago
parent
commit
fdd7a8bfdf
  1. 8
      CHANGELOG.md
  2. 6
      Docs/Cmdlets/Get-NTFSEffectiveAccess.md
  3. 9
      NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml
  4. 33
      Security2/Win32/Lib.cs
  5. 30
      Tests/Lab/NTFSSecurity.Live.Tests.ps1

8
CHANGELOG.md

@ -199,6 +199,14 @@ The format is based on
on computers where Windows takes an empty name for this one. An empty name
never asks the remote interface of the authorization manager now: the
cmdlet warns and returns the result of this computer on every computer
- Fix `Get-NTFSEffectiveAccess` for a user who isn't an administrator on a
computer in a domain. For a name of this computer, such as the default
`localhost`, the cmdlet wrote the error "Access is denied" and no result for
every account, because the remote authorization manager of a computer
answers only its administrators and the members of Access Control Assistance
Operators. It now uses the local authorization manager of this computer when
the remote one refuses the user, as it already did when the remote one can't
be reached. For the name of another computer, the error stays
- Fix `Get-NTFSOwner`, which wrote a "The pipeline has been stopped" error
for every path when a command such as `Select-Object -First 1` stopped the
pipeline, and which repeated a failed read instead of reporting the

6
Docs/Cmdlets/Get-NTFSEffectiveAccess.md

@ -31,7 +31,7 @@ Calculates the rights an account really has on a file or a folder and writes the
The calculation covers the NTFS permissions of the item only. Share permissions are stored in a separate security descriptor and are not part of the result, so access over a network share can be more restrictive than this cmdlet reports.
When `-Account` is omitted, the account that runs the session is used. `-ServerName` selects the computer whose authorization manager resolves the group memberships of the account and defaults to `localhost`; when the remote authorization manager of the named computer cannot be reached, the cmdlet falls back to the local one and warns that the result is based on the group memberships known on this computer and may be inaccurate. The warning names the computer that couldn't be reached. For a name of this computer, such as `localhost`, `.`, or its computer name, the local authorization manager gives the result of the named computer, so the cmdlet doesn't warn. The authorization manager of the named computer answers only the administrators of that computer and the members of its local group Access Control Assistance Operators; for any other account, the cmdlet writes an error and doesn't fall back. Reading effective access relies on the Security privilege, and the cmdlet warns when the account does not hold it or the privilege is disabled.
When `-Account` is omitted, the account that runs the session is used. `-ServerName` selects the computer whose authorization manager resolves the group memberships of the account and defaults to `localhost`; when the remote authorization manager of the named computer cannot be reached, the cmdlet falls back to the local one and warns that the result is based on the group memberships known on this computer and may be inaccurate. The warning names the computer that couldn't be reached. For a name of this computer, such as `localhost`, `.`, or its computer name, the local authorization manager gives the result of the named computer, so the cmdlet doesn't warn. The remote authorization manager of a computer answers only the administrators of that computer and the members of its local group Access Control Assistance Operators. For a name of this computer, the cmdlet uses the local authorization manager when the remote one refuses the user who runs the cmdlet, so a user who isn't an administrator gets the result on a computer in a domain, too; for the name of another computer, the cmdlet writes an error and doesn't fall back. Reading effective access relies on the Security privilege, and the cmdlet warns when the account does not hold it or the privilege is disabled.
When `-Path` is omitted, the cmdlet calculates the effective access to the current location. In the `SecurityDescriptor` parameter set, it calculates the effective access from a `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, without reading the item again.
@ -139,7 +139,7 @@ Accept wildcard characters: False
### -ServerName
Specifies the computer whose authorization manager resolves the group memberships of the account. The default is `localhost`. Name the computer that stores the item when you query a network path, because the group memberships known there determine the result; if that computer cannot be reached, the cmdlet falls back to the local authorization manager and warns that the result may be inaccurate. The authorization manager of a computer answers only its administrators and the members of its local group Access Control Assistance Operators; for any other account, the cmdlet writes a non-terminating `GetEffectiveAccessError` with the message "Access is denied" and returns no result for the item.
Specifies the computer whose authorization manager resolves the group memberships of the account. The default is `localhost`. Name the computer that stores the item when you query a network path, because the group memberships known there determine the result; if that computer cannot be reached, the cmdlet falls back to the local authorization manager and warns that the result may be inaccurate. The remote authorization manager of a computer answers only its administrators and the members of its local group Access Control Assistance Operators. For the name of another computer, any other user gets a non-terminating `GetEffectiveAccessError` with the message "Access is denied" and no result for the item; for a name of this computer, the cmdlet uses the local authorization manager instead.
```yaml
Type: String
@ -186,6 +186,8 @@ Before 5.0.0, `-ExcludeNoneAccessEntries` had no effect, and the cmdlet returned
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`.
## RELATED LINKS
[Get-NTFSAccess](Get-NTFSAccess.md)

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

@ -4957,7 +4957,7 @@ PS C:\&gt; Get-NTFSAudit -SecurityDescriptor $sd</dev:code>
<maml:description>
<maml:para>Calculates the rights an account really has on a file or a folder and writes the result as a single `Security2.FileSystemAccessRule2` object per item. The cmdlet evaluates the complete discretionary access control list (DACL) of the item against the group memberships of the account with the Windows Authorization API, so allow entries, deny entries, and inherited entries are combined the same way the Windows access check combines them. This is the equivalent of the "Effective Access" tab of the advanced security dialog.</maml:para>
<maml:para>The calculation covers the NTFS permissions of the item only. Share permissions are stored in a separate security descriptor and are not part of the result, so access over a network share can be more restrictive than this cmdlet reports.</maml:para>
<maml:para>When `-Account` is omitted, the account that runs the session is used. `-ServerName` selects the computer whose authorization manager resolves the group memberships of the account and defaults to `localhost`; when the remote authorization manager of the named computer cannot be reached, the cmdlet falls back to the local one and warns that the result is based on the group memberships known on this computer and may be inaccurate. The warning names the computer that couldn't be reached. For a name of this computer, such as `localhost`, `.`, or its computer name, the local authorization manager gives the result of the named computer, so the cmdlet doesn't warn. The authorization manager of the named computer answers only the administrators of that computer and the members of its local group Access Control Assistance Operators; for any other account, the cmdlet writes an error and doesn't fall back. Reading effective access relies on the Security privilege, and the cmdlet warns when the account does not hold it or the privilege is disabled.</maml:para>
<maml:para>When `-Account` is omitted, the account that runs the session is used. `-ServerName` selects the computer whose authorization manager resolves the group memberships of the account and defaults to `localhost`; when the remote authorization manager of the named computer cannot be reached, the cmdlet falls back to the local one and warns that the result is based on the group memberships known on this computer and may be inaccurate. The warning names the computer that couldn't be reached. For a name of this computer, such as `localhost`, `.`, or its computer name, the local authorization manager gives the result of the named computer, so the cmdlet doesn't warn. The remote authorization manager of a computer answers only the administrators of that computer and the members of its local group Access Control Assistance Operators. For a name of this computer, the cmdlet uses the local authorization manager when the remote one refuses the user who runs the cmdlet, so a user who isn't an administrator gets the result on a computer in a domain, too; for the name of another computer, the cmdlet writes an error and doesn't fall back. Reading effective access relies on the Security privilege, and the cmdlet warns when the account does not hold it or the privilege is disabled.</maml:para>
<maml:para>When `-Path` is omitted, the cmdlet calculates the effective access to the current location. In the `SecurityDescriptor` parameter set, it calculates the effective access from a `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, without reading the item again.</maml:para>
</maml:description>
<command:syntax>
@ -5001,7 +5001,7 @@ PS C:\&gt; Get-NTFSAudit -SecurityDescriptor $sd</dev:code>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="none">
<maml:name>ServerName</maml:name>
<maml:description>
<maml:para>Specifies the computer whose authorization manager resolves the group memberships of the account. The default is `localhost`. Name the computer that stores the item when you query a network path, because the group memberships known there determine the result; if that computer cannot be reached, the cmdlet falls back to the local authorization manager and warns that the result may be inaccurate. The authorization manager of a computer answers only its administrators and the members of its local group Access Control Assistance Operators; for any other account, the cmdlet writes a non-terminating `GetEffectiveAccessError` with the message "Access is denied" and returns no result for the item.</maml:para>
<maml:para>Specifies the computer whose authorization manager resolves the group memberships of the account. The default is `localhost`. Name the computer that stores the item when you query a network path, because the group memberships known there determine the result; if that computer cannot be reached, the cmdlet falls back to the local authorization manager and warns that the result may be inaccurate. The remote authorization manager of a computer answers only its administrators and the members of its local group Access Control Assistance Operators. For the name of another computer, any other user gets a non-terminating `GetEffectiveAccessError` with the message "Access is denied" and no result for the item; for a name of this computer, the cmdlet uses the local authorization manager instead.</maml:para>
</maml:description>
<command:parameterValue required="true" variableLength="false">String</command:parameterValue>
<dev:type>
@ -5052,7 +5052,7 @@ PS C:\&gt; Get-NTFSAudit -SecurityDescriptor $sd</dev:code>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="none">
<maml:name>ServerName</maml:name>
<maml:description>
<maml:para>Specifies the computer whose authorization manager resolves the group memberships of the account. The default is `localhost`. Name the computer that stores the item when you query a network path, because the group memberships known there determine the result; if that computer cannot be reached, the cmdlet falls back to the local authorization manager and warns that the result may be inaccurate. The authorization manager of a computer answers only its administrators and the members of its local group Access Control Assistance Operators; for any other account, the cmdlet writes a non-terminating `GetEffectiveAccessError` with the message "Access is denied" and returns no result for the item.</maml:para>
<maml:para>Specifies the computer whose authorization manager resolves the group memberships of the account. The default is `localhost`. Name the computer that stores the item when you query a network path, because the group memberships known there determine the result; if that computer cannot be reached, the cmdlet falls back to the local authorization manager and warns that the result may be inaccurate. The remote authorization manager of a computer answers only its administrators and the members of its local group Access Control Assistance Operators. For the name of another computer, any other user gets a non-terminating `GetEffectiveAccessError` with the message "Access is denied" and no result for the item; for a name of this computer, the cmdlet uses the local authorization manager instead.</maml:para>
</maml:description>
<command:parameterValue required="true" variableLength="false">String</command:parameterValue>
<dev:type>
@ -5116,7 +5116,7 @@ PS C:\&gt; Get-NTFSAudit -SecurityDescriptor $sd</dev:code>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="none">
<maml:name>ServerName</maml:name>
<maml:description>
<maml:para>Specifies the computer whose authorization manager resolves the group memberships of the account. The default is `localhost`. Name the computer that stores the item when you query a network path, because the group memberships known there determine the result; if that computer cannot be reached, the cmdlet falls back to the local authorization manager and warns that the result may be inaccurate. The authorization manager of a computer answers only its administrators and the members of its local group Access Control Assistance Operators; for any other account, the cmdlet writes a non-terminating `GetEffectiveAccessError` with the message "Access is denied" and returns no result for the item.</maml:para>
<maml:para>Specifies the computer whose authorization manager resolves the group memberships of the account. The default is `localhost`. Name the computer that stores the item when you query a network path, because the group memberships known there determine the result; if that computer cannot be reached, the cmdlet falls back to the local authorization manager and warns that the result may be inaccurate. The remote authorization manager of a computer answers only its administrators and the members of its local group Access Control Assistance Operators. For the name of another computer, any other user gets a non-terminating `GetEffectiveAccessError` with the message "Access is denied" and no result for the item; for a name of this computer, the cmdlet uses the local authorization manager instead.</maml:para>
</maml:description>
<command:parameterValue required="true" variableLength="false">String</command:parameterValue>
<dev:type>
@ -5168,6 +5168,7 @@ PS C:\&gt; Get-NTFSAudit -SecurityDescriptor $sd</dev:code>
<maml:para>Reading effective access needs the Security privilege. In a session that does not hold it, the cmdlet warns before it starts and the calculation may fail with an error. Use `Enable-Privileges` in an elevated session to enable the privilege, and `Get-Privileges` to see which privileges the session holds. When the calculation fails, the error names the cause that Windows reported, such as a security descriptor without an owner; before 5.0.0, it blamed a missing Security privilege whenever the privilege wasn't enabled.</maml:para>
<maml:para>Before 5.0.0, `-ExcludeNoneAccessEntries` had no effect, and the cmdlet returned nothing without `-Path` or for `-SecurityDescriptor`. When the computer of `-ServerName` couldn't be reached, the cmdlet warned that it had calculated the result on this computer, but returned no access instead of that result.</maml:para>
<maml:para>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.</maml:para>
<maml:para>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`.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>

33
Security2/Win32/Lib.cs

@ -18,6 +18,10 @@ namespace Security2
IntPtr pGrantedAccess = IntPtr.Zero;
IntPtr pErrorSecObj = IntPtr.Zero;
// Whether the remote resource manager is the one of this computer. Its remote interface answers only the
// administrators of the computer and the members of Access Control Assistance Operators.
bool remoteResourceManagerIsLocal;
#region GetInheritedFrom
// Returns the source of each entry of the DACL, or of the SACL for audit entries, in the order of the ACL. Before
// 5.0.0-rc6, a descriptor with a SACL returned the sources of the audit entries also for the access entries.
@ -194,14 +198,19 @@ namespace Security2
if (AuthzInitializeRemoteResourceManager(pRpcInitInfo.ToIntPtr(), out authzRM))
{
remoteServerAvailable = true;
remoteResourceManagerIsLocal = IsLocalComputer(serverName);
return;
}
int error = Marshal.GetLastWin32Error();
bool isLocalComputer = IsLocalComputer(serverName);
// The computer can't be resolved or reached (RPC server unavailable), or it doesn't offer the remote
// interface (endpoint not registered); the local authorization manager calculates the result instead.
if (error != Win32Error.EPT_S_NOT_REGISTERED && error != Win32Error.RPC_S_SERVER_UNAVAILABLE)
// This computer can also refuse the caller, who isn't one of its administrators; its own manager
// answers then, too.
if (error != Win32Error.EPT_S_NOT_REGISTERED && error != Win32Error.RPC_S_SERVER_UNAVAILABLE &&
!(isLocalComputer && error == Win32Error.ERROR_ACCESS_DENIED))
{
throw new Win32Exception(error);
}
@ -209,12 +218,17 @@ namespace Security2
// The local authorization manager is the one of this computer, so its result is accurate for any name
// of this computer. Before 5.0.0-rc7, only localhost in lowercase counted, and the cmdlet warned for
// the others, such as ., the computer name, or LOCALHOST.
if (IsLocalComputer(serverName))
if (isLocalComputer)
{
remoteServerAvailable = true;
}
}
GetEffectivePermissions_AuthzInitializeLocalResourceManager();
}
private void GetEffectivePermissions_AuthzInitializeLocalResourceManager()
{
//
// As a fallback we do AuthzInitializeResourceManager. But the results can be inaccurate.
//
@ -248,6 +262,21 @@ namespace Security2
{
Win32Exception win32Expn = new Win32Exception(Marshal.GetLastWin32Error());
// A computer in a domain offers the remote interface of its authorization manager to every caller, but
// answers only its administrators and the members of Access Control Assistance Operators; any other
// account gets "Access is denied", whichever account the check is for. For a name of this computer, the
// local authorization manager is the manager of that computer and answers every caller. For another
// computer, the denial stays an error: no access instead would be a wrong result.
if (win32Expn.NativeErrorCode == Win32Error.ERROR_ACCESS_DENIED && remoteResourceManagerIsLocal)
{
remoteResourceManagerIsLocal = false;
userClientCtxt = IntPtr.Zero;
authzRM.Dispose();
GetEffectivePermissions_AuthzInitializeLocalResourceManager();
GetEffectivePermissions_AuthzInitializeContextFromSid(id);
return;
}
if (win32Expn.NativeErrorCode != Win32Error.RPC_S_SERVER_UNAVAILABLE)
{
throw win32Expn;

30
Tests/Lab/NTFSSecurity.Live.Tests.ps1

@ -463,6 +463,36 @@ Describe 'Get-NTFSEffectiveAccess as an account that is not an administrator of
}
}
Describe 'Get-NTFSEffectiveAccess for a domain account as an account that is not an administrator of the client' -Tag 'ServerAdmin' -Skip:(-not $configured) {
# The remote authorization manager of a computer answers only its administrators and the members of its group Access
# Control Assistance Operators, and a computer in a domain offers it to every caller. Before 5.0.0-rc7, the cmdlet
# wrote "Access is denied" for this computer, too, so the default -ServerName (localhost) failed for every user who
# isn't an administrator of the client. Now the local authorization manager of the client answers, which is the
# manager that the name asks for.
BeforeAll {
$path = Get-LabPath -RelativePath 'Case3\EffectiveAccess'
$subject = $configuration.Accounts.Subject.Name
}
It 'Should return the rights through the domain groups without -ServerName, like for an administrator of the client' {
$result = @(Get-NTFSEffectiveAccess -Path $path -Account $subject -WarningAction SilentlyContinue -ErrorVariable operationErrors -ErrorAction SilentlyContinue)
Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty
$result | Should -HaveCount 1
Format-LabRight -Right $result[0].AccessRights | Should -Be (Format-LabRight -Right $configuration.EffectiveAccess.ClientRights)
}
It 'Should return the same rights without a warning for the name of the client' {
$result = @(Get-NTFSEffectiveAccess -Path $path -Account $subject -ServerName $env:COMPUTERNAME -WarningVariable operationWarnings -WarningAction SilentlyContinue -ErrorVariable operationErrors -ErrorAction SilentlyContinue)
Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty
# The warning that the account doesn't hold the Security privilege is allowed; a warning about an unreachable computer isn't.
($operationWarnings.Message -join '|') | Should -Not -BeLike '*can''t be reached*'
$result | Should -HaveCount 1
Format-LabRight -Right $result[0].AccessRights | Should -Be (Format-LabRight -Right $configuration.EffectiveAccess.ClientRights)
}
}
Describe 'Get-NTFSOrphanedAccess with the entry of a deleted domain account on a share folder' -Tag 'Admin' -Skip:(-not $configured) {
BeforeAll {
$folder = Get-LabPath -RelativePath 'Case4\OrphanedAccess'

Loading…
Cancel
Save