Tuesday, October 20, 2009

How to do LDAP authentication using Ruby, How to Run Ruby as a Windows Service and to Write Events into the EventLog (Event Viewer) Service

We needed a way to authenticate users against LDAP and required that the ruby processing running the Authentication code (in a file we called "ad_login_verify.rb") appear as a Windows Service that can be started from services.msc by our networking guys. For *whatever* reasons, when running as a service, it was found that writing to a log file did not work properly.

Anyway, the more network-operations-friendly approach was for me to figure out a way to write into the Windows EventLog Service so that events could be viewed using Event Viewer.

Once everything is set up properly, the objective is to be able to send a query to a url (like http://server:4243/ldap?username=ausername;password=apassword) and to get "AUTH" or "NON-AUTH" back

So the setup is a windows box running Windows Server 2003, with the following base software installed:

Windows 2003 Resource Kit Tools (available here)

ruby (mine is ruby 1.8.6 (2007-09-24 patchlevel 111) [i386-mswin32])

win32-eventlog gem (mine is win32-eventlog (0.4.6) )

ruby-net-ldap gem (min is ruby-net-ldap (0.0.4) )


The contents of ad_login_verify.rb are as follows: (note, sensitive areas surrounded by ** **)


#!C:\ruby\bin ruby

require "rubygems"
require "webrick"
require "net/ldap"
require 'win32/eventlog'
include Win32
include WEBrick

# Write to EventViewer that Service Started
EventLog.open('Application') do |log|
log.report_event(
:source => "CrewResAuthEventSvc",
:event_type => EventLog::WARN,
:category => "0x00000002L".hex,
:event_id => "0x00000003L".hex,
:data => "CrewResAuth Service successfully started"
)
end

class LDAPServlet < HTTPServlet::AbstractServlet
def do_GET( request, response )
username = request.query['username']
password = request.query['password']
ldap_con = initialize_ldap_con("**DOMAIN**\\**USER**","**PASSWORD**")
treebase = "DC= **DOMAIN** , DC =LOCAL"
user_filter = Net::LDAP::Filter.eq( "sAMAccountName", username )
op_filter = Net::LDAP::Filter.eq( "objectClass", "organizationalPerson" )
dn = String.new
ldap_con.search( :base => treebase, :filter => op_filter & user_filter, :attributes=> 'dn') do |entry|
dn = entry.dn
end
login_succeeded = false
unless dn.empty?
ldap_con = initialize_ldap_con(dn,password)
login_succeeded = true if ldap_con.bind
end
response.status = 200
response.body = login_succeeded ? 'AUTH' : 'NON_AUTH'

# Write to Windows Event Log
EventLog.open('Application') do |log|
log.report_event(
:source => "CrewResAuthEventSvc",
:event_type => EventLog::WARN,
:category => "0x00000002L".hex,
:event_id => "0x000003E9L".hex,
:data => "#{username}/#{password} LOGIN #{login_succeeded ? 'SUCEEDED' : '**FAILED**' }"
)
end
end

alias do_POST :do_GET

def initialize_ldap_con(username, password)
Net::LDAP.new( {:host => '**LDAP FQDN**', :port => 389, :auth => { :method => :simple, :username => username, :password => password }} )
end

end

server = HTTPServer.new(:Port => 4243)
server.mount('/ldap', LDAPServlet)

%w(INT TERM).each do |signal|
trap(signal) {server.shutdown}
end

server.start

The event_id values are generated when we build the .dll that will be used to propogate messages from Ruby into the Event Viewer.



STEP 1 Copy Files

On the box that will be running the authentication service, create the following folder: c:\crewres\services
br/>
Copy the following three files into c:\crewres\services:

  • ad_login_verify.rb
  • crewResAuthStart.bat
  • CrewResAuthEventSvc.dll

Once these files are coped over, you can test that things work by dropping to a command prompt, going to the c:\crewres\services folder, typing crewResAuthStart.bat and then going to a web browser and typing in the url above.


The contents of crewResAuthStart.bat is simply the following command:


c:\ruby\bin\ruby.exe c:\crewres\services\ad_login_verify.rb

The reason this is done will become apparent as we set things up to run as a windows service.


STEP 2 Create a Service

This is a two-step process. NOTE – for this section, I essentially followed the instructionshere

2.1) First we create a “stub” Windows Service using SRVANY.

From a command prompt, type:

INSTSRV "CrewResAuthSvc" "C:\Program Files\Windows Resource Kits\Tools\srvany.exe"

This will create a service in Service Manager called “CrewResAuthSvc”. Note the path to srvany may differ.

2.2) Second, you need to edit the registry, telling it what command to run when the service is started.

Navigate to the registry key: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\CrewResAuthSvc
Add a key called Parameters
Underneath Parameters, add a Value called Application with a value being C:\crewres\services\CrewResAuthStart.bat

Now, you should be able to start the service using services.msc

NOTE ON STOPPING THE SERVICE - Please note that a SRVANY solution does not have a way to actually stop a service. You can click on “Stop Service” in services.msc and you will see that it indicates that the service has stopped.

HOWEVER, SRVANY does not have a way to stop ruby (or *whatever* it had launched originally).

Therefore, you have to go to the Windows Task Manager and kill the ruby.exe Process manually.

Note – a tricky way to determine the PID of the process (assuming you have MKS toolkit installed) is to do the following:
ps -a | grep "ruby.exe" | head -1 | awk '{print $1}'

this will print out the PID, after which you can do kill -9 *PID*

STEP 3 Register the .dll as an Event Source for Event Viewer

There is a list of Services that are allowed to send messages to the EventLog Service.

This list may be found under the following registry key: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Eventlog\Application\Sources

Now, since this is weird registry type that you cannot directly edit (it’s a bunch of null-terminated strings), you have to run some code that will insert CrewResAuthEventSvc into the list of “approved” event sources.

Do this by running the following command from the command prompt (this file is also in .svn)

ruby registerEventSource.rb

The contents of registerEventSource.rb are as follows:

require 'win32/eventlog'
include Win32

dll_file = 'C:\crewres\services\CrewResAuthEventSvc.dll'

EventLog.add_event_source(
"source" => "Application",
"key_name" => "CrewResAuthEventSvc",
"category_count" => 2,
"event_message_file" => dll_file,
"category_message_file" => dll_file
)

After running this, the service "CrewResAuthEventSvc" is on the list of allowed services who can write events into the event log.

How to Create/Build the .DLL

CrewResAuthEventSvc.dll is a .dll that marshals event messages into the Windows EventLog. ("marshal" may not be the right word, but that's what I'm calling it)

I essentially followed the instructions found at
http://rubyforge.org/docman/view.php/85/1734/mc_tutorial.html

To build the .dll, you need to first create an .mc file and then compile that into a .dll

The .dll really does nothing other than to accept messages from Ruby and posts them to the event viewer.

You will need Visual Studio installed and a program called mc.exe that creates header (.h) files and .RES files, which are used by the linker to create a .dll.

I created a file, “CrewResAuthEventSvc.mc” to this end.

The contents of CrewResAuthEventSvc.mc are as follows:

LanguageNames =
(
English = 0x0409:Messages_ENU
)


;////////////////////////////////////////
;// Eventlog categories
;//
;// These always have to be the first entries in a message file
;//

MessageId = 1
SymbolicName = CATEGORY_ONE
Severity = Success
Language = English
First category event
.

MessageId = +1
SymbolicName = CATEGORY_TWO
Severity = Success
Language = English
LDAP Authentication Event
.


;////////////////////////////////////////
;// Events
;//

MessageId = +1
SymbolicName = EVENT_STARTED
Language = English
CrewResAuth Service successfully started
.


;////////////////////////////////////////
;// Additional messages
;//

MessageId = +1
SymbolicName = CREWRES_AUTH
Language = English
XFO AUTH: %1
.


Note that there must be an extra CRLF at the end of the file.

To build the .mc file into a .dll, you must execute the following three commands, which I put into a batch file for convenience:

Contents of buildDll.bat

@ECHO OFF
call "C:\Program Files\Microsoft Visual Studio 8\VC\bin\vcvars32.bat"
mc CrewResAuthEventSvc.mc
rc -r CrewResAuthEventSvc.rc
link -dll -noentry -machine:x86 -out:CrewResAuthEventSvc.dll CrewResAuthEventSvc.RES


Once the .dll is built, copy it into c:\crewres\services folder and do the above voodoo to register CrewResAuthEventSvc as a Registered Event Source.

Note that you need to look into the generated header (.h) file to see which event_id to use.


Here is the contents of the generated .h file:

////////////////////////////////////////
// Eventlog categories
//
// These always have to be the first entries in a message file
//
//
// Values are 32 bit values layed out as follows:
//
// 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
// 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
// +---+-+-+-----------------------+-------------------------------+
// |Sev|C|R| Facility | Code |
// +---+-+-+-----------------------+-------------------------------+
//
// where
//
// Sev - is the severity code
//
// 00 - Success
// 01 - Informational
// 10 - Warning
// 11 - Error
//
// C - is the Customer code flag
//
// R - is a reserved bit
//
// Facility - is the facility code
//
// Code - is the facility's status code
//
//
// Define the facility codes
//


//
// Define the severity codes
//


//
// MessageId: CATEGORY_ONE
//
// MessageText:
//
// First category event
//
#define CATEGORY_ONE 0x00000001L

//
// MessageId: CATEGORY_TWO
//
// MessageText:
//
// LDAP Authentication Event
//
#define CATEGORY_TWO 0x00000002L

////////////////////////////////////////
// Events
//
//
// MessageId: EVENT_STARTED
//
// MessageText:
//
// CrewResAuth Service successfully started
//
#define EVENT_STARTED 0x00000003L

////////////////////////////////////////
// Additional messages
//
//
// MessageId: CREWRES_AUTH
//
// MessageText:
//
// XFO AUTH: %1
//
#define CREWRES_AUTH 0x000003E9L

Do you see the two id's I used in my Ruby code? CREWRES_AUTH is id 0x000003E9L and the one I use to tell event viewer that the service has started is id of 0x00000002L

Anyway, the ids you will use will be generated by mc.exe from the .mc file and will appear in your header file.

That's it!

Lots of steps. Somewhat complicated. But the good news is that it works!

Good luck,

Shannon Norrell