File.Exists with File/Dir Permissions

Posted on: February 22, 2016 1:48:16 AM
While attempting to troubleshoot a failing unit test on my work machine that didn't fail on my peer's machine or on the build machine I came across an interesting situation. Basically, I found the only difference between my code base and everyone else's was it's location on my computer. Mine was in my Windows user directory whereas everyone else's existed in custom folders directly off the root directory. I began to investigate permissions because of this scenario and what I found was very interesting and a bit misleading around System.IO.File.Exists. The MSDN Documenation for File.Exists says, "If the caller does not have sufficient permissions to read the specified file, no exception is thrown and the method returns false regardless of the existence of path." Based on this line, I wrote out a simple test program:
using System;
using System.DirectoryServices;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;

namespace ConsoleApplication1
{
    internal class Program
    {
        private const string DirName = "TestDir";
        private const string FileName = "File.txt";
        private const string Password = "Password1";
        private const string UserName = "PermissionTestUser";
        private static WindowsImpersonationContext Identity = null;
        private static IntPtr LogonToken = IntPtr.Zero;

        public enum LogonProvider
        {
            LOGON32_PROVIDER_DEFAULT = 0,
            LOGON32_PROVIDER_WINNT35 = 1,
            LOGON32_PROVIDER_WINNT40 = 2,
            LOGON32_PROVIDER_WINNT50 = 3
        };

        public enum LogonType
        {
            LOGON32_LOGON_INTERACTIVE = 2,
            LOGON32_LOGON_NETWORK = 3,
            LOGON32_LOGON_BATCH = 4,
            LOGON32_LOGON_SERVICE = 5,
            LOGON32_LOGON_UNLOCK = 7,
            LOGON32_LOGON_NETWORK_CLEARTEXT = 8, // Win2K or higher
            LOGON32_LOGON_NEW_CREDENTIALS = 9 // Win2K or higher
        };

        public static void Main(string[] args)
        {
            string filePath = Path.Combine(DirName, FileName);
            try
            {
                CreateUser();
                CreateDir();
                CreateFile(filePath);

                // grant user full control to the dir
                SetAccess(DirName, AccessControlType.Allow);
                // deny user full control to the file
                SetAccess(filePath, AccessControlType.Deny);

                // impersonate user
                Impersonate();
                Console.WriteLine("File.Exists (with dir permissions): {0}", File.Exists(filePath));
                UndoImpersonate();

                // deny access to dir
                SetAccess(DirName, AccessControlType.Deny);

                // impersonate user
                Impersonate();
                Console.WriteLine("File.Exists (without dir permissions): {0}", File.Exists(filePath));
                UndoImpersonate();
            }
            finally
            {
                UndoImpersonate();
                DeleteDir();
                DeleteUser();
            }
        }

        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        private static extern bool CloseHandle(IntPtr handle);

        private static void CreateDir()
        {
            Directory.CreateDirectory(DirName);
        }

        private static void CreateFile(string path)
        {
            File.Create(path).Dispose();
        }

        private static void CreateUser()
        {
            DirectoryEntry ad = new DirectoryEntry("WinNT://" + Environment.MachineName + ",computer");
            DirectoryEntry newUser = ad.Children.Add(UserName, "user");
            newUser.Invoke("SetPassword", new object[] { Password });
            newUser.Invoke("Put", new object[] { "Description", "Test user" });
            newUser.CommitChanges();
        }

        private static void DeleteDir()
        {
            Directory.Delete(DirName, true);
        }

        private static void DeleteUser()
        {
            DirectoryEntry ad = new DirectoryEntry("WinNT://" + Environment.MachineName + ",computer");
            DirectoryEntries users = ad.Children;
            DirectoryEntry user = users.Find(UserName, "user");

            if (user != null)
            {
                users.Remove(user);
            }
        }

        private static void Impersonate()
        {
            if (LogonUser(UserName, ".", Password, (int)LogonType.LOGON32_LOGON_INTERACTIVE, (int)LogonProvider.LOGON32_PROVIDER_DEFAULT, ref LogonToken))
            {
                Identity = WindowsIdentity.Impersonate(LogonToken);
                return;
            }
        }

        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        private static extern bool LogonUser(string lpszUserName,
            string lpszDomain,
            string lpszPassword,
            int dwLogonType,
            int dwLogonProvider,
            ref IntPtr phToken);

        private static void SetAccess(string path, AccessControlType type)
        {
            FileSecurity fs = File.GetAccessControl(path);
            FileSystemAccessRule far = new FileSystemAccessRule(UserName, FileSystemRights.FullControl, type);
            fs.AddAccessRule(far);
            File.SetAccessControl(path, fs);
        }

        private static void UndoImpersonate()
        {
            if (Identity != null)
            {
                Identity.Undo();
                Identity = null;
            }

            if (LogonToken != IntPtr.Zero)
            {
                CloseHandle(LogonToken);
                LogonToken = IntPtr.Zero;
            }
        }
    }
}
This program basically tests file permissions and folder permissions with File.Exists. What I expected to see based on the documentation was both instances should return false; however, I found that when the user has permissions on the directory but none on the file, it still returns true. This ends up being due to a very subtle keyword in the documentation, "sufficient". If the user has the LIST permission in the directory the file lives in, File.Exists has sufficient permissions to return whether or not the file exists.

Introduction

Posted on: February 15, 2016 1:47:48 AM

I suppose the best way to begin this blog is an introduction about myself. My name is Justin Sommercorn, I was born in Salt Lake City, UT. I am currently 29 years old and have been married happily to my wife Leah for the past 5 years. I've had two wonderful children with a third on the way.

From a very young age, I've always had an interest in math and science. My family bought their first computer when I was approximately 8 years old and from that time on, I spent as much time as I could on it. I enjoyed playing games and learning how to do things. I very quickly became the resident expert on the computer and when things went wrong it was up to me to fix it. I knew from a very early age that I wanted to go into software development; although, I wanted to go into game development. In high school, I had the opportunity to intern at Microsoft Games in Salt Lake City. It was there that I learned that game development was very fast paced and not as rewarding as I thought it would be. I still knew that development was my career choice, so I started learning about Windows application development and web development.

Shortly after high school, I lucked into getting my first software development job. I had no college degree and knew that my best bet was to send my resume everywhere hoping to impress during the interview. I was hired on as a junior software engineer at Paraben and began work on a Java project. At the time, it was me and one other developer on the project, but it didn't remain that way very long. The other developer moved into a management position and I was left as the only developer on the project. I learned a lot about coding on this project and was able to start a few other projects there based on the knowledge that I picked up.

After Paraben, I worked for several other companies finally ending up where I currently am at Domo. I still enjoy learning about coding and why things are done the way they are. I make a big effort to stay up to date with the latest releases of the languages and frameworks that I use. I also have many side projects that I work on in my own time, including some game development. I am very happy with what I am doing and look forward to a long career.