Wednesday, January 17, 2007

Writing your own XSD.exe

If you spend any time working with Web Services or even just XML, you'll inevitably come into contact with XSD.exe and WSDL.exe, they both generate .net code from XSD type definitions. With XSD.exe, you simply give it the path to an xsd document and it will spit out a .cs file. That file defines types that will serialize to an XML document instance that validates against the xsd. De-serializing your XML to a strongly typed object model is almost always better than fiddling with the XML DOM, but what if you don't like the code that XSD.exe generates? Well, you can easily spin your own XSD.exe since it simply uses the public framework types System.Xml.Serialization.XmlSchemaImporter, System.Xml.Serialization.XmlCodeExporter and CodeDom. For some reason the MSDN documentation on these classes says, "This class supports the .NET Framework infrastructure and is not intended to be used directly from your code.", but don't let that put you off, they're public types and work fine. At a high level the process goes like this, you can follow it with the code sample below:
  1. Load your xsd file into an XmlSchema.
  2. Create an XmlSchemaImporter instance that references your schema. This class is used to generate mappings from XSD types to .net types.
  3. Create a CodeDom CodeNamespace instance where you'll build the syntactic structure of your .net types.
  4. Create an XmlCodeExporter instance with a reference to the CodeNamespace that you use to export your type. This is the class that actually creates the syntactic structure of the .net types in the CodeNamespace.
  5. Create an XmlTypeMapping instance for each type that you wish to export from the XSD.
  6. Call the ExportTypeMapping method on XmlCodeExporter for each XmlTypeMapping object, this creates the types syntax in the CodeNamespace object.
  7. Use a CSharpCodeProvider to output C# source code for the types that were created in CodeNamespace object.
Once the CodeNamespace has been fully populated (after step 6 above) there's an opportunity to make any changes that we wish to the code we output. Note that at this stage, the CodeDom CodeNamespace object represents an IL syntactic structure rather than code in a particular language. We could just as easily generate VB.NET at this point. We can use the CodeDom methods to alter that structure before outputting source code. In the example below I run the RemoveAttributes function to remove some attributes from the type definition.
using System;
using System.IO;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using System.Xml.Schema;
using System.CodeDom;
using System.CodeDom.Compiler;

using Microsoft.CSharp;

using NUnit.Framework;

namespace XmlSchemaImporterTest
{
  [TestFixture]
  public class XsdToClassTests
  {
      // Test for XmlSchemaImporter
      [Test]
      public void XsdToClassTest()
      {
          // identify the path to the xsd
          string xsdFileName = "Account.xsd";
          string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
          string xsdPath = Path.Combine(path, xsdFileName);

          // load the xsd
          XmlSchema xsd;
          using(FileStream stream = new FileStream(xsdPath, FileMode.Open, FileAccess.Read))
          {
              xsd = XmlSchema.Read(stream, null);
          }
          Console.WriteLine("xsd.IsCompiled {0}", xsd.IsCompiled);

          XmlSchemas xsds = new XmlSchemas();
          xsds.Add(xsd);
          xsds.Compile(null, true);
          XmlSchemaImporter schemaImporter = new XmlSchemaImporter(xsds);

          // create the codedom
          CodeNamespace codeNamespace = new CodeNamespace("Generated");
          XmlCodeExporter codeExporter = new XmlCodeExporter(codeNamespace);

          List maps = new List();
          foreach(XmlSchemaType schemaType in xsd.SchemaTypes.Values)
          {
              maps.Add(schemaImporter.ImportSchemaType(schemaType.QualifiedName));
          }
          foreach(XmlSchemaElement schemaElement in xsd.Elements.Values)
          {
              maps.Add(schemaImporter.ImportTypeMapping(schemaElement.QualifiedName));
          }
          foreach(XmlTypeMapping map in maps)
          {
              codeExporter.ExportTypeMapping(map);
          }

          RemoveAttributes(codeNamespace);

          // Check for invalid characters in identifiers
          CodeGenerator.ValidateIdentifiers(codeNamespace);

          // output the C# code
          CSharpCodeProvider codeProvider = new CSharpCodeProvider();

          using(StringWriter writer = new StringWriter())
          {
              codeProvider.GenerateCodeFromNamespace(codeNamespace, writer, new CodeGeneratorOptions());
              Console.WriteLine(writer.GetStringBuilder().ToString());
          }

          Console.ReadLine();
      }

      // Remove all the attributes from each type in the CodeNamespace, except
      // System.Xml.Serialization.XmlTypeAttribute
      private void RemoveAttributes(CodeNamespace codeNamespace)
      {
          foreach(CodeTypeDeclaration codeType in codeNamespace.Types)
          {
              CodeAttributeDeclaration xmlTypeAttribute = null;
              foreach(CodeAttributeDeclaration codeAttribute in codeType.CustomAttributes)
              {
                  Console.WriteLine(codeAttribute.Name);
                  if(codeAttribute.Name == "System.Xml.Serialization.XmlTypeAttribute")
                  {
                      xmlTypeAttribute = codeAttribute;
                  }
              }
              codeType.CustomAttributes.Clear();
              if(xmlTypeAttribute != null)
              {
                  codeType.CustomAttributes.Add(xmlTypeAttribute);
              }
          }
      }
  }
}

Orthogonal Code

As Steve McConnel says in Code Complete, Managing complexity is the fundamental imperative of any software developer, but how do you do that? Well Code Complete spends several hundred pages outlining some techniques and you should also read Bob Martin's Agile Software Development and Martin Fowler's Refactoring. But if you want something that you can digest in your lunch hour, I recently discovered Jeremy Miller's nice series of posts on writing maintainable code, but the one which really struck a chord with me was this one, orthogonal code. Learning how to write code without digging myself into a pit of tangled complexity has been a constant thread in my development as a programmer and he succinctly captures some of the main techniques that I've learnt over the last ten years to avoid doing just that...

Any here's a great quote:

Don't write this post off as just academic hot air, this is about consistently leaving the office on time and protecting your company's code assets because you'll be much more likely to smoothly sustain a forward progress with your existing code.

Thursday, January 04, 2007

Easy impersonation

I've been quite pleasantly surprised recently how easy it is to do impersonation in .net. The trick is a win32 api function that's not covered by the BCL, LogonUser

[System.Runtime.InteropServices.DllImport("advapi32.dll")]
public static extern int LogonUser(
    String lpszUsername, 
    String lpszDomain, 
    String lpszPassword, 
    int dwLogonType, 
    int dwLogonProvider, 
    out  IntPtr phToken);

LogonUser does what it says and logs the given user onto the computer the code is running on. It takes a user name, domain name and a clear text password as well as two constants, a logon type which allows you to specify an interactive user, network user etc and a logon provider (I just used default). It returns a pointer to a token which represents the user and which you can use to create a new WindowsIdentity instance. Once you've got a WindowsIdentity, you can just call the Impersonate method to switch your code execution context to the new identity. Here's a little NUnit test to demonstrate:

[NUnit.Framework.Test]
public void ImpersonationSpike()
{
    string username = "imptest";
    string domain = "mikesMachine"; // this is the machine name
    string password = "imptest";
    IntPtr userToken;

    int hresult = LogonUser(
        username, 
        domain, 
        password, 
        (uint)LogonSessionType.Network, 
        (uint)LogonProvider.Default, 
        out userToken);

    if(hresult == 0)
    {
        int error = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
        Assert.Fail("Error occured: {0}", error);
    }
    WindowsIdentity identity = new WindowsIdentity(userToken);
    Console.WriteLine(identity.Name);

    Console.WriteLine("I am: '{0}'", WindowsIdentity.GetCurrent().Name);
    System.IO.File.WriteAllText(@"c:\Program Files\hello.txt", "Hello");

    WindowsImpersonationContext context = null;
    try
    {
        context = identity.Impersonate();
        Console.WriteLine("Impersonating: '{0}'", WindowsIdentity.GetCurrent().Name);
    }
    finally
    {
        if(context != null)
        {
            context.Undo();
        }
        if(userToken != IntPtr.Zero)
        {
            CloseHandle(userToken);
        }
    }
    Console.WriteLine("I am: '{0}'", WindowsIdentity.GetCurrent().Name);
}

[System.Runtime.InteropServices.DllImport("advapi32.dll")]
public static extern int LogonUser(
    String lpszUsername, 
    String lpszDomain, 
    String lpszPassword, 
    uint dwLogonType, 
    uint dwLogonProvider, 
    out  IntPtr phToken);

[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)]
static extern bool CloseHandle(IntPtr handle);

enum LogonSessionType : uint
{
    Interactive = 2,
    Network,
    Batch,
    Service,
    NetworkCleartext = 8,
    NewCredentials
}

enum LogonProvider : uint
{
    Default = 0, // default for platform (use this!)
    WinNT35,     // sends smoke signals to authority
    WinNT40,     // uses NTLM
    WinNT50      // negotiates Kerb or NTLM
}

It should output the following...

mikesMachine\imptest
I am: 'mikesMachine\mike'
Impersonating: 'mikesMachine\imptest'
I am: 'mikesMachine\mike'

1 passed, 0 failed, 0 skipped, took 1.27 seconds.

A few things to note, you have to have pInvoke permission, which might be problem in some web hosting environments. Also, you have to have a cleartext password in order to log on the user you're impersonating, so your user has to supply it, or you have to store it somewhere which is an obvious security risk.

Of course, if you want to impersonate a particular account in ASP.NET you can just use the <identity impersonate="true" username="theUserName" password="ThePassword" />. On Server 2003, you can even more simply just set your application to run in a custom application pool and set the identity in the application pool settings. All this stuff is explained in this msdn article. But this technique here is great if you just want to grab an identity for a single method call or if you need to do impersonation in something other than an ASP.NET application.

Wednesday, January 03, 2007

SQL Authorization Manager

A while back I blogged about AzMan, a tool for managing operation level permissions that's supplied with Windows Server 2003. Recently I've been introduced to (thanks Chris!) a neat extension of the AzMan idea, SQL Authorization Manager (SqlAzMan) that's written by an Italian developer, Andrea Ferendeles. It takes the basic idea of operation level permissions management, but bases it on a SQL Server database rather than Active Directory, which probably makes more sense for most application developers. You can, however, also reference Active Directory users/groups if you want. It's also written entirely in .net 2.0 so you don't have to tackle all that irritating COM stuff like you do with AzMan and like AzMan it's user interface is an mmc snapin. Another neat addition is the time limiting of permissions, so you can allow someone to do an operation only for within a specific period. It comes with a built in RoleProvider, so you can plug it straight into the existing ASP.NET security framework, but to properly leverage the full power of operation level permissions, you can write directly to it's .net API. Here's a little test I wrote to check it out:

using System;
using System.Security.Principal;
using NUnit.Framework;

using NetSqlAzMan;
using NetSqlAzMan.Interfaces;

namespace Ace.Web.Security.SqlAzMan.Test
{
    /// 
    /// This test is a 'spike' to try out the functionality provided by NetSqlAzMan 
    /// see http://sourceforge.net/projects/netsqlazman/
    /// 
    /// To work it requires a NetSqlAzMan database to have been set up with the following properties:
    /// 
    /// Store name:         Test
    /// Application name:   TestApplication
    /// Operation name:     DoSomething
    /// 
    /// The user running the test should be given permission to execute operation 'DoSomething', see
    /// the NetSqlAzMan documentation for details
    /// 
    /// given authorisation to the user executing the test. You will also have to have installed NetSqlAzMan
    /// on the machine being used for testing.
    /// 
    [TestFixture]
    public class SqlAzManSpike
    {
        string _connectionString = "Data Source=(local);Initial Catalog = NetSqlAzManStorage;Integrated Security = SSPI;";
        IAzManStorage _storage;

        [SetUp]
        public void SetUp()
        {
            _storage = new SqlAzManStorage(_connectionString);
        }

        [NUnit.Framework.Test]
        public void CheckPermissionForCurrentUser()
        {
            WindowsIdentity identity = WindowsIdentity.GetCurrent();
            Console.WriteLine("WindowsIdentity = '{0}'", identity.Name);

            // Check if I can do the DoSomething operation
            AuthorizationType authorization = _storage.CheckAccess("Test", "TestApplication", "DoSomething",
                identity, DateTime.Now, true);

            Assert.AreEqual(AuthorizationType.Allow, authorization);
        }

        /// 
        /// This test expects that a SqlAzMan db user exists called 'Domain\user'
        /// 
        [NUnit.Framework.Test]
        public void CheckPermissionForStringUser()
        {
            string username = @"Domain\user";
            IAzManDBUser user = _storage.GetDBUser(username);
            Assert.IsNotNull(user, "user is null");

            Console.WriteLine("Db user = '{0}'", user.UserName);

            // Check if this user can do the DoSomething operation
            AuthorizationType authorization = _storage.CheckAccess("Test", "TestApplication", "DoSomething",
                user, DateTime.Now, true);

            Assert.AreEqual(AuthorizationType.Allow, authorization);
        }
    }
}

It's all very nice. The only complaint I've got is that Andrea hasn't caught all the SQL exceptions and given them more meaningful messages. If you try and check access of an operation that doesn't exist you get a nasty SQL exception rather than a NetSqlAzMan message saying that the given operation doesn't exist.

Tuesday, January 02, 2007

Are you confused by character encodings?

I am. Being a native English speaker leads to a number of pathologies, the the most obvious one is our inability to speak foreign languages. What's the point when everyone speaks English? I only learnt to speak Japanese because I worked in a Japanese school for the JET programme for two years, before that I'd been a language dunce. Three years of secondary school French lessons had left me being only able to order a coffee, tell you my name and complain about the weather. There's a similar tendency with English speaking programmers, a total lack of knowledge about character encodings. After learning about ASCII as a boy I really haven't progressed at all beyond thinking that each character is a byte with all the important ones between 32 and 127. I have a vague awareness of Unicode and other things like UTF-8, but I don't really know what they mean in technical terms. If you're like me, it's well worth reading Joel Spolsky's excellent post on his Joel On Software blog where he has a brief potted history of character encodings and what every programmer should know about them. Joel On Software should be required reading for anyone working in the IT world, not just programmers, so long as you pass his stuff through the coding horror filter :)