It's ->find_user($authinfo, $c), not ->find_user($c, $authinfo).
[catagits/Catalyst-Authentication-Realm-Adaptor.git] / lib / Catalyst / Authentication / Realm / Adaptor.pm
1 package Catalyst::Authentication::Realm::Adaptor;
2
3 use warnings;
4 use strict;
5 use Carp;
6 use Moose;
7 extends 'Catalyst::Authentication::Realm';
8
9 =head1 NAME
10
11 Catalyst::Authentication::Realm::Adaptor - Adjust parameters of authentication processes on the fly
12
13 =head1 VERSION
14
15 Version 0.01
16
17 =cut
18
19 ## goes in catagits@jules.scsys.co.uk:Catalyst-Authentication-Realm-Adaptor.git
20
21 our $VERSION = '0.01';
22
23 sub authenticate {
24     my ( $self, $c, $authinfo ) = @_;
25
26     my $newauthinfo;
27
28     if (exists($self->config->{'credential_adaptor'})) {
29
30         if ($self->config->{'credential_adaptor'}{'method'} eq 'merge_hash') {
31
32             $newauthinfo = _munge_hash($authinfo, $self->config->{'credential_adaptor'}{'merge_hash'}, $authinfo);
33
34         } elsif ($self->config->{'credential_adaptor'}{'method'} eq 'new_hash') {
35
36             $newauthinfo = _munge_hash({}, $self->config->{'credential_adaptor'}{'new_hash'}, $authinfo);
37
38         } elsif ($self->config->{'credential_adaptor'}{'method'} eq 'action') {
39
40             my $controller = $c->controller($self->config->{'credential_adaptor'}{'controller'});
41             if (!$controller) {
42                 Catalyst::Exception->throw(__PACKAGE__ . " realm: " . $self->name . "'s credential_adaptor tried to use a controller that doesn't exist: " .
43                                             $self->config->{'credential_adaptor'}{'controller'});
44             }
45
46             my $action = $controller->action_for($self->config->{'credential_adaptor'}{'action'});
47             if (!$action) {
48                 Catalyst::Exception->throw(__PACKAGE__ . " realm: " . $self->name . "'s credential_adaptor tried to use an action that doesn't exist: " .
49                                             $self->config->{'credential_adaptor'}{'controller'} . "->" .
50                                             $self->config->{'credential_adaptor'}{'action'});
51             }
52             $newauthinfo = $c->forward($action, $self->name, $authinfo, $self->config->{'credential_adaptor'});
53
54         } elsif ($self->config->{'credential_adaptor'}{'method'} eq 'code' ) {
55
56             if (ref($self->config->{'credential_adaptor'}{'code'}) eq 'CODE') {
57                 my $sub = $self->config->{'credential_adaptor'}{'code'};
58                 $newauthinfo = $sub->($self->name, $authinfo, $self->config->{'credential_adaptor'});
59             } else {
60                 Catalyst::Exception->throw(__PACKAGE__ . " realm: " . $self->name . "'s credential_adaptor is configured to use a code ref that doesn't exist");
61             }
62         }
63         return $self->SUPER::authenticate($c, $newauthinfo);
64     } else {
65         return $self->SUPER::authenticate($c, $authinfo);
66     }
67 }
68
69 sub find_user {
70     my ( $self, $authinfo, $c ) = @_;
71
72     my $newauthinfo;
73
74     if (exists($self->config->{'store_adaptor'})) {
75
76         if ($self->config->{'store_adaptor'}{'method'} eq 'merge_hash') {
77
78             $newauthinfo = _munge_hash($authinfo, $self->config->{'store_adaptor'}{'merge_hash'}, $authinfo);
79
80         } elsif ($self->config->{'store_adaptor'}{'method'} eq 'new_hash') {
81
82             $newauthinfo = _munge_hash({}, $self->config->{'store_adaptor'}{'new_hash'}, $authinfo);
83
84         } elsif ($self->config->{'store_adaptor'}{'method'} eq 'action') {
85
86             my $controller = $c->controller($self->config->{'store_adaptor'}{'controller'});
87             if (!$controller) {
88                 Catalyst::Exception->throw(__PACKAGE__ . " realm: " . $self->name . "'s store_adaptor tried to use a controller that doesn't exist: " .
89                                             $self->config->{'store_adaptor'}{'controller'});
90             }
91
92             my $action = $controller->action_for($self->config->{'store_adaptor'}{'action'});
93             if (!$action) {
94                 Catalyst::Exception->throw(__PACKAGE__ . " realm: " . $self->name . "'s store_adaptor tried to use an action that doesn't exist: " .
95                                             $self->config->{'store_adaptor'}{'controller'} . "->" .
96                                             $self->config->{'store_adaptor'}{'action'});
97             }
98             $newauthinfo = $c->forward($action, $self->name, $authinfo, $self->config->{'store_adaptor'});
99
100         } elsif ($self->config->{'store_adaptor'}{'method'} eq 'code' ) {
101
102             if (ref($self->config->{'store_adaptor'}{'code'}) eq 'CODE') {
103                 my $sub = $self->config->{'store_adaptor'}{'code'};
104                 $newauthinfo = $sub->($self->name, $authinfo, $self->config->{'store_adaptor'});
105             } else {
106                 Catalyst::Exception->throw(__PACKAGE__ . " realm: " . $self->name . "'s store_adaptor is configured to use a code ref that doesn't exist");
107             }
108         }
109         return $self->SUPER::find_user($newauthinfo, $c);
110     } else {
111         return $self->SUPER::find_user($authinfo, $c);
112     }
113 }
114
115 sub _munge_hash {
116     my ($sourcehash, $modhash, $referencehash) = @_;
117
118     my $resulthash = { %{$sourcehash} };
119
120     foreach my $key (keys %{$modhash}) {
121         if (ref($modhash->{$key}) eq 'HASH') {
122             if (ref($sourcehash->{$key}) eq 'HASH') {
123                 $resulthash->{$key} = _munge_hash($sourcehash->{$key}, $modhash->{$key}, $referencehash)
124             } else {
125                 $resulthash->{$key} = _munge_hash({}, $modhash->{$key}, $referencehash);
126             }
127         } else {
128             if (ref($modhash->{$key} eq 'ARRAY') && ref($sourcehash->{$key}) eq 'ARRAY') {
129                 push @{$resulthash->{$key}}, _munge_value($modhash->{$key}, $referencehash)
130             }
131             $resulthash->{$key} = _munge_value($modhash->{$key}, $referencehash);
132             if (ref($resulthash->{$key}) eq 'SCALAR' && ${$resulthash->{$key}} eq '-') {
133                 ## Scalar reference to a string '-' means delete the element from the source array.
134                 delete($resulthash->{$key});
135             }
136         }
137     }
138     return($resulthash);
139 }
140
141 sub _munge_value {
142     my ($modvalue, $referencehash) = @_;
143
144     my $newvalue;
145     if ($modvalue =~ m/^([+-])\((.*)\)$/) {
146         my $action = $1;
147         my $keypath = $2;
148         ## do magic
149         if ($action eq '+') {
150             ## action = string '-' means delete the element from the source array.
151             ## otherwise it means copy it from a field in the original hash with nesting
152             ## indicated via '.' - IE similar to Template Toolkit handling of nested hashes
153             my @hashpath = split /\./, $keypath;
154             my $val = $referencehash;
155             foreach my $subkey (@hashpath) {
156                 if (ref($val) eq 'HASH') {
157                     $val = $val->{$subkey};
158                 } elsif (ref($val) eq 'ARRAY') {
159                     $val = $val->[$subkey];
160                 } else {
161                     ## failed to find that key in the hash / array
162                     $val = undef;
163                     last;
164                 }
165             }
166             $newvalue = $val;
167         } else {
168             ## delete the value... so we return a scalar ref to '-'
169             $newvalue = \'-';
170         }
171     } elsif (ref($modvalue) eq 'ARRAY') {
172         $newvalue = [];
173         foreach my $row (0..$#{$modvalue}) {
174             if (defined($modvalue->[$row])) {
175                 my $val = _munge_value($modvalue->[$row], $referencehash);
176                 ## this is the first time I've ever wanted  to use unless
177                 ## to make things clearer
178                 unless (ref($val) eq 'SCALAR' && ${$val} eq '-') {
179                     $newvalue->[$row] = $val;
180                 }
181             }
182         }
183     } else {
184         $newvalue = $modvalue;
185     }
186     return $newvalue;
187 }
188
189
190 =head1 SYNOPSIS
191
192 The Catalyst::Authentication::Realm::Adaptor allows for modification of
193 authentication parameters within the catalyst application. It's basically a
194 filter used to adjust authentication parameters globally within the
195 application or to adjust user retrieval parameters provided by the credential
196 in order to be compatible with a different store. It provides for better
197 control over interaction between credentials and stores. This is particularly
198 useful when working with external authentication such as OpenID or OAuth.
199
200  __PACKAGE__->config(
201     'Plugin::Authentication' => {
202             'default' => {
203                 class => 'Adaptor'
204                 credential => {
205                     class => 'Password',
206                     password_field => 'secret',
207                     password_type  => 'hashed',
208                     password_hash_type => 'SHA-1',
209                 },
210                 store => {
211                     class      => 'DBIx::Class',
212                     user_class => 'Schema::Person',
213                 },
214                 store_adaptor => {
215                     method => 'merge_hash',
216                     merge_hash => {
217                         status => [ 'temporary', 'active' ]
218                     }
219                 }
220             },
221         }
222     }
223  );
224
225
226 The above example ensures that no matter how $c->authenticate() is called
227 within your application, the key 'status' is added to the authentication hash.
228 This allows you to, among other things, set parameters that should always be
229 applied to your authentication process or modify the parameters to better
230 connect a credential and a store that were not built to work together. In the
231 above example, we are making sure that the user search is restricted to those
232 with a status of either 'temporary' or 'active.'
233
234 This realm works by intercepting the original authentication information
235 between the time C<< $c->authenticate($authinfo) >> is called and the time the
236 realm's C<< $realm->authenticate($c,$authinfo) >> method is called, allowing for
237 the $authinfo parameter to be modified or replaced as your application
238 requires. It can also operate after the call to the credential's
239 C<authenticate()> method but before the call to the store's C<find_user>
240 method.
241
242 If you don't know what the above means, you probably do not need this module.
243
244 =head1 CONFIGURATION
245
246 The configuration for this module goes within your realm configuration alongside your
247 credential and store options.
248
249 This module can operate in two points during authentication processing.
250 The first is prior the realm's C<authenticate> call (immediately after the call to
251 C<< $c->authenticate() >>.) To operate here, your filter options should go in a hash
252 under the key C<credential_adaptor>.
253
254 The second point is after the call to credential's C<authenticate> method but
255 immediately before the call to the user store's C<find_user> method. To operate
256 prior to C<find_user>, your filter options should go in a hash under the key
257 C<store_adaptor>.
258
259 The filtering options for both points are the same, and both the C<store_adaptor> and
260 C<credential_adaptor> can be used simultaneously in a single realm.
261
262 =head2 method
263
264 There are four ways to configure your filters.  You specify which one you want by setting
265 the C<method> configuration option to one of the following: C<merge_hash>, C<new_hash>,
266 C<code>, or C<action>.  You then provide the additional information based on which method
267 you have chosen.  The different options are described below.
268
269 =over 8
270
271 =item merge_hash
272
273  credential_adaptor => {
274      method => 'merge_hash',
275      merge_hash => {
276          status => [ 'temporary', 'active' ]
277      }
278  }
279
280 This causes the original authinfo hash to be merged with a hash provided by
281 the realm configuration under the key C<merge_hash> key. This is a deep merge
282 and in the case of a conflict, the hash specified by merge_hash takes
283 precedence over what was passed into the authenticate or find_user call. The
284 method of merging is described in detail in the L<HASH MERGING> section below.
285
286 =item new_hash
287
288  store_adaptor => {
289      method => 'new_hash',
290      new_hash => {
291          username => '+(user)',  # this sets username to the value of $originalhash{user}
292          user_source => 'openid'
293      }
294  }
295
296 This causes the original authinfo hash to be set aside and replaced with a new hash provided under the
297 C<new_hash> key. The new hash can grab portions of the original hash.  This can be used to remap the authinfo
298 into a new format.  See the L<HASH MERGING> section for information on how to do this.
299
300 =item code
301
302  store_adaptor => {
303      method => 'code',
304      code => sub {
305          my ($realmname, $original_authinfo, $hashref_to_config ) = @_;
306          my $newauthinfo = {};
307          ## do something
308          return $newauthinfo;
309      }
310  }
311
312 The C<code> method allows for more complex filtering by executing code
313 provided as a subroutine reference in the C<code> key. The realm name,
314 original auth info and the portion of the config specific to this filter are
315 passed as arguments to the provided subroutine. In the above example, it would
316 be the entire store_adaptor hash. If you were using a code ref in a
317 credential_adaptor, you'd get the credential_adapter config instead.
318
319 =item action
320
321  credential_adaptor => {
322      method => 'action',
323      controller => 'UserProcessing',
324      action => 'FilterCredentials'
325  }
326
327 The C<action> method causes the adaptor to delegate filtering to a Catalyst
328 action. This is similar to the code ref above, except that instead of simply
329 calling the routine, the action specified is called via C<<$c->forward>>. The
330 arguments passed to the action are the same as the code method as well,
331 namely the realm name, the original authinfo hash and the config for the adaptor.
332
333 =back
334
335 =head1 HASH MERGING
336
337 The hash merging mechanism in Catalyst::Authentication::Realm::Adaptor is not
338 a simple merge of two hashes. It has some niceties which allow for both
339 re-mapping of existing keys, and a mechanism for removing keys from the
340 original hash. When using the 'merge_hash' method above, the keys from the
341 original hash and the keys for the merge hash are simply combined with the
342 merge_hash taking precedence in the case of a key conflict. If there are
343 sub-hashes they are merged as well.
344
345 If both the source and merge hash contain an array for a given hash-key, the
346 values in the merge array are appended to the original array.  Note that hashes
347 within arrays will not be merged, and will instead simply be copied.
348
349 Simple values are left intact, and in the case of a key existing in both
350 hashes, the value from the merge_hash takes precedence. Note that in the case
351 of a key conflict where the values are of different types, the value from the
352 merge_hash will be used and no attempt is made to merge or otherwise convert
353 them.
354
355 =head2 Advanced merging
356
357 Whether you are using C<merge_hash> or C<new_hash> as the method, you have access
358 to the values from the original authinfo hash.  In your new or merged hash, you
359 can use values from anywhere within the original hash.  You do this by setting
360 the value for the key you want to set to a special string indicating the key
361 path in the original hash.  The string is formatted as follows:
362 C<<'+(key1.key2.key3)'>>  This will grab the hash associated with key1, retrieve the hash
363 associated with key2, and finally obtain the value associated with key3.  This is easier to
364 show than to explain:
365
366  my $originalhash = {
367                         user => {
368                                 details => {
369                                     age       => 27,
370                                     haircolor => 'black',
371                                     favoritenumbers => [ 17, 42, 19 ]
372                                 }
373                         }
374                     };
375
376   my $newhash = {
377                     # would result in a value of 'black'
378                     haircolor => '+(user.details.haircolor)',
379
380                     # bestnumber would be 42.
381                     bestnumber => '+(user.details.favoritenumbers.1)'
382                 }
383
384 Given the example above, the value for the userage key would be 27, (obtained
385 via C<<'+(user.details.age)'>>) and the value for bestnumber would be 42. Note
386 that you can traverse both hashes and arrays using this method. This can be
387 quite useful when you need the values that were passed in, but you need to put
388 them under different keys.
389
390 When using the C<merge_hash> method, you sometimes may want to remove an item
391 from the original hash. You can do this by providing a key in your merge_hash
392 at the same point, but setting it's value to '-()'.  This will remove the key
393 entirely from the resultant hash.  This works better than simply setting the
394 value to undef in some cases.
395
396 =head1 NOTES and CAVEATS
397
398 The authentication system for Catalyst is quite flexible.  In most cases this
399 module is not needed.  Evidence of this fact is that the Catalyst auth system
400 was substantially unchanged for 2+ years prior to this modules first release.
401 If you are looking at this module, then there is a good chance your problem would
402 be better solved by adjusting your credential or store directly.
403
404 That said, there are some areas where this module can be particularly useful.
405 For example, this module allows for global application of additional arguments
406 to authinfo for a certain realm via your config.  It also allows for preliminary
407 testing of alternate configs before you adjust every C<< $c->authenticate() >> call
408 within your application.
409
410 It is also useful when combined with the various external authentication
411 modules available, such as OpenID, OAuth or Facebook. These modules expect to
412 store their user information in the Hash provided by the Minimal user store.
413 Often, however, you want to store user information locally in a database or
414 other storage mechanism. Doing this lies somewhere between difficult and
415 impossible normally. With the Adapter realm, you can massage the authinfo hash
416 between the credential's verification and the creation of the local user, and
417 instead use the information returned to look up a user instead.
418
419 Using the external auth mechanisms and the C<action> method, you can actually
420 trigger an action to create a user record on the fly when the user has
421 authenticated via an external method.  These are just some of the possibilities
422 that Adaptor provides that would otherwise be very difficult to accomplish,
423 even with Catalyst's flexible authentication system.
424
425 With all of that said, caution is warranted when using this module.  It modifies
426 the behavior of the application in ways that are not obvious and can therefore
427 lead to extremely hard to track-down bugs.  This is especially true when using
428 the C<action> filter method.  When a developer calls C<< $c->authenticate() >>
429 they are not expecting any actions to be called before it returns.
430
431 If you use the C<action> method, I strongly recommend that you use it only as a
432 filter routine and do not do other catalyst dispatch related activities (such as
433 further forwards, detach's or redirects).  Also note that it is B<EXTREMELY
434 DANGEROUS> to call authentication routines from within a filter action.  It is
435 extremely easy to accidentally create an infinite recursion bug which can crash
436 your Application.  In short - B<DON'T DO IT>.
437
438 =head1 AUTHOR
439
440 Jay Kuri, C<< <jayk at cpan.org> >>
441
442 =head1 BUGS
443
444 Please report any bugs or feature requests to C<bug-catalyst-authentication-realm-adaptor at rt.cpan.org>, or through
445 the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Catalyst-Authentication-Realm-Adaptor>.  I will be notified, and then you'll
446 automatically be notified of progress on your bug as I make changes.
447
448
449 =head1 SUPPORT
450
451 You can find documentation for this module with the perldoc command.
452
453     perldoc Catalyst::Authentication::Realm::Adaptor
454
455 You can also look for information at:
456
457 =over 4
458
459 =item * Search CPAN
460
461 L<http://search.cpan.org/dist/Catalyst-Authentication-Realm-Adaptor/>
462
463 =item * Catalyzed.org Wiki
464
465 L<http://wiki.catalyzed.org/cpan-modules/Catalyst-Authentication-Realm-Adaptor>
466
467 =back
468
469
470 =head1 ACKNOWLEDGEMENTS
471
472
473 =head1 COPYRIGHT & LICENSE
474
475 Copyright 2009 Jay Kuri, all rights reserved.
476
477 This program is free software; you can redistribute it and/or modify it
478 under the same terms as Perl itself.
479
480
481 =cut
482
483 1; # End of Catalyst::Authentication::Realm::Adaptor