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