Tuesday, December 29, 2009

Understanding Authlogic Plugin Dynamics

authlogic is by far and away my favourite authentication framework for Rails. I've raved enough in my slides on Authlogic_RPX.

It's true beauty is making authentication so unobtrusive for application developers.

However, the same can't be said for Authlogic plugin developers. I spent quite a bit of time meandering through the authlogic source and other plugins in order to produce Authlogic_RPX (the RPX plugin for authlogic, to support JanRain's RPX service).

I recently returned to the Authlogic_RPX in order to provide an update that finally adds identity mapping (with contributions from John and Damir; thanks guys!).

Luckily my previous exploits were recent enough that much of what I learned about authlogic were still pretty fresh. But before I forget it all again, I thought it would be worthwhile to write up a few of the "insights" I had on the authlogic source.

Hence this post. I'm just going to focus on one thing for now. Since authlogic is so "unobtrusive", one of the big conceptual hurdles you need to get over if you are attempting to write an authlogic plugin is simply:
Just how the heck does it all get loaded and mixed in with my models??

(To follow this discussion, I'd recommend you have a plugin close to hand. Either my previously mentioned Authlogic_RPX, or another like Authlogic_OAuth, or Authlogic_openid)

By unobtrusive, I mean like this. Here is the minimal configuration for a user model that uses Authlogic_RPX:
  class User < ActiveRecord::Base
acts_as_authentic
end

Pretty simple, right? But what power lies behind that little "acts_as_authentic" statement?

What follows is my attempt at a description of what goes on behind the scenes..

First: get loaded


The main file in an authlogic plugin/gem is going to have the relevant requires to the library files. But they do squat. We start mixing in our plugin with the includes and helper registrations:
require "authlogic_rpx/version"
require "authlogic_rpx/acts_as_authentic"
require "authlogic_rpx/session"
require "authlogic_rpx/helper"
require "authlogic_rpx/rpx_identifier"

ActiveRecord::Base.send(:include, AuthlogicRpx::ActsAsAuthentic)
Authlogic::Session::Base.send(:include, AuthlogicRpx::Session)
ActionController::Base.helper AuthlogicRpx::Helper

Note that your plugin ActsAsAuthentic module get's mixed in with ActiveRecord itself (not just a specific ActiveRecord model). That's crucial to remember when considering class methods in your plugin (they are basically global across all ActiveRecord).

What including ActsAsAuthentic in ActiveRecord::Base does..


What happens when the previous lines included the plugin's ActsAsAuthentic module?
The self.included method handles the initial bootstrap..


module AuthlogicRpx
module ActsAsAuthentic
def self.included(klass)
klass.class_eval do
extend Config
add_acts_as_authentic_module(Methods, :prepend)
end
end
..

Here we see we do a class_eval on the class that the module is included in (i.e. ActiveRecord::Base). You'll immediately get the sense we're doing some kind of mixin with the Config and Methods modules. The Config / Methods module structure is a common pattern you will see throughout authlogic.

extend Config takes the Config module (AuthlogicRpx::ActsAsAuthentic::Config) and add it to the ActiveRecord::Base class cdefinition. i.e. methods defined in Config become class methods of ActiveRecord::Base. (If you add a def self.extended(klass) method to Config you'll be able to hook the extension).

add_acts_as_authentic_module(Methods, :prepend) adds the Methods module (AuthlogicRpx::ActsAsAuthentic::Methods) to the authlogic modules list. That's all. Take a look at add_acts_as_authentic_module:


def add_acts_as_authentic_module(mod, action = :append)
modules = acts_as_authentic_modules
case action
when :append
modules << mod
when :prepend
modules = [mod] + modules
end
modules.uniq!
write_inheritable_attribute(:acts_as_authentic_modules, modules)
end


Ready to launch..


It is only when we add the acts_as_authentic in our model class that things start to happen. This method loads all the modules from the list built up by all the call(s) to "add_acts_as_authentic_module". Note the include in the last line of the method:

def acts_as_authentic(unsupported_options = nil, &block)
# Stop all configuration if the DB is not set up
return if !db_setup?

raise ArgumentError.new("You are using the old v1.X.X configuration method for Authlogic. Instead of " +
"passing a hash of configuration options to acts_as_authentic, pass a block: acts_as_authentic { |c| c.my_option = my_value }") if !unsupported_options.nil?

yield self if block_given?
acts_as_authentic_modules.each { |mod| include mod }
end


Ignition..


Once the include is invoked, our plugin will usually hook the event and do some setup activity in our module's def self.included method.


module Methods
def self.included(klass)
klass.class_eval do
..
end
..
end
..

Unlike the Config extension, the class you are including in (the klass parameter in the example), is the specific ActiveRecord model you have marked as "acts_as_authentic".

In other words, the methods in the Methods module get included as instance methods for the specific ActiveRecord models class (User in the example I presented earlier).

Hanging it on the line..


Let's hang it all out in a simplified and contrived example. Take this basic structure:

module AuthlogicPlugin
module ActsAsAuthentic
def self.included(klass)
klass.class_eval do
extend Config
add_acts_as_authentic_module(Methods, :prepend)
end
end
module Config
def config_item
end
end
module Methods
def self.included(klass)
klass.class_eval do
def self.special_setting
end
end
end
def instance_item
end
end
end
end

If we add this to our User model, then the result we'd end up with is this:

  • config_item: will be a class method on ActiveRecord::Base

  • instance_item: will be an instance method on User

  • special_setting: will be a class method on User



Conclusions & Implications?


I've covered the main points in bootstrapping authlogic. There's obviously a lot more that goes on, but I think once you get these basics it makes authlogic-related code so much easier to read and understand. It's a pretty neat demonstration of dynamic ruby at work.

Understanding the loading process is also makes it possible to be definitive about how your application will behave, rather than just treating it as a heuristic black box.

Take authlogic configuration settings for example. Say we have a configuration parameter in our plugin called "big_red_button" that takes values :on and :off.

Syntactically, both of these user model definitions are valid:


class User < ActiveRecord::Base
acts_as_authentic do |c|
c.big_red_button :on
end
end

class User < ActiveRecord::Base
acts_as_authentic
big_red_button :on
end

However, the behaviour is slightly different, and the difference will be significant if you have any initialisation code in the plugin that cares about the setting of the big_red_button.

In the second case, it should be clear that setting big_red_button :on only happens after all the plugin initialisation is complete.

But in the first case, it is a little more subtle. If you go back to review the acts_as_authentic method you'll see that setting the big_red_button occurs at yield self if block_given?. Implications:

  • Config extension of ActiveRecord::Base takes place before the big_red_button is set

  • Method methods are included in the User model before the big_red_button is set

  • Method's def self.included is called after the big_red_button is set (meaning you can safely do conditional initialisation here based on the big_red_button setting)


How's that? Pretty cool stuff, but thankfully as I mentioned before, these details only really concern plugin authors and anyone who just loves to read dynamic ruby code.

There's much more to authlogic that what I've discussed here of course (and RPX). Perhaps good fodder for a future post? Let's see..


Soundtrack for this post: Because it's There - Michael Hedges

No comments: