Thursday, October 14, 2010

Python webservice that executes local commands

There are a few different options when it comes to managing server-side scripting on a web site. Usually folks use php or perl and even python in many occasions.

This blog post is about using python to execute code locally on the server in response to http GET requests.

So far you are thinking so what? You are already crafting your comment and it is saying something like, "Google mod_python" or "Google mod_perl". You are right, the best way to do CGI is via mod_perl, mod_php or mod_perl. The problem is user access and chroot.

Apache will execute server side scripts as the user / group defined in the main httpd.conf. In my case: apache / apache.
Apache will also assume a document root of /var/www/ for scripts (on a Centos 5.5 box) even if the userdir module is in use.

My problem was: How to get apache to execute scripts as dave:dave on doc root = /home/dave/. It was critical to get this working because the scripts in question interact with the .gnupg/pubkeyring and .gnupg/seckeyring files under /home/dave/.gnupg/.

Basically, I was making some kind of web based PGP key server. A web based gui for remote users to manage keys.

In the end I settled for python and the BaseHTTPServer.

First of all a simple class that will accept a shell command, execute it and return the stdout.


import popen2

class MyShellCommand:

"""execute a command, capture stdout and return it."""
def __callShellCmd(self, cmd):

stdout, stdin = popen2.popen2(command)
data = ""
while True:
c = stdout.read(1)
if( c ):
data += c
else:
break
return data

"""concrete example"""
def getPublicGPGKey(self, keyid):

"""TODO: Add logic to validate key id..."""
command = "gpg -a --export '%s'" % keyid
return self.__callShellCmd(command)


Now that we have a utility to retrieve public keys from the gpg keyring, lets call it from a webservice that is owned and operated by user:group, dave:dave.


import MyShellCommand
import time
import BaseHTTPServer, cgi

"""Configure host ip and port to listen on. Use high port for non root users."""
HOST_NAME = '127.0.0.1'
PORT_NUMBER = 8080

class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler):

def do_HEAD(s):
s.send_response(200)
s.send_header("Content-type", "text/html")
s.end_headers()

def do_GET(s):
s.send_response(200)
s.send_header("Content-type", "text/html")
s.end_headers()

"""Get the path and find the parameters"""
path, query_string = s.path.split('?', 1)
params = dict(cgi.parse_qsl(query_string))

"""create a shell object"""
shell = MyShellCommand()

"""Validate the call being made"""
if path == '/publickey':
"""Validate the parameters"""
if params.has_key('id'):
s.wfile.write('%s' % shell.getPublicGPGKey(params['id']))

"""I dont like descriptive errors."""
else: s.wfile.write('An error occurred.')
else: s.wfile.write('An error occurred.')



if __name__ == '__main__':
server_class = BaseHTTPServer.HTTPServer
httpd = server_class((HOST_NAME, PORT_NUMBER), MyHandler)
print time.asctime(), "Server Starts - %s:%s" % (HOST_NAME, PORT_NUMBER)
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
httpd.server_close()
print time.asctime(), "Server Stops - %s:%s" % (HOST_NAME, PORT_NUMBER)


If you execute the above script, you will have a working webservice that responds nicely to only one specific set of GET data. Call it with a URL like this:


http://127.0.0.1:8080/publickey?id=mykeyname

Friday, October 1, 2010

Use PHP to perform an LDAP Bind to Windows Active Directory for User Authentication

This example shows some basic LDAP bind lookups on a Windows 2003 active directory server.

The sequence is:

  • Connect 
  • Bind using privileged user 
  • search for 'dn' of supplied credentials
  • re-bind using this dn and password of supplied credentials
  • unbind

The per-requisite is that php_ldap modules are loaded and compiled.  These are fairly standard now days.  In fedora it was just a matter of executing:



# yum install php-ldap

Anyway:  Here is the example.  It's farily well commented so should be a simple matter to make work in your own environment.



<?php
// example.php
//
// David Latham @ 2010
// david-latham.blogspot.com
//
// This code cannot be executed on the same server as AD is installed on!!!
//
// Active Directory has an Organizational Unit of "_Test_Users" with 3 users loaded
// into it.
// user1 | User One
// user2 | User Two
// ldapbind | ldap bind
//
// execute with php -q example.php

// credentials to test
$user='user2@example.local';
// user='user2'; //Use this one to test binding without an email address.
$pass='2wsx#EDC';

// AD server details
$ldap_server = "192.168.122.231";
$dn = "OU=_Test_Users,DC=example,DC=local";

// Connect
$ad = ldap_connect($ldap_server); //Must be a real active directory server
//(WIN2003 used in this example)

// Set some variables
ldap_set_option($ad, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ad, LDAP_OPT_REFERRALS, 0);

// Bind to the ldap directory.
// Here we are binding using a fixed user who has privileges to bind to the
// active directory. This user also has a fixed password. It can not change
// otherwise this script will break.


$bd = @ldap_bind($ad,"ldap user","pa$$word")
or die("Couldn't bind to AD!");

// Search the directory for a distinguished name (dn) using the supplied
// credentials.
//
// If $user is an email address then use 'mail' otherwise use 'sAMAccountName'

if( strpos($user, '@') > 0 ) {
$field='mail';
} else {
$field='sAMAccountName';
}
echo "Searching AD using ...".$field."\n";

$result = @ldap_search($ad, $dn, "(".$field."=".$user.")", array('dn'));

// Create result set
$entries = @ldap_get_entries($ad, $result);

// Show an array of result
print_r($entries);

// If we have a result then we should 're-bind' using the destinguished name found
// in the search.
if ( $entries[0] ) {
if(@ldap_bind($ad, $entries[0]['dn'], $pass)) {
// If bind was successful then show some data. This is user
// authentication success.
echo "found: ".$entries[0]['dn'];
} else {
// Bind failed so this would equate to user authentication failure.
echo "Failed to bind.";
}
} else {
// Failure to search would indicate a problem with your $dn variable.
echo "failed to search";
}

//never forget to unbind!
ldap_unbind($ad);

?>

the result might look something like this:



[dave@fedora php-ldap]$ php -q example.php
Searching AD using ...mail
Array
(
[count] => 1
[0] => Array
(
[count] => 0
[dn] => CN=user2,OU=_Test_Users,DC=example,DC=local
)

)
found: CN=user2,OU=_Test_Users,DC=example,DC=local

Have fun authenticating your users against your work active directory. You will:

  • Please your security manager

  • Avoid the headache of managing passwords password compliance

  • Write safer applications in terms of locking them down

  • Feel good about yourself