The simple task of getting the owner of a file is not supported by the "native" .Net Framework v1.1, and is poorly supported in .Net Framework v2.0. That means that instead of .Net "managed code", we are forced to use the Windows API.
Note: The new GetOwner() method of the FileSecurity and DirectorySecurity classes (in v2.0 Framework only) throws an exception when it attempts to resolve an unknown Security Identifier (SID) and can not be used to resolve an owner of a remote file
Note: There are other options available... You can use WMI's Win32_LogicalFileSecuritySetting methods, see Introduction to Windows Management Instrumentation for a discussion of WMI.
The ownership of a file is held inside a "Security Descriptor" for each file (on an NTFS partition). So, we use a bunch of Windows API calls to get the owner of the file from the Security Descriptor. All user accounts in Windows are stored behind the scenes as Security Identifiers (SIDs). When we use the API to get the file's owner, it returns the SID. We then have to look up the name of the user account associated with that SID.
API Declarations
Let's start with the API "boiler plate" stuff:
Imports System.Runtime.InteropServices
Imports System.ComponentModel
Imports System.IO
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 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 LookupAccountSid Lib "advapi32.dll" ( _
ByVal lpSystemName As String, _
ByVal lpSid As IntPtr, _
ByVal lpName As String, _
ByRef cchName As Integer, _
ByVal lpReferenceDomainName As String, _
ByRef cchReferencedDomainName As Integer, _
ByRef peUse As Integer _
) As Boolean
Private Declare Auto Function ConvertSidToStringSid Lib "advapi32.dll" ( _
ByVal Sid As IntPtr, _
ByRef StringSid As IntPtr _
) As Boolean
Private Const NAME_SIZE As Integer = 64
API Documentation Links
The Example Code
The first thing we do is use the GetNamedSecurityInfo API to get the SID of the owner. Notice the use of FreeHGlobal to clean up memory after we're done.
Dim UserName, MachineName, SidString, name, domain_name As String
Dim OwnerSid, SD, StringPtr As IntPtr
Dim ret, LastError, name_len, domain_len, peUse As Integer
Dim Win32Error As Win32Exception
Dim ObjectType As SE_OBJECT_TYPE
ObjectType = SE_OBJECT_TYPE.SE_FILE_OBJECT
' Get the Owner's SID.
ret = GetNamedSecurityInfo(Path, ObjectType, _
SECURITY_INFORMATION.OWNER_SECURITY_INFORMATION, OwnerSid, Nothing, _
Nothing, Nothing, SD)
If ret <> 0 Then
Return "Error"
End If
Marshal.FreeHGlobal(SD)
To resolve the SID into a human-readable name, we use the LookupAccountSid API. It's a strange function in that you must reserve room for the strings that it uses. Alternately, you could call the API with NULLs for the strings and it will do a "dry run" that only returns the correct sizes for the strings. This would require two calls... one to get the sizes, and a second one to get the SID. Look at Checking File Permissions for an example of using this technique (using LookupAccountName).
The error code 1332 means that we could not resolve the SID... this is not an error, per se... it's an indication that you've got a "dead SID". A dead SID would occur if you deleted an account, yet that old account still is the owner of some files.
' are we doing this remotely? If so, we need to resolve the SID remotely
If Path.StartsWith("\\") Then
MachineName = Path.Split("\")(2)
Else
MachineName = ""
End If
name_len = NAME_SIZE
domain_len = NAME_SIZE
name = Space(name_len)
domain_name = Space(domain_len)
' look up the Account associated with that SID
If LookupAccountSid(MachineName, OwnerSid, name, name_len, domain_name, _
domain_len, peUse) = False Then
LastError = Marshal.GetLastWin32Error()
' this means we've got a "dead" SID, so just return the SID as a string
If LastError = 1332 Then
' convert the SID to a string
If ConvertSidToStringSid(OwnerSid, StringPtr) = False Then
Return "Error"
End If
SidString = Marshal.PtrToStringAuto(StringPtr)
Marshal.FreeHGlobal(StringPtr)
domain_len = 0
name = SidString
name_len = Len(name)
Else
Return "Error"
End If
End If
' put the name parts together
If domain_len > 0 Then
UserName = Left(domain_name, domain_len) & "\" & Left(name, name_len)
Else
UserName = Left(name, name_len)
End If
Notice the use Marshal.PtrToStringAuto to copy the string pointer returned by the API into a normal everyday .Net Framework String.
Download/Links
Download the complete VB.Net Source code example used in this article: GetOwner.zip