Setting Folder Security in MSI

I had a surprisingly difficult time trying to figure out how to set folder permissions in my windows service installer. Here’s the story: I have a Windows service that writes log information into the Logs directory. If the service is installed under c:\program files\company\product, then it wants to write to c:\program files\company\product\Logs.

The service works under “NETWORK SERVICE” account, and by default it does not get write access to the Program Files directories. It turned out that I need to create a special installer class to create the Logs folder and change NTFS security on it. Maybe there is a better way to do it, but I am not aware of it. I had to solve the following problems:

  1. creating my installer class
  2. having it called from the main installer
  3. finding installation folder from inside my class
  4. actually setting the security

There was surprisingly little accurate information about this on the Internet. Here’s how I solved those problems:

Creating Installer Class

This is easy: just create a class and derive it from Installer (System.Configuration.Install.Installer). Leave it empty and compile the project. I called the class LogInstaller.

Having your class called from the main installer

I had my service installer class (created by double-clicking on my Windows Service class, right-clicking and selecting “Add Installer”). I have added it as a custom action in my installer project.

Once you have created and compiled the LogInstaller class, it appears in the tool box. Just drag it to the design surface of the service installer. This does almost everything, but you will have to manually edit the designer code: locate “this.Installers.AddRange()” call in the InitializeComponent() method and add the newly created log installer instance to the list of child installers. NB: this is in the *.designer.cs file for the service installer.

Find installation folder

This was the most tricky one, and admittedly I cheated. When your installer class is called, some parameters are passed in this.Context.Paremeters field. This is a string-to-string dictionary. I printed the whole contents of this dictionary, and I found only four parameters: “action”, “installtype”, “assemblypath”, and “logfile”, of which only “assemblypath” was useful.

Since I knew my assembly is installed in the root directory of the product (c:\program files\company\product), I just used Path.GetDirectoryName(this.Context.Parameters[“assemblypath”]) as the application install location. This is not very nice – what if next time I install the assembly in some subfolder? I will have to modify my log folder installer, and this is a hidden dependency.

The code that handles all that goes into LogDirectoryInstaller.Commit(), by the way.

Actually setting the security

Well, there is plenty of information on the Internet for this one. Please find the code of LogDirectoryInstaller below.

using System.Configuration.Install;
using System.IO;
using System.Security.AccessControl;

namespace Consulting.Sungard
{
    public class LogDirectoryInstaller : Installer
    {
        public override void Install(System.Collections.IDictionary stateSaver)
        {
            base.Install(stateSaver);
            string logDir = GetLogPath();
            DirectoryInfo dirInfo = Directory.CreateDirectory(logDir);
            SetSecurity(dirInfo);
        }

        private void SetSecurity(DirectoryInfo dirInfo)
        {
            dirInfo.SetAccessControl(GetLogFolderSecurity(dirInfo));
        }

        private DirectorySecurity GetLogFolderSecurity(DirectoryInfo dirInfo)
        {
            DirectorySecurity security = dirInfo.GetAccessControl();
            FileSystemAccessRule rule =
                new FileSystemAccessRule(
                    "NETWORK SERVICE",
                    FileSystemRights.Modify,
                    InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
                    PropagationFlags.None,
                    AccessControlType.Allow);

            security.AddAccessRule(rule);
            return security;
        }

        public override void Commit(System.Collections.IDictionary savedState)
        {
            base.Commit(savedState);
        }

        private string GetLogPath()
        {
            string assemblyPath = Context.Parameters["assemblypath"];
            string installDir = Path.GetDirectoryName(assemblyPath);
            string logDir = Path.Combine(installDir, "Logs");
            return logDir;
        }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *