File Coverage

blib/lib/Catalyst/Authentication/Realm/Adaptor.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Catalyst::Authentication::Realm::Adaptor;
2              
3 2     2   326010 use warnings;
  2         5  
  2         69  
4 2     2   11 use strict;
  2         6  
  2         66  
5 2     2   12 use Carp;
  2         9  
  2         165  
6 2     2   1660 use Moose;
  0            
  0            
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.02
16              
17             =cut
18              
19             ## goes in catagits@jules.scsys.co.uk:Catalyst-Authentication-Realm-Adaptor.git
20              
21             our $VERSION = '0.02';
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