Creeper documentation and code example : AWS tools and Drupal interactions made easy

Last modified: May 12, 2009 - 00:41

The creeper module offers a wrapper to the tarzan API, so let's start by a code example :

We will code a module that plug the drupal authentication system on an Amazon SimpleDB domain.

A few consideration before we start :
You have installed the creeper module, and set up the AWS keys in the administration > Site configuration > Tarzan settings.
We will named the module simpledbusers.

Now let's implements a very simple external authentication module based on amazon simpleDB

We begin by the .info file.

here is the simpledbusers.info file :

name = simpleDB Users module
description = Module for using Amazon Simple DB as storage for users data
core = 6.x
dependencies[] = creeper

The important thing here is the line

dependencies[] = creeper

Which will allow you to use easily the tarzan API in this module code.

This module is so simple that it needn't a .install file, so move on studying the .module file :

<?php


//////////////////////////////// DRUPAL HOOKS  IMPLEMENTATION ///////////////////////////

/**
* Implementation of hook_menu()
* a simple admin settings screen which will
* allow to set up the salt for encoding users passwords
*/
function simpledbusers_menu() {
 
$items = array();

 
$items['admin/settings/simpledbusers/settings'] = array(
   
'title' => 'Simple DB users Settings',
   
'page callback' => 'drupal_get_form',
   
'page arguments' => array('simpledbusers_admin_settings'),
   
'description' => t('Configure Simple DB Users settings'),
   
'access arguments' => array('administer site configuration'),
   
'file' => 'simpledbusers.admin.inc'
   
);
  return
$items;
}


/**
* implements hook_form_alter()
* in order to change the validator of the default login form
*/

function simpledbusers_form_alter(&$form, $form_state, $form_id) {
 
// Replace the drupal authenticate function is it's used as validation.
 
if($form_id == 'user_login' || $form_id == 'user_login_block'){
    if (
is_array($form['#validate']) && ($key = array_search('user_login_authenticate_validate', $form['#validate']))){
     
$form['#validate'][$key] = 'simpledbusers_login_validate';
    }
    else if (
$key === FALSE) {
     
// Could not find it. Some other module must have run form_alter().
      // We will simply add our validation just before the final validator.
     
$final_validator = array_pop($form['#validate']);
     
$form['#validate'][] = 'simpledbusers_login_validate';
     
$form['#validate'][] = $final_validator;
    }

  }
}

/**
* Main user validation function.
*
* If successful, sets the global $user object.
*
*
*/

function simpledbusers_login_validate($form, &$form_state) {

  global
$user;
  if (!empty(
$user->uid)) {
   
// Another module has already handled authentication.
   
return;
  }

 
$values = $form_state['values'];
 
$pass = trim($values['pass']);

 
// (Design decision) uid=1 (admin user) must always authenticate to local database
  // this user is critical for all drupal admin and upgrade operations so it is best
  // left with drupal's native authentication.
 
$result = db_query("SELECT uid FROM {users} WHERE name = '%s'", $values['name']);
 
$account = db_fetch_object($result);
  if (
$account->uid == 1 ) {
   
$account = user_load(array('name' => $values['name'], 'pass' => trim($values['pass']), 'status' => 1));
   
$user = $account;
   
//drupal way to handle properly an external authentication
   
user_authenticate_finalize($values);
    return;
  }
 
 
// Authenticate on simpledb the user.
 
if(!simpledbusers_auth($values['name'], $pass)) {
   
form_set_error('name',t('Unrecognized username/password'));
  }
  return;
}


/**
* Implements hook_user().
* mainly to allow synchro between the drupal user ref and the simpleDB user ref.
*/
function simpledbusers_user($op, &$edit, &$account, $category = NULL) {
  switch (
$op) {
  
  
//submit op allow to modify the account before it gets saved
   //so each time the account is modified we sync with the SimpleDB domain
  
case 'submit':
   if(
$account->uid != 1){
     
$account->pass=$edit['pass'];
     
simpledbusers_update_account_infos($account,$edit);
     }
   break;
   
//the delete op is called After the delete op in the database
   
case 'delete' :
      if(!
$registration_delete){
        
simpledbusers_user_delete($account);
       }
      break;

    case
'insert':
     
// New user was just added; if we authenticate him via simpledb we sync his data
      // because he has been auto created in drupal db if we are here.
      // we  set the uid generated by drupal in the users domain and the email field from simpledb
     
global $simpledbusers_authenticated;
      if (
$simpledbusers_authenticated) {
        
simpledbusers_create_content_profile($account);
        
//add the validated user role
        
$roles = $account->roles + array(6 => 'validated user');
        
user_save($account, array('roles' => $roles));
        
simpledbusers_update_uid($account);

      }
      else {
//creation of user by registration or admin creation
       
         
$account->pass = $edit['pass']; // we need the clear password
           //we set a false password in database for the user
         
db_query("UPDATE {users} SET pass = '%s' WHERE uid = %d", user_password(), $account->uid);
       
//we create the simpledb item
         
simpledbusers_register_user(array('account' => $account));
      
      }
      break;
  }
}


//////////////////// SIMPLE DB INTERACTIONS FUNCTIONS //////////////////////////

/**
* Authenticate the user against SimpleDB.
*
* @param $name
*   A username.
* @param $pass
*   A password.
*
* @return
*  TRUE if success, FALSE otherwise.
*/
function simpledbusers_auth($name, $pass) {
  global
$simpledbusers_authenticated;
 
// Don't allow empty passwords because they cause problems on some setups.
 
if (empty($pass))
  return
FALSE;


 
$sha265_password = simpledbusers_encode_password($pass);
 
// Try to authenticate.
 
$sdb = new AmazonSDB();
 
$selection = $sdb->select("select name from users where name = '".$name."' and password= '".$sha265_password."'");
    
  if(!
$selection->isOK()) {
   
form_set_error('name',t('Problem during authentication request'));
    return
FALSE;
  }
  if(!isset(
$selection->body->SelectResult->Item))
    return
FALSE;
 
 
//authentication ok :
 
$simpledbusers_authenticated = TRUE;
 
user_external_login_register($name, 'simpledbusers');
  return
TRUE;
}

/*
* Delete a user item in the simpledb users domain.
*/
function simpledbusers_user_delete($account) {

 
$item_name = $account->name;

 
$sdb = new AmazonSDB();
 
 
$del = $sdb->delete_attributes(variable_get('simpledbusers_simpledb_domain','users'),$item_name);
//todo add error handling here....
}



/*
* Update in simpledb the uid user attribute
*
*/

function simpledbusers_update_uid($account)
{
 
$sdb = new AmazonSDB();

$keypairs = array(
   
'drupal_uid' => $account->uid,
  );
 
$put = $sdb->put_attributes(variable_get('simpledbusers_simpledb_domain','users'),$account->name,$keypairs,TRUE);
}

/*
* Update simpledb mail and password user attribute
*
*/

function simpledbusers_update_account_infos($account,$edit)
{ global
$external_account_update_ok;
 
$external_account_update_ok = TRUE;
 
$sdb = new AmazonSDB();
  if(isset(
$edit['mail'])){
 
$keypairs = array('email' => $edit['mail']);
    if(isset(
$edit['pass'])){
       
$keypairs = $keypairs + array('password' => simpledbusers_encode_password($edit['pass']));
    }
   
$put = $sdb->put_attributes(variable_get('simpledbusers_simpledb_domain','users'),$account->name,$keypairs,TRUE);
   
$external_account_update_ok = $put->isOK();
  }
}
/*
* Create an item in the simpledb users domain for a new user
*/
function simpledbusers_register_user($args) {

 
$account = $args['account'];

 
//the password is availaible in clear text at the moment (cf the hook_user code)
 
$account_clear_pwd = $account->pass;
 
$account = user_save($account,  array('authname_simpledbusers' => $account->name));

 
$sdb = new AmazonSDB();

 
$keypairs = array(
   
'name' => $account->name,
   
'email' => $account->mail,
   
'password' => simpledbusers_encode_password($account_clear_pwd),
   
//sample additional field
   
'time_purchased' => time() + (6 * 30 * 24 * 60 * 60),
  );
 
 
$put = $sdb->put_attributes(variable_get('simpledbusers_simpledb_domain','users'),$account->name,$keypairs);
  global
$external_registration_ok;//global var that other modules ca use to check elsewhere to see if anything strange occured
 
if(!$put->isOK())
    
$external_registration_ok = FALSE;//a problem occur if we are here so we set the flag
}


function
simpledbusers_encode_password($clear_password) {
  return
hash('sha256',$clear_password.variable_get('simpledbusers_salt','mys4ltH3r3'));
}


?>

The important things here are the simpleDB interactions functions, with for example :

<?php
function simpledbusers_register_user($args) {

 
$account = $args['account'];

 
//the password is availaible in clear text at the moment (cf the hook_user code)
 
$account_clear_pwd = $account->pass;
 
$account = user_save($account,  array('authname_simpledbusers' => $account->name));

 
$sdb = new AmazonSDB();

 
$keypairs = array(
   
'name' => $account->name,
   
'email' => $account->mail,
   
'password' => simpledbusers_encode_password($account_clear_pwd),
   
//sample additional field
   
'time_purchased' => time() + (6 * 30 * 24 * 60 * 60),
  );
 
 
$put = $sdb->put_attributes(variable_get('simpledbusers_simpledb_domain','users'),$account->name,$keypairs);
  global
$external_registration_ok;//global var that other modules ca use to check elsewhere to see if anything strange occured 
 
if(!$put->isOK())
    
$external_registration_ok = FALSE;//a problem occur so set the flag
}
?>

Or

<?php
/*
* Delete a user item in the simpledb users domain.
*/
function simpledbusers_user_delete($account) {

 
$item_name = $account->name;

 
$sdb = new AmazonSDB();
 
 
$del = $sdb->delete_attributes(variable_get('simpledbusers_simpledb_domain','users'),$item_name);
//todo add error handling here, depending on your needs....
}
?>

You can notice the use of the AmazonSDB class, the entrance point to the SimpleDB API of the tarzan framework :

$sdb = new AmazonSDB();

Simple isn't it ?

the simpleDB domain (a domain is mainly an SQL table equivalent in Amazon simpleDB) used here include a name, a password a drupal_uid field and an additional "time_purchased" field as example.

for the records here is the simpledbusers.admin.inc but it's not the point to focus on for this example :

<?php
function simpledbusers_admin_settings(&$form_state) {

 
$form = array();
 
$form['simpledb-settings'] = array(
   
'#type' => 'fieldset',
   
'#title' => t('Simple DB settings'),
   
'#collapsible' => TRUE,
   
'#collapsed' => FALSE,
  );

 
$form['simpledb-settings']['simpledbusers_simpledb_domain'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Domain Name'),
   
'#default_value' =>  variable_get('simpledbusers_simpledb_domain','users'),
   
'#size' => 50,
   
'#maxlength' => 255,
   
'#description' => t('<p>The SimpleDB domain name where users will be stored.</p>'),
   
'#required' => TRUE,
  );

$form['simpledb-settings']['simpledbusers_salt'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Password Salt'),
   
'#default_value' =>  variable_get('simpledbusers_salt','mys4ltH3r3'),
   
'#size' => 50,
   
'#maxlength' => 255,
   
'#description' => t('<p>The salt used for passwords encryption.</p>'),
   
'#required' => TRUE,
  );

  return
system_settings_form($form);
}

function
simpledbusers_admin_settings_submit($form,&$form_state) {

 
variable_set('simpledbusers_simpledb_domain',$form_state['values']['simpledbusers_simpledb_domain']);
 
variable_set('simpledbusers_salt',$form_state['values']['simpledbusers_salt']);
}
?>

Conclusion
This simple example demonstrate that when you declare a dependencie to the creeper module, all you need to worry to interact with AWS is the tarzan API : http://tarzan-aws.com/docs/2.0/
I have found drupal dev with tarzan very pleasant because you will use 2 good API and you will make powerfull things very easily.

 
 

Drupal is a registered trademark of Dries Buytaert.