Transactions and Authorization made simple

| No Comments | No TrackBacks

So I really like to follow DRY: Don't Repeat Yourself. In the development of Epitafio (A cemetery management system I mentioned earlier), I was workin on my model classes - note that this is not a DBIC model, but a regular model that do access a DBIC schema - and I realized that for every single method of the models I would need to do two things:


  • Enclose code in a transaction, much like:
    $schema->txn_do(sub { ... })

  • Authorize the user against a specific role:
    die 'Access denied!' unless $user->in_role('foo')

So I started wondering at #catalyst if there would be a pretty way of doing it. I was already using Catalyst::Component::InstancePerContext, but mst quickly guided me to avoid saving the context itself in the object, but rather getting the values I need from there. Since my app models will basically follow this same principle I did a model superclass with:


package Epitafio::Model;
use Moose;
with 'Catalyst::Component::InstancePerContext';
has 'user' => (is => 'rw');
has 'dbic' => (is => 'rw');

sub build_per_context_instance {
my ($self, $c) = @_;
$self->new(user => $c->user->obj,
dbic => $c->model('DB')->schema->restrict_with_object($c->user->obj));
}
1;

Note that I'm still using the C::M::DBIC::Schema as usual, but I'm additionally making a local dbic schema that is restricted according with the logged user. Check DBIx::Class::Schema::RestrictWithObject for details on how that works, and mst++ for the tip.

Ok, now my model classes can know which user is logged in (in a Cat-independent way) as well as have access to the main DBIC::Schema used in the application. Now we just need to DRO - Don't Repeat Ourselves.

Following, again, mst++ tip, I decided against doing a more fancy solution and gone to a plain and simple:


txn_method 'foo' => authorize 'rolename' => sub {
...
}

For those who didn't get how that is parsed, this could be rewritten as:


txn_method('foo',authorize('rolename',sub { }))

This works as:


  • authorize receives a role name and a code ref and returns a code ref that does the user role checking before invoking the actual code.
  • txn_method receives the method name and a code ref and installs a new coderef that encloses the given coderef into a transcation in the package namespace as if it were a regular sub definition.

That means you can have a txn_method without authorization, but you would require

our &foo = authorize 'rolename' => sub { ... }

to get authorization without transaction. But as in my application I'll probably have both most of the time, I thought it should suffice the way it is.

But for the txn_method..authorize thing to parse, both subs need to be in the package namespace at BEGIN time, so to solve that, without having to re-type it every time, I wrote a simple Epitafio::ModelUtil module that exports this helpers.


package Epitafio::ModelUtil;
use strict;
use warnings;
use base 'Exporter';

our @EXPORT = qw(txn_method authorized);

sub txn_method {
my ($name, $code) = @_;
my $method_name = caller().'::'.$name;
no strict 'refs';
*{$method_name} = sub {
$_[0]->dbic->txn_do($code, @_)
};
}

sub authorized {
my ($role, $code) = @_;
return sub {
if ($_[0]->user->in_role($role)) {
$code->(@_);
} else {
die 'Access Denied!';
}
}
}

1;

And now the code of the model looks just pretty and non-repetitive ;). See the sources for the full version.

No TrackBacks

TrackBack URL: http://daniel.ruoso.com/cgi-bin/mt/mt-tb.cgi/142

Leave a comment

About this Entry

This page contains a single entry by Daniel Ruoso published on August 18, 2009 12:14 PM.

SMOPP5 first steps was the previous entry in this blog.

Missing :ignoreaccent in Perl 5 is the next entry in this blog.

Find recent content on the main index or look in the archives to find all content.