Category Archives: C# Snippets

Manage IIS Security from a C# Assembly

I have discovered over the past couple of days that it is possible to control many aspects of IIS behaviour from a .NET assembly (in this case I will be using C# as the underlying language), by using the System.DirectoryServices namespace.

Using very similar methods to those you would use to get the properties of an Active Directory object, you can get or set properties on IISWebVirtualDirs, IISWebFiles, or any of the other available object types within IIS.

The basic way to select a DirectoryEntry object from IIS is as follows.

DirectoryEntry myObject = new DirectoryEntry("IIS://localhost/W3SVC/1/Root/myAppFolder/myFile.aspx");

The above code contains one of the first problems you will have to surmount when setting the properties on a file. What is the metabase path to the object you want to work on? Fortunately, there are two solutions to this. A manual one, and a more programmatic one.

The manual solution is to look in the %systemroot%\System32\Inetsrv\metabase.xml for the file you are interested in managing. (nb. Be careful with this file. Corrupting it’s contents can break IIS completely.) An example entry for a Virtual Directory will look like this :

<iiswebvirtualdir Location ="/LM/W3SVC/1/ROOT/menu2" AccessFlags="AccessRead | AccessScript" AppFriendlyName="menu2" AppIsolated="2" AppRoot="/LM/W3SVC/1/Root/menu2" AuthFlags="AuthNTLM" DirBrowseFlags="DirBrowseShowDate | DirBrowseShowTime | DirBrowseShowSize | _ DirBrowseShowExtension | DirBrowseShowLongDate | EnableDefaultDoc" Path="D:\TestArea\menu2" >

The “Location” property is the one you will need. Notice, though, that this entry begins “/LM/W3SVC” rather than “IIS://localhost/W3SVC”. You will need to take that into account when you search for the object.

To do this more programmatically, you could use the following snippet to traverse up to 4 levels down your IIS hierarchy.

private void button1_Click(object sender, System.EventArgs e) { treeView1.Nodes.Clear(); treeView1.Nodes.Add("IIS://localhost/W3SVC"); DirectoryEntry root = new DirectoryEntry(@"IIS://localhost/W3SVC"); DirectoryEntries level1 = root.Children; foreach(DirectoryEntry myDI in level1) { TreeNode l1parent = treeView1.Nodes[0].Nodes.Add(myDI.Name); DirectoryEntries level2 = myDI.Children; foreach(DirectoryEntry myDI2 in level2) { TreeNode l2Parent = l1parent.Nodes.Add(myDI2.Name); try { DirectoryEntries level3 = myDI2.Children; foreach(DirectoryEntry myDI3 in level3) { TreeNode l3Parent = l2Parent.Nodes.Add(myDI3.Name); try { DirectoryEntries level4 = myDI3.Children; foreach(DirectoryEntry myDI4 in level4) { l3Parent.Nodes.Add(myDI4.Name); } } catch { Console.WriteLine(l3Parent.Name + " has no child nodes"); } } } catch { Console.WriteLine(l2Parent.Name + " has no child nodes"); } } } }

It would then be fairly simple to find any uniquely named objects within the st
ructure and use that as your base reference path.

If you are trying to modify an object that does not yet have an entry in the metabase (for example, a file with no properties that differ from the folder it resides in) you will have to create a metabase object for that file. The following block will give you an idea how.

public static bool EnsureObjectExists(string metabasePath, string itemType) { try { DirectoryEntry path = new DirectoryEntry(metabasePath); string myName = path.Name; Console.WriteLine("Got it"); return true; } catch(Exception e2) { string newobjectParent = metabasePath.Substring(0,metabasePath.LastIndexOf("/")); string newitemName = metabasePath.Substring(metabasePath.LastIndexOf("/") + 1); bool createdOK = false; switch(itemType) { case "IISWebFile": createdOK = CreateWebFileObject(newobjectParent,newitemName); break; case "IISVirtualFolder": Console.WriteLine("Can't create that. Not written yet."); break; default: Console.WriteLine("Can't create that. Dunno what it is"); break; } if(createdOK) { Console.WriteLine("created the new object"); } else { Console.WriteLine("BARF!!!!!!!!!!"); createdOK; } }

(Of course, I didn’t write the methods to create other types of object. At the time of writing I don’t actually need them, but I will do that at some point.)

So now you know that your object exists (either that, or the method returned false and creation failed.) The next step I will show you is how to restric access to a location to a specific IP address (or multiple addresses).

In order to allow only specific addresses access, you must make sure that the “GrantByDefault” property of the object is set to false.

public static bool SetIPSecurityProperty(string metabasePath, string member, string item) { // metabasePath is of the form "IIS:///" // for example "IIS://localhost/SMTPSVC/1" // member is of the form "IPGrant|IPDeny|DomainGrant|DomainDeny" // item is of the form "", for example, 157.56.236.15 or domain.microsoft.com Console.WriteLine("\nEnumerating the IPSecurity property at {0}:", metabasePath); try { if (("IPGrant" != member) && ("IPDeny" != member) && ("DomainGrant" != member) && ("DomainDeny" != member)) { Console.WriteLine(" Failed in SetIPSecurityProperty; " + "second param must be one of IPGrant|IPDeny|DomainGrant|DomainDeny"); } else { DirectoryEntry path = new DirectoryEntry(metabasePath); path.RefreshCache(); object ipsecObj = path.Invoke("Get", new string[] { "IPSecurity" }); Type t = ipsecObj.GetType(); Console.Write(t.ToString()); Array data = (Array)t.InvokeMember(member, BindingFlags.GetProperty, null, ipsecObj, null); Console.WriteLine(" Old {0} =", member); bool exists = false; Debug.WriteLine("Starting to check the object"); foreach (object dataItem in data) { Console.WriteLine(" {0}", dataItem.ToString()); if (dataItem.ToString().StartsWith(item)) { exists = true; } } if (exists) { Console.WriteLine(" {0} already exists in {1}", item, member); } else { object[] newData = new object[data.Length + 1]; data.CopyTo(newData, 0); newData.SetValue(item, data.Length); t.InvokeMember(member, BindingFlags.SetProperty, null, ipsecObj, new object[] { newData }); path.Invoke("Put", new object[] { "IPSecurity", ipsecObj }); path.CommitChanges(); path.RefreshCache(); ipsecObj = path.Invoke("Get", new string[] { "IPSecurity" }); data = (Array)t.InvokeMember(member, BindingFlags.GetProperty, null, ipsecObj, null); Console.WriteLine(" New {0} =", member); foreach (object dataItem in data) Console.WriteLine(" {0}", dataItem.ToString()); Console.WriteLine(" Done."); return true; } } return false; } catch (Exception ex) { if ("HRESULT 0x80005006" == ex.Message) Console.WriteLine("Property IPSecurity does not exist at {0}", metabasePath); else Console.WriteLine("Failed in SetIPSecurityProperty with the following exception: \n"); Console.WriteLine(ex.Message + Environment.NewLine + ex.ToString()); return false; } }

To call the method above, you should use the following syntax :

SetIPSecurityProperty(metabasePath,"IPGrant","192.168.0.1"); SetIPSecurityProperty(metabasePath,"DomainGrant","test.example.com");

The first call will allow access from a single IP address. Simple. Nothing more to say there. The second line is a bit more interesting. This will allow anyone connecting from the test.example.com to actually get to the file. The reason that this is more interesting is the way IIS handles these rules. Because you have specified a domain, IIS has to do a reverse DNS lookup for each request. In fact, if you set this property through IIS Manager, a popup appears with the text “Warning: Restricting access by domain name requires a DNS reverse lookup on each connecion. This is a very expensive operation and will dramatically affect server performance”. I should try to stick to IP addres rules if I were you.

So that is it as far as setting IP based security properties in IIS via a C# application. There are lots of other properties that can be set on each object, and I will describe those here when I have worked with them some more.

Adding a “Please wait” message to long form submissions

I have a couple of forms on the company SharePoint Portal site that take a few seconds to submit. In the back end, they link into our financial packages, and rely on the external application to do some of the processing. As a result, it can take 10 seconds to return the results page. My problem i
s “How do I display a “Processing, please wait message” to the page?

Simple.

1) Set up your message, and style it with css.

<style> #processing{ position:absolute; width:150px; height:100px; top:50%; margin-top:-50px; left:50%; margin-left:-75px; padding:10px; font-weight:700; background-color:blue; color:white; display:none; ) </style>

The important thing in the style definition is “display:none;”. This stops the div from displaying initially.

<div id="processing"> please wait...<br /> <blink>Processing</blink> </div>

2) Add a bit of simple Javascript to the page

<script language="javascript"> function showstatus(){ processing.style.display = 'block'; } </script>

The javascript depends upon your div having an id of “processing”. If you want a different id for your div, please change the line “processing.style.display = ‘block';” accordingly.

nb. In SharePoint WebParts it is a little trickier to do. Add the div in a standard label, and the javascript can be output in the RenderWebPart method using output.Write(blah); It will probably work in the same label as the div, but I used the method above. easy to do, using the Page.RegisterClientScriptBlock() method.

3) Add an “onClick” handler to your button. The easiest way to do it is in the codeBehind.

buttonName.Attributes.Add("onClick","Javascript:showstatus()");

You can add that in your aspx file, if you are using one, but SharePoint WebParts don’t. :(

4) Build and deploy.

Easy when you know how.

Hiding Columns in a dynamic DataGrid

resultDataGrid is populated from resultDataSet, which is populated directly from SQL.

private void formatDataGridColumns() { for(int i=0;i<resultdataset .Tables[0].Columns.Count;i++) { BoundColumn objbc = new BoundColumn(); objbc.DataField = _ resultDataSet.Tables[0].Columns[i].ColumnName; objbc.HeaderText = _ resultDataSet.Tables[0].Columns[i].ColumnName; resultDataGrid.Columns.Add(objbc); objbc.Visible = false; if (objbc.HeaderText == _ "PreferredName" || objbc.HeaderText == "department") { objbc.Visible=true; } } }

Get SharePoints Database names and connection strings.

C Sharp source code for grabbing database details in sharepoint

Using Sharepoint’s internal mechanisms, it is possible to retrieve the database name and connection string for the content databases that power a site. The code in the attached file uses the following methodology.

1) create a new topology object.
2) loop through objTopology.Databases
3) test whether the returned DB is a profile, Site or Server database
4) allow access to string[] objects that contain the database name and connection string for each type of database.

Simple*.

* It is a little bit hacky, though. I can’t find any methods that tell me database-X functions as the profiles database, or that database-Y is the Sites database. To do this, I have had to resort to running queries against the database in question to determine whether a distinctive table exists. Not ideal, but until I find a better way, it will have to do.

**UPDATE**

The original version will fail if you try to get database details on a host with multiple portals. To get around this, you need to instantiate the object by passing in the site id.

for example:

// find the base url for this portal site.
string PortalUrl = Page.Request.Url.ToString().Replace(Page.Request.RawUrl.ToString(),”/”);
// Create a SPSite object
SPSite mySite = new SPSite(PortalUrl);
// Create a sharepointInfo object to get the databases back from
InTechnology.InTranet.SharepointInfo.Information myInfo = new InTechnology.InTranet.SharepointInfo.Information(mySite.ID.ToString());
profileConnectionString = myInfo.profilesDatabase()[1];

Again, I’m sure there is an easier way to get the site ID, but I don’t know it.

Conditional methods

To create methods that will only be called in a debug build of your application, you can use the following syntax :

[Conditional( “DEBUG” )]
private void SetDebugVariables()
{
connect
ionString = “datasource=developmentDB;SSPI=true;initial catalog=development”;
Trace.WriteLine(“Called in debug build”);
}

One use is to override release build settings to allow you to reference a local data store instead of the production data.

private function setVariables(){ connectionString = "datasource=productionDB;SSPI=true;initial catalog=production"; SetDebugVariables(); } [Conditional( "DEBUG" )] private void SetDebugVariables() { connectionString = "datasource=developmentDB;SSPI=true;initial catalog=development"; Trace.WriteLine("Called in debug build"); }

In a debug build, the method SetDebugVariables() is called, and will therefore override the default (reslease) settings. In a release build, the call to SetDebugVariables() is never made. This is because the compiler recognises the Conditional nature of the method, and simply ignores calls to it in a release build.

Using XPath to narrow results from an XML file

// These two lines read config setting from the app.config file. filename = ConfigurationSettings.AppSettings["inputfile"]; filter = Configu rationSettings.AppSettings["filter1"]; XmlDataDocument myDocument = new XmlDataDocument(); myDocument.Load(filename); XmlNodeList myList = myDocument.SelectNodes("//item[@class='" + filter + "']/*"); for (int i=0; i < myList.Count; i++) { label1.Text += "/r/n" + myList[i].InnerText; }

The associated xml file looks like this…

<?xml version="1.0" encoding="utf-8" ?> <itemlist> <item class="vegetable"> <name>Carrot</name> <colour>Orange</colour> </item> <item class="animal"> <name>Elephant</name> <colour>Grey</colour> </item> <item class="mineral"> <name>Quartz</name> <colour>white</colour> </item> </itemlist>

**edit**

notes:

// will match anywhere in the document.
/ will match the next element
./ will match the previous element.

Create a DataSet from a Stored Procedure

SqlConnection myConn = new SqlConnection(ConnectionString);
SqlDataAdapter da = new SqlDataAdapter();
ds.SelectCommand = new SqlCommand(“GetByOffice”,myConn);
da.SelectCommand.CommandType=CommandType.StoredProcedure;
SqlParameter myParam = new SqlParameter(“@Office”,SqlDbType.VarChar,50);
myParam.Direction=ParameterDirection.Input;
myParam.Value = textBox1.Text;
da.SelectCommand.Parameters.Add(myParam);
DataSet ds = new DataSet();
da.Fill(ds,”results”);
dataGrid1.DataSource = ds.Tables[“Results”];
ds.WriteXml(“result.xml”,XmlWriteMode.WriteSchema);