Sunday, 20 March 2011

Combining F# and log4net to create a domain authentication tool for Perforce

F# is really just like any other .NET language or at least it becomes so once you get your head around certain "stranges" and a few syntax differences to C#. Rather than explaining each of these differences I expect that the majority of readers will be able to put 1 and 1 together from the code example below and will focus on those that are not obvious.

Those of you familiar with Perforce will know that Domain Authentication is achieved by means of a server side trigger which calls an executable to perform an ActiveDirectory validation of the user and password passed. Perforce has a tool for this which they provide free of charge, documented in their Knowledge Base (article 74). However the code is hard to read (predominantly because of the ActiveDirectory C++ API), and the command line is far more complicated than it need be.

The example below is an attempt at making this tool significantly easier to read and use,while using F# combined with log4net. There are 3 files to the solution:
  1. AssemblyInfo.fs
  2. auth.fs
  3. app.config
The result is a tool that only requires a domain and user name to be passed from the Perforce trigger to authenticate the user, and to log Perforce login activity to any number of destinations.
===================================================================
Using an 'AssemblyInfo' file in F# is just like in C#  except maybe for the closing '()' which is a do-binding; MSDN has an article with the details. The only thing that is really different and important to know in F# is that  file order matters, this means that in our example 'AssemblyInfo.fs' must come before 'auth.fs' in the '.fsproj' file, more details below.


#light
namespace AntekBaranski
open System.Reflection
open System.Runtime.CompilerServices
open System.Runtime.InteropServices
[<assembly: AssemblyCompany("Company")>]
[<assembly: AssemblyProduct("auth")>]
[<assembly: AssemblyName("Name")>]
[<assembly: AssemblyCopyright("Copyright")>]
[<assembly: AssemblyTrademark("Trademark ™")>]
[<assembly: AssemblyDescription("Description")>]
#if DEBUG
[<assembly: AssemblyConfiguration("Debug")>]
#else
[<assembly: AssemblyConfiguration("Release")>]
#endif
[<assembly: ComVisible(false)>]
[<assembly: AssemblyVersion("1.0.0.0")>]
[<assembly: AssemblyFileVersion("1.0.0.0")>]
// Instructs log4net to read its configuration from AppName.exe.config file
[<assembly: log4net.Config.XmlConfigurator(Watch = true)>]
()
===================================================================
The F# code for the main ActiveDirectory validation looks and feels just like C#, even the inclusion of log4net isn't any different, it all just tastes like chicken. However there is an important difference, above I mentioned that file order matters in a F# project, and that's because of the '[<EntryPoint>] ' attribute, it tells F# where to begin execution. The '[<EntryPoint>] ' attribute must be part of the last '.fs' file in a F# project, otherwise the compiler will complain about its location.

#light
namespace AntekBaranski
open System
open System.IO
open System.DirectoryServices.AccountManagement
open log4net
module p4auth_ad = 
    // Initialise the log4net logger with the AppName in this case

    let logger = LogManager.GetLogger("p4auth_ad")
    let ValidateCredentails (domain : string) (user : string) : int =
        try
            let pc = new PrincipalContext(ContextType.Domain, domain)
            let passwd : string = System.Console.ReadLine()
            if pc.ValidateCredentials(user, passwd)
                then logger.DebugFormat("User authentication succesful for {0}", user)
                     0
                else logger.WarnFormat("Unknown user or bad password for {0}", user)
                     1
        with
            | _ as ex
                -> logger.Error("Network or other error occured", ex)
                   1
    [<EntryPoint>]
    let Main args =
        match args with
            | [| domain; user |] ->
                ValidateCredentails domain user
            | _ ->
                logger.Fatal("Usage: auth domain user")
                1
===================================================================
The app.config file is no different than with any other .NET language and because its using log4net, user authentication from Perforce can now be logged in any number of ways, in this case I choose to write to the EventLog.


<?xml version="1.0" encoding="utf-8"?> 
<configuration> 
  <configSections> 
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" /> 
  </configSections> 
  <startup> 
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0" /> 
  </startup> 
  <log4net> 
    <appender name="DefaultAppender" type="log4net.Appender.EventLogAppender" > 
      <layout type="log4net.Layout.PatternLayout"> 
        <conversionPattern value="%date [%thread] %-5level %logger - %message%newline" /> 
      </layout> 
    </appender> 
    <root> 
      <priority value="ALL" /> 
      <appender-ref ref="DefaultAppender" /> 
    </root> 
  </log4net> 
</configuration>

Saturday, 5 March 2011

A simple MSBuild task in F# to calculate the MD5 hash of a file

namespace MSBuild.Tasks

open System
open System.IO
open System.Security.Cryptography
open Microsoft.Build.Utilities
open Microsoft.Build.Framework

type Md5Sum() =
    inherit Task()

    let mutable inputFile = ""
    let mutable md5Value = ""

    [<Required>]
    member this.InputFile
        with get() = inputFile
        and set(v) = inputFile <- v

    [<Output>]
    member this.Md5Value
        with get() = md5Value
        and set(v) = md5Value <- v

    override this.Execute() = 
        let resultCode =
            try
                base.Log.LogMessage(MessageImportance.Normal, "Computing MD5 hash for {0}", inputFile)
                let md5csp = MD5CryptoServiceProvider.Create()
                let md5Value = inputFile |> File.ReadAllBytes |> md5csp.ComputeHash |> Convert.ToBase64String
                base.Log.LogMessage(MessageImportance.Normal, "Computed MD5 hash is {0}", md5Value.ToString())
                true
            with
                | _ as ex -> base.Log.LogErrorFromException(ex, false); false
        resultCode