Transactions and Authorization made simple
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.