Creating a shared directory and setting security with C#

I’ve recently had to write an application to, among other things, create a shared directory in the file system and assign security privileges to specific users. In addition, I also needed to modify permissions of non-shared directories and ensure they propagated to its children. This set me on the path into calling WMI functions from .NET. These included Win32_Share, Win32_Trustee, Win32_Ace, Win32_SecurityDescriptor, and Win32_LogicalFileSecuritySetting. I’m not even going to pretend I completely understand what all the code is doing. But I figured I’d at least document what I got working from my hours of scouring the web.

abandonallhope
Let’s first address how to share a directory. When creating a share, you need to ensure the directory is not already shared using the same name. Windows will allow you to share the same physical directory multiple times with different share names, but if you try to create a share with a name that already exist, you’ll get an error. So first we need a way to check if that share name is already used.

 

public static bool IsDirectoryShared(string fullDirectoryPath, string shareName)
{
    bool isShared = false;
    
    var sharedFolders = new List();
    ManagementObjectSearcher searcher = new ManagementObjectSearcher("select * from win32_share");
    
    foreach (ManagementObject share in searcher.Get())
    {
        string type = share["Type"].ToString();
        
        if (type == "0") // 0 = DiskDrive, 1 = Print Queue, 2 = Device, 3 = IPH
        {
            if (share["Path"].ToString().Equals(fullDirectoryPath, StringComparison.OrdinalIgnoreCase))
            {
                if (!string.IsNullOrWhiteSpace(shareName) && share["Name"].ToString().Equals(shareName, StringComparison.OrdinalIgnoreCase))
                {
                    isShared = true;
                }
                
                isShared = true; 
            }
        }
    }
    
    return isShared;
}

 

IsDirectoryShared accepts the physical path and the share name to look up. It uses the ManagementObjectSearcher to query all shares returned by Win32_Share. There might be a way to enhance the query to return a more specific dataset but I haven’t tried yet. Instead, I loop through the results and check if the share is from the disk drive (Win32_Share gets other types of shares also). Then I check to see if the path and share name matches what I’m looking for. If they match, I know that physical directory is already shared with that name.

Let’s pretend IsDirectoryShared returns false. Good, now we can move forward with creating the share. To complicate things, it’s not enough to create a share. I also only want certain users to be able to access it. I should also mention that in my case, I’m only dealing with local account. No domains here.

 

public static void SetDirectoryShare(string fullDirectoryPath, string accountToAccess, string description, string shareName)
{
    // Share folder
    ManagementClass managementClass = new ManagementClass("Win32_Share");
    ManagementBaseObject inParams = managementClass.GetMethodParameters("Create");
    ManagementBaseObject outParams;
    inParams["Description"] = description;
    inParams["Name"] = shareName;
    inParams["Path"] = fullDirectoryPath;
    inParams["Type"] = 0x0;
    
    // Grant access
    NTAccount account = new NTAccount(accountToAccess);
    SecurityIdentifier sid = (SecurityIdentifier)account.Translate(typeof(SecurityIdentifier));
    byte[] sidArray = new byte[sid.BinaryLength];
    sid.GetBinaryForm(sidArray, 0);
    ManagementObject Trustee = new ManagementClass(new ManagementPath("Win32_Trustee"), null);
    Trustee["Name"] = accountToAccess;
    Trustee["SID"] = sidArray;
    ManagementObject AdminACE = new ManagementClass(new ManagementPath("Win32_Ace"), null);
    // 1179817 - read    //1245631 - modify     2032127 - full
    AdminACE["AccessMask"] = 1179817;
    AdminACE["AceFlags"] = 3;
    AdminACE["AceType"] = 0;
    AdminACE["Trustee"] = Trustee;
    ManagementObject SecurityDescriptor = new ManagementClass(new ManagementPath("Win32_SecurityDescriptor"), null);
    SecurityDescriptor["ControlFlags"] = 4;
    SecurityDescriptor["DACL"] = new object[] { AdminACE };
    
    inParams["Access"] = SecurityDescriptor;
    
    outParams = managementClass.InvokeMethod("Create", inParams, null);
    
    var result = (uint)(outParams.Properties["ReturnValue"].Value);
    if (result != 0)
    {
        // Error occurred creating share
        StringBuilder msg = new StringBuilder(String.Format("An error occurred sharing directory {0} with share name {1} with user {2}. ", fullDirectoryPath, shareName, accountToAccess));
        switch (result)
        {
            case 2:
            msg.Append("Access denied.");
            break;
            case 22:
            msg.Append("Duplicate share.");
            break;
            default:
            break;
        }
    }
}

 

I access the Win32_Share class again but this time use it through the ManagementClass. Then I prepare the use its Create method and pass it some parameters. Next I give access to the one user that’s passed into my method. This is where it took me a while to get things working and where things start to get gray for me. The world of Trustee’s, Ace, and Dacl and how they relate will require more research on my part, but at least I know this code works. The bitmask used for the AccessMask is important because that will determine the level of access. You can read up on it here: https://msdn.microsoft.com/en-us/library/aa394063(v=vs.85).aspx. I had a hard time figuring out exactly what bitmask to use so I used this tool (https://technet.microsoft.com/en-us/library/2006.05.scriptingguy.aspx) to analyze an exiting shared directory and got bitmask value from there. You should also pay attention to the AceFlags property as that is what set’s the inheritance level.

Cool! Now that we got all of that working, time to go home early right? Nope. The client now wants to be able to grant permission to an unshared folder. *rolls up sleeves* Let’s get started.

When we don’t have to create a share, granting privileges to a user turns out to be easier than imagined.

 

public static void SetDirectorySecurityModifyAccess(string fullPath, string accountToAccess)
{
    DirectoryInfo dInfo = new DirectoryInfo(fullPath);
    DirectorySecurity dSecurity = dInfo.GetAccessControl();
    dSecurity.AddAccessRule(new FileSystemAccessRule(accountToAccess, FileSystemRights.Modify, InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, PropagationFlags.None, AccessControlType.Allow));
    dInfo.SetAccessControl(dSecurity);
}

 

Hmm, only 4 lines of code. Where’s the fun in that? Let’s see how to do it the manly way.

 

public static void SetDirectorySecurityModifyAccessManlyWay(string fullPath, string accountToAccess)
{
    ManagementObject lfs = new ManagementObject(@"Win32_LogicalFileSecuritySetting.Path='" + fullPath + "'");
    bool EnablePrivileges = lfs.Scope.Options.EnablePrivileges;
    lfs.Scope.Options.EnablePrivileges = true;
    
    // Get the security descriptor for this object
    ManagementBaseObject outP = lfs.InvokeMethod("GetSecurityDescriptor", null, null);

    if (((uint)(outP.Properties["ReturnValue"].Value)) == 0)
    {
        ManagementBaseObject descriptor = ((ManagementBaseObject)(outP.Properties["Descriptor"].Value));
        ManagementBaseObject[] daclObject = ((ManagementBaseObject[])(descriptor.Properties["Dacl"].Value));
        
        Array newDacl = Array.CreateInstance(typeof(ManagementBaseObject), 1);
        ManagementObject trustee = new ManagementClass(new ManagementPath("Win32_Trustee"), null);
        ManagementObject ace = new ManagementClass(new ManagementPath("Win32_Ace"), null);
        trustee.Properties["Name"].Value = accountToAccess;
        ace.Properties["AccessMask"].Value = 1245631;
        ace.Properties["AceFlags"].Value = 3;
        ace.Properties["AceType"].Value = 0;
        ace.Properties["Trustee"].Value = trustee;
        newDacl.SetValue(ace, 0);
        
        descriptor.Properties["Dacl"].Value = newDacl;
        ManagementBaseObject inParams = lfs.GetMethodParameters("SetSecurityDescriptor");
        inParams["Descriptor"] = descriptor;
        
        ManagementBaseObject outParams = lfs.InvokeMethod("SetSecurityDescriptor", inParams, null);
        var ret = ((uint)(outParams.Properties["ReturnValue"].Value));
        if (ret != 0)
        {
            throw new Exception("Failed setting security");
        }
    }
    
    lfs.Scope.Options.EnablePrivileges = EnablePrivileges;
}

 

Nice, back to the Win32 stuff. I think if you do it this way, an advantage is being able to set the permission at a more granular level using the AccessMask property and your desired bitmask.

Now it’s time to go home.