Checking File Permissions

There are a few simple built-in .Net techniques for checking to see if the current user has read/write permission against a file. However, if you want to see if *somebody else* has permissions, then none of the built-in .Net functions are gonna be of much use. You're forced into the dark world of Windows APIs.

Let's start with how permissions work behind the scenes. The NTFS file system attaches a collection of Access Control Entries (ACE) to each file and directory. The ACE includes all of the stuff you're probably already familiar with when setting file permissions manually... The User Name, the Permission (i.e. Modify), and whether or not this entry is an "allow" or "deny". This collection of ACEs is called the Discretionary Access Control List (DACL).

Permissions are recorded in the ACE as numbers, so we need the following table of commonly-used permissions and their numeric equivalents.

Full Control = 20321127
Modify = 1245631
Read & Execute = 1179817
Read = 1179785
Write = 1179926
Execute = 1179808

API Declarations

The functions that retrieve the DACL, read the ACEs, and compare the permissions are all Windows API calls. So let's start with declaring all of our API stuff:

Imports System.Runtime.InteropServices
Imports System.ComponentModel

<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Auto)> _
Private Structure TRUSTEE
   Dim pMultipleTrustee As IntPtr
   Dim MultipleTrusteeOperation As Integer
   Dim TrusteeForm As Integer
   Dim TrusteeType As Integer
   Dim ptstrName As IntPtr
End Structure

Private Enum SE_OBJECT_TYPE As Integer
   SE_UNKNOWN_OBJECT_TYPE = 0
   SE_FILE_OBJECT
   SE_SERVICE
   SE_PRINTER
   SE_REGISTRY_KEY
   SE_LMSHARE
   SE_KERNEL_OBJECT
   SE_WINDOW_OBJECT
   SE_DS_OBJECT
   SE_DS_OBJECT_ALL
   SE_PROVIDER_DEFINED_OBJECT
   SE_WMIGUID_OBJECT
   SE_REGISTRY_WOW64_32
End Enum

Private Enum SECURITY_INFORMATION As Integer
   OWNER_SECURITY_INFORMATION = 1
   GROUP_SECURITY_INFORMATION = 2
   DACL_SECURITY_INFORMATION = 4
   SACL_SECURITY_INFORMATION = 8
   PROTECTED_SACL_SECURITY_INFORMATION = 16
   PROTECTED_DACL_SECURITY_INFORMATION = 32
   UNPROTECTED_SACL_SECURITY_INFORMATION = 64
   UNPROTECTED_DACL_SECURITY_INFORMATION = 128
End Enum

Private Declare Auto Sub BuildTrusteeWithSid Lib "advapi32.dll" ( _
   ByVal pTrustee As IntPtr, _
   ByVal pSid As IntPtr _
)

Private Declare Auto Function GetNamedSecurityInfo Lib "advapi32.dll" ( _
   ByVal pObjectName As String, _
   ByVal ObjectType As SE_OBJECT_TYPE, _
   ByVal SecurityInfo As SECURITY_INFORMATION, _
   ByRef ppsidOwner As IntPtr, _
   ByRef ppsidGroup As IntPtr, _
   ByRef ppDacl As IntPtr, _
   ByRef ppSacl As IntPtr, _
   ByRef ppSecurityDescriptor As IntPtr _
) As Integer

Private Declare Auto Function GetEffectiveRightsFromAcl Lib "advapi32.dll" ( _
   ByVal pacl As IntPtr, _
   ByVal pTrustee As IntPtr, _
   ByRef pAccessRights As Integer _
) As Integer

Private Declare Auto Function LookupAccountName Lib "advapi32.dll" ( _
   ByVal lpSystemName As String, _
   ByVal lpAccountName As String, _
   ByVal Sid As IntPtr, _
   ByRef cbSid As Integer, _
   ByVal lpReferenceDomainName As String, _
   ByRef cchReferencedDomainName As Integer, _
   ByRef peUse As Integer _
) As Boolean

API Documentation Links

The Example Code

The next step is to build a "Trustee"... (a Trustee is the internal name for the User/Group that is found inside an ACE). This requires that we use LookupAccountName to retrieve the Security Identifier (SID) of the user account being used to test the permissions. Next we use BuildTrusteeWithSid to convert the SID into the Trustee structure

Dim pTrustee, pSID As IntPtr
Dim Domain As String
Dim lenDomain, lenSid, peUse, LastError As Integer
Dim Win32Error As Win32Exception
Dim t As TRUSTEE

' do a "dry run" to get the size of the SID and Domain string
LookupAccountName(Nothing, UserName, Nothing, lenSid, Nothing, lenDomain, peUse)
Domain = Space(lenDomain)
pSID = Marshal.AllocHGlobal(lenSid)

' do it again, for real this time
If LookupAccountName(Nothing, UserName, pSID, lenSid, Domain, lenDomain, peUse) _
 = False Then
   LastError = Marshal.GetLastWin32Error()
   Win32Error = New Win32Exception(LastError)
   Throw New Exception(Win32Error.Message)
End If

' Build a trustee
pTrustee = Marshal.AllocHGlobal(Marshal.SizeOf(t))
BuildTrusteeWithSid(pTrustee, pSID)

OK, now we need to get the DACL from the file. The DACL is found inside the Security Descriptor for each file (on an NTFS partition). The GetNamedSecurityInfo API is used to retrieve both the Security Descriptor and the DACL.

Dim pDACL, pSD As IntPtr
Dim AccessMask, Mask, ret As Integer
Dim Win32Error As Win32Exception

' Get the DACL from the file
ret = GetNamedSecurityInfo(Path, SE_OBJECT_TYPE.SE_FILE_OBJECT, _
 SECURITY_INFORMATION.DACL_SECURITY_INFORMATION, Nothing, Nothing, _
 pDACL, Nothing, pSD)
If ret <> 0 Then
   Win32Error = New Win32Exception(ret)
   Throw New Exception(Win32Error.Message)
End If

The last step is to use GetEffectiveRightsFromAcl to get all of the permissions that the user is allowed to perform based upon the DACL from the file. Then we compare this "permissions mask" that we just generated to the numeric equivalent of the permission that you provided (i.e. Read = 1179785). If the two masks match, that permission is allowed.

' Get the Access Mask using the supplied user account
ret = GetEffectiveRightsFromAcl(pDACL, pTrustee, Mask)
If ret <> 0 Then
   Marshal.FreeHGlobal(pSD)
   Win32Error = New Win32Exception(ret)
   Throw New Exception(Win32Error.Message)
End If

' We don't want a memory leak!
Marshal.FreeHGlobal(pSD)

' Let's see if we've got a match!
If (AccessMask And Mask) = AccessMask Then
    Return True
Else
    Return False
End If

Downloads/Links

Read a related article on Setting File Permissions
Download the complete VB.Net Source code example used in this article: CheckPerm.zip