Did you ever need to set an ACE (Access Control Entry) on a directory or a file? I’ve had plenty of applications in the past where I needed to set permissions on a directory or file. Most of these were application configuration applications. Usually this is required for automatically installing Web applications that get redistributed in some way.


I recently was working on a generic Web Configuration utility, that performs setup tasks for Web applications in a semi-generic way so that you can ship a generic install utility that can configure your Web application. One of the important features of this tool is the ability to set permissions on a folder.

 

Assigning permissions to a file or directory is one of those black art topics when it comes to doing it via code. I remember looking for examples in C++ for this a few years back as part of the West Wind Web Connection installation. At the time I gave up on it because it took a ton of really ugly code that I couldn’t figure out .

 

That was then, this is now and we’re still not a whole lot better off - .NET also doesn’t include ACE/L support (although .NET 2.0 is rumored to have a class to allow setting of directory permissions).

 

Regardless, it’s possible to fairly easily integrate this functionality by using the CACLS.EXE command line utility which is included in all versions of WinNT forward through XP and Win2003 Server. CACLS allows you to create individual ACE entries. With CACLS it's pretty easy to create ACE entries easily.

 

The following class encapsulates the basics of creating a single ACE entry.

 

///

/// This class sets an individual ACL value on a file or directory.

///

public class DirectoryPermission

{

      public DirectoryPermission()

      {

      }

 

      public DirectoryPermission(string Path, string Username, string UserRights)

      {

            this.Pathname = Path;

            this.Username = Username;

            this.UserRights = UserRights;

      }

 

      ///

      /// Directory Resource to assign ACL to. This is a physical disk path

      /// like d:\temp\test.

      ///

      public string Pathname = "";

 

      ///

      /// Username or group name to assign to this resource

      ///

      public string Username = "";

     

      ///

      /// Rights to assign for the given user or group.

      /// N - None

      /// R - Read

      /// C - Change

      /// F - Full

      ///

      public string UserRights = "R";

 

      ///

      /// Determines whether the permissions walk down all child

      /// directories.

      ///

      public bool InheritSubDirectories = true;

 

      ///

      /// When set overrides the existing ACLs and only attaches this

      /// ACL effectively deleting all other ACLs. Generally you won't

      /// want to do this and only change/add the current ACL.

      ///

      public bool OverrideExistingRights = false;

 

      ///

      /// Error Message set by SetAcl

      ///

      [XmlIgnore]

      public string ErrorMessage = "";

 

      ///

      /// Sets the actual ACL based on the property settings of this class

      ///

      ///

      public bool SetAce()

      {

            if ( this.Pathname == null || this.Pathname == "")

            {

                  ErrorMessage +=  "Path cannot be empty.";

                  return false;

            }

 

            string CommandLine = '"' + System.IO.Path.GetFullPath(this.Pathname) + '"' +

                                 " /C ";

 

            if (this.InheritSubDirectories)

                  CommandLine += @" /T ";

            if (!this.OverrideExistingRights)

                  CommandLine += @" /E ";

 

            CommandLine += @" /P " + this.Username + ":" + this.UserRights;

 

            Process p = new Process();

            p.StartInfo.FileName = "cacls.exe";

            p.StartInfo.Arguments = CommandLine;

            p.StartInfo.RedirectStandardOutput = true;

            p.StartInfo.UseShellExecute = false;

            p.StartInfo.WindowStyle = ProcessWindowStyle.Minimized ;

            p.StartInfo.CreateNoWindow = true;

 

            p.Start();

 

            string Response = p.StandardOutput.ReadToEnd();

 

            // *** If no output gets generated there's an error

            if (Response == null || Response == "")

            {

                  this.ErrorMessage += "Unable to set permissions on " +

                                       this.Pathname + " for " + this.Username ;

                  return false;

            }

 

            return true;

      }

}

 

 

This class works by simply using the Process class to execute the CACLS utility. It maps the various property settings to command line options and then simply executes the command.

 

You need to be an administrator in order to be able to set permissions on directories and to be able to execute external programs out of the application.

 

The routine redirects StandardOutput to an internal stream that’s checked for a result. If the result is empty the CACLS call failed. Otherwise the call worked and the routine returns .t.

 

The command line looks something like this to create a single ACE entry:

 

CACLS.EXE "d:\MyApp\WebPath" /T /P IUSR_RASNOTEBOOK:R

 

The /T switch specifies that subdirectories will inherit the settings. /P plus the user or group name followed by colon and one of the following first letters: Read, Change, Write, Full.

 

This is sort of a brute force way of performing this task but it works well and the class encapsulates the process in such a way that it’s non-intrusive for the calling application. The Console window that CACLS runs in is minimized so it doesn’t affect the desktop other than a quick flash in the taskbar. 

 

How would you use this? In the Web Configuration utility the code figures out the Anonymous Web User by querying IIS through ADSI, and adding this user to the application directory with Read attributes. It also checks for the IIS version and if it's IIS 5 add ASPNET and with IIS 6 NETWORKSERVICE and adds it with Full rights for the directory (this app needs to read the web.config file as well as write a couple of XML files). These rights are automatically added to a listbox and then shown in the UI. The actual routines then pick up the values in the list box and assign it to Configuration object which contains an ArrayList of DirectoryPermission objects. The code then simply loops through the list and calls the SetAce method to set the rights.

 

The assignment logic looks like this:

 

public bool AddDefaultWebPermissions()

{

      if (this.Virtual.Virtual == null || this.Virtual.Virtual == "")

            return true;

 

      VirtualDirectory vd = new VirtualDirectory();

 

      vd.IISPath = this.Virtual.IISPath;

      vd.Virtual = this.Virtual.Virtual;

 

      if (!vd.GetVirtual() )

      {

            if (! vd.GetVirtual("") )

            {

                  this.SetError(vd.ErrorMessage);

                  return false;

            }

      }

 

      // *** Add the Anonymous User

      string AnonymousUser = vd.AnonymousUserName;

      this.DirectoryPermissions.Add( new DirectoryPermission( this.Virtual.Path,AnonymousUser,"R") );

 

      if (vd.ServerType == WebServerTypes.IIS6)

      {

            this.DirectoryPermissions.Add( new DirectoryPermission( this.Virtual.Path,"NETWORKSERVICE","F") );

      }

      else

      {

            this.DirectoryPermissions.Add( new DirectoryPermission( this.Virtual.Path,"ASPNET","F") );

      }

 

      return true;

}

 

and to run the actual permissions:


 

public bool SetDirectoryPermissions()

{

      bool Error = false;

 

      // *** Create Directory Permissions

      foreach( DirectoryPermission dm in this.DirectoryPermissions )

      {

            if (!dm.SetAce() ) 

            {

                  this.SetError( dm.ErrorMessage );

                  Error = true;

            }

      }

 

      return !Error;

}

 

Setting directory permissions isn't something you do every day, but when you need it it usually is critical to avoid manual configuration.

 

You can apply this same logic to other languages easily enough. For example, here's a Visual FoxPro version that is now used as part of the West Wind Web Connection setup:

 

************************************************************************

* wwUtils :: SetACL

****************************************

***  Function:

***    Assume:

***      Pass: lcPath        -   Path or Filename to assign ACE to

***            lcUser        -   The Username to assign

***            lcAccess      -   [N]one, [R]ead, [C]hange, [F]ull

***            llInherit     -   Pass rights down the directory tree

***            llReplace     -   Replace rights on the resource

***                              (deletes all ACL entries except this one)

***    Return:

************************************************************************

FUNCTION SetACL(lcPath,lcUser,lcAccess,llInherit,llReplace)

LOCAL lcCommand, lcFile

 

*** Strip off any trailing backslashes

IF RIGHT(lcPath,1)="\" AND LEN(lcPath) > 1

   lcPath = SUBSTR(lcPath,1,LEN(lcPath)-1)

ENDIF

  

lcCommand = "RUN Cacls.exe " + ShortPath( lcPath)

 

IF llInherit

   lcCommand = lcCommand + " /T "

ENDIF

IF !llReplace

   lcCommand = lcCommand + " /E "

ENDIF

 

lcCommand = lcCommand + " /P " + lcUser + ":" + lcAccess + " > cacls.txt"

&lcCommand

 

lcFile = ""

IF FILE("Cacls.txt")

   lcFile = FILETOSTR("cacls.txt")

   ERASE Cacls.txt

ENDIF

IF EMPTY(lcFile)

   RETURN .F.

ENDIF

 

RETURN .T.

ENDFUNC

*  wwUtils :: SetACL

 

 

Remember, that even though you can do this under full program control, setting permissions on a directory or file is a super high trust operation and as such may affect the system security significantly. It's good form to always let the user know that you are making this sort of change with a dialog. Also, you should only set permissions if absolutely necessary and always try to minimize the paths that are affected by it.