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.

RadChart with .NET MVC

I have a webform app that I converted to a webform/mvc hybrid. Everything was fine until I tested the Telerik RadCharts on the existing webform pages. It had worked before the conversion. Now whenever the chart was supposed to load, I was confronted with this error message:

Error loading RadChart image. You may also wish to check the ASP.NET Trace for further details. Display stack trace?

Clicking “OK” simply took me to a 404 page. I did notice the url of this was http://localhost:11239/reports/ChartImage.axd?UseSession=true&ChartID=139df24e-1578-44d3-988b-c66f2d7df98c_chart_ctl00$MainContent$ReportChart1&imageFormat=Png&random=0.725656411482793

Somehow MVC routing disrupted the RadChart handler. To fix this, you need to explicitly define the HttpHanderUrl property of the RadChart.

ReportChart1.HttpHandlerUrl = ResolveUrl("~/ChartImage.axd");

That should fix it!

Add MVC capabilites to ASP.NET webform application

Because they both are built on the same framework, it’s pretty simple to get the two to play nice together. I used this as a guide: http://guy.dotnet-expertise.com/PermaLink,guid,4f5a8ea1-1f90-4b32-a635-1d73d906aeca.aspx. It’s an old article but it worked for me. I’m using Visual Studio 2010, .NET 4.0 and MVC 3 with Razor.

The steps are as follows:

Add the following 3 references to your project:
System.Web.Abstraction
System.Web.Mvc (be aware of what DLL version you’re adding)
System.Web.Routing

Modify the project’s web.config. The easiest way is to create a new MVC project and copy over the value from there. You need to copy over the following sections, but don’t overwrite the existing values:
Compilation
Pages
HttpHandlers (this didn’t apply for me because it wasn’t in the MVC web.config. Might be a .NET 4.0 or MVC 3 thing.)
System.WebServer
Runtime

Next modify Global.asax Application_Start method to perform the same functions as your temp MVC functions.

Then copy the web.config in the View folder of the MVC project and paste it in the View folder of your webform project. Also don’t forget to copy any scaffolding files such as _Layout.cshtml and _ViewStart.cshtml.

Finally, you need to let Visual Studio know that your project now handles MVC pages as well. This will allow you to use the IDE to create Controller, Views, and other MVC goodies. Open the temp MVC project file in notepad and look for the <ProjectTypeGuids> section. Copy that guid and add it to your webform project folder. The guid in my project is {E53F8FEA-EAE0-44A6-8774-FFD645390401}.

That’s it. Now you should be able to create MVC files just was if the webform project was an MVC project itself.

WCF basicHttpBinding – 504 Gateway Timeout

I had a WCF service secured with SSL, using wsHttpBinding, Message security mode, and UserName authentication. Everything worked find because I created the client app in VisualStudio and it automatically generated the appropriate tags in web.config. I was able to connection, authenticate and call methods without issue.

But now I need to adjust the service to allow clients of other technologies to connect to it…more specifically PHP. PHP can’t connect to a service that uses wsHttpBinding. It has to be basicHttpBinding because it conforms to a broader standard.

The first thing to do is change system.serviceModel/services/service binding attribute to “basicHttpBinding”. Then create a <basicHttpBinding> node under system.serviceModel/bindings. Because we can’t use the Message security mode with basicHttpBinding, it needs to be changed to TransportWithMessageCredential. And that’s basically it for the service.

Now just publish it, connect to it from your client and call it a day. Well it didn’t go that smoothly for me. My client app I create for the original wsHttpBinding service was griping with an error: The remote server returned an error: (504) Gateway Timeout. Update the service reference? Nope that didn’t help. Remove and create the service reference? Nope, same error. So then I tried playing with the service configuration to no avail.

Finally I decided to read the stack trace and saw “There was no endpoint listening at https://myremoteservername/service.svc that could accept the message. This is often caused by an incorrect address or SOAP action.” Weird, how did it know the name of my server? I went back to my client’s web.config and looked at the endpoint VS created with I added the service reference. Indeed the address contained the server name! I changed it to the publicly accessible domain name and it works.

For some reason, when VS references a service with basicHttpBinding, it uses the server name. But I have no idea how it got the name in the first place. When adding the reference, I entered the public (correct) .svc url.

I’ll have to check with our network guy about it but I’m happy it’s finally working.

iTextSharp – Removing Fields from PDF templates

Sometimes the iTextSharp RemoveField() method won’t work. This is likely because the PDF template was created in Adobe LiveCycle. LiveCycle saves the file in XFA format. iTextSharp functionality against XfA files are limited to get and set. RemoveField(), at least for now, only works on AcroForm templates created in Adobe Acrobat.

BTW, here’s the code to read from a template, edit a field and save to a new file…or output to the browser.

string templateFile = Server.MapPath(“pdf/FSSTemplate.pdf”);
string newFile = Server.MapPath(“pdf/FSS.pdf”);

PdfReader reader = new PdfReader(templateFile);
PdfStamper stamper = new PdfStamper(reader, new FileStream(newFile, FileMode.Create));

//Use the two lines below to write the pdf to the browser
//MemoryStream ms = new System.IO.MemoryStream();
//PdfStamper stamper = new PdfStamper(reader, ms);

AcroFields fields = stamper.AcroFields;
fields.SetField(“field1”, “Hello World!”);

//Flatten and close
stamper.FormFlattening = true;
stamper.Close();

//If you’re writing to a file, you’re done here. If you want to write to the browser, use the code below.

Byte[] byteArray = ms.ToArray();
ms.Flush();
ms.Close();
Response.BufferOutput = true;
Response.Clear();
Response.ClearHeaders();
string timestamp = DateTime.Now.ToString(“MMddyyyy_HHmmss”);
Response.AddHeader(“Content-Disposition”, “attachment; filename=PDF_” + timestamp + “.pdf”);
Response.ContentType = “application/pdf”;
Response.BinaryWrite(byteArray);
Response.End();

Dividing by zero

Don’t forget people, dividing by zero does NOT always throw an exception. Sometimes the result is Infinity. It seems to only throw a DivideByZeroException when the zero is an integer or decimal. Here are my test results for C#.

z = 10 / y;

int y = 0; //throws exception
decimal y = 0; //throws exception
decimal y = 0m; //throws exception
double y = 0; //Infinity
double y = 0d; //Infinity
float y = 0; //Infinity
float y = 0F; //Infinity

REFERENCES
http://msdn.microsoft.com/en-us/library/system.dividebyzeroexception.aspx
http://msdn.microsoft.com/en-us/library/aa691373.aspx
http://www.jaggersoft.com/csharp_standard/14.7.2.htm