Initial commit of Catalyst::Authentication::Realm::Adaptor
[catagits/Catalyst-Authentication-Realm-Adaptor.git] / lib / Catalyst / Authentication / Realm / Adaptor.pm
CommitLineData
c6a2d572 1package Catalyst::Authentication::Realm::Adaptor;
2
3use warnings;
4use strict;
5use Carp;
6use Moose;
7extends 'Catalyst::Authentication::Realm';
8
9=head1 NAME
10
11Catalyst::Authentication::Realm::Adaptor - Adjust parameters of authentication processes on the fly
12
13=head1 VERSION
14
15Version 0.01
16
17=cut
18
19## goes in catagits@jules.scsys.co.uk:Catalyst-Authentication-Realm-Adaptor.git
20
21our $VERSION = '0.01';
22
23sub 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
69sub 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::authenticate($c, $newauthinfo);
110 } else {
111 return $self->SUPER::authenticate($c, $authinfo);
112 }
113}
114
115sub _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
141sub _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
192The Catalyst::Authentication::Realm::Adaptor allows for modification of
193authentication parameters within the catalyst application. It's basically a
194filter used to adjust authentication parameters globally within the
195application or to adjust user retrieval parameters provided by the credential
196in order to be compatible with a different store. It provides for better
197control over interaction between credentials and stores. This is particularly
198useful 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
226The above example ensures that no matter how $c->authenticate() is called
227within your application, the key 'status' is added to the authentication hash.
228This allows you to, among other things, set parameters that should always be
229applied to your authentication process or modify the parameters to better
230connect a credential and a store that were not built to work together. In the
231above example, we are making sure that the user search is restricted to those
232with a status of either 'temporary' or 'active.'
233
234This realm works by intercepting the original authentication information
235between the time C<< $c->authenticate($authinfo) >> is called and the time the
236realm's C<$realm->authenticate($c,$authinfo)> method is called, allowing for
237the $authinfo parameter to be modified or replaced as your application
238requires. It can also operate after the call to the credential's
239C<authenticate()> method but before the call to the store's C<find_user>
240method.
241
242If you don't know what the above means, you probably do not need this module.
243
244=head1 CONFIGURATION
245
246The configuration for this module goes within your realm configuration alongside your
247credential and store options.
248
249This module can operate in two points during authentication processing.
250The first is prior the realm's C<authenticate> call (immediately after the call to
251C<<$c->authenticate()>>.) To operate here, your filter options should go in a hash
252under the key C<credential_adaptor>.
253
254The second point is after the call to credential's C<authenticate> method but
255immediately before the call to the user store's C<find_user> method. To operate
256prior to C<find_user>, your filter options should go in a hash under the key
257C<store_adaptor>.
258
259The filtering options for both points are the same, and both the C<store_adaptor> and
260C<credential_adaptor> can be used simultaneously in a single realm.
261
262=head2 method
263
264There are four ways to configure your filters. You specify which one you want by setting
265the C<method> configuration option to one of the following: C<merge_hash>, C<new_hash>,
266C<code>, or C<action>. You then provide the additional information based on which method
267you 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
280This causes the original authinfo hash to be merged with a hash provided by
281the realm configuration under the key C<merge_hash> key. This is a deep merge
282and in the case of a conflict, the hash specified by merge_hash takes
283precedence over what was passed into the authenticate or find_user call. The
284method 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
296This causes the original authinfo hash to be set aside and replaced with a new hash provided under the
297C<new_hash> key. The new hash can grab portions of the original hash. This can be used to remap the authinfo
298into 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
312The C<code> method allows for more complex filtering by executing code
313provided as a subroutine reference in the C<code> key. The realm name,
314original auth info and the portion of the config specific to this filter are
315passed as arguments to the provided subroutine. In the above example, it would
316be the entire store_adaptor hash. If you were using a code ref in a
317credential_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
327The C<action> method causes the adaptor to delegate filtering to a Catalyst
328action. This is similar to the code ref above, except that instead of simply
329calling the routine, the action specified is called via C<<$c->forward>>. The
330arguments passed to the action are the same as the code method as well,
331namely the realm name, the original authinfo hash and the config for the adaptor.
332
333=back
334
335=head1 HASH MERGING
336
337The hash merging mechanism in Catalyst::Authentication::Realm::Adaptor is not
338a simple merge of two hashes. It has some niceties which allow for both
339re-mapping of existing keys, and a mechanism for removing keys from the
340original hash. When using the 'merge_hash' method above, the keys from the
341original hash and the keys for the merge hash are simply combined with the
342merge_hash taking precedence in the case of a key conflict. If there are
343sub-hashes they are merged as well.
344
345If both the source and merge hash contain an array for a given hash-key, the
346values in the merge array are appended to the original array. Note that hashes
347within arrays will not be merged, and will instead simply be copied.
348
349Simple values are left intact, and in the case of a key existing in both
350hashes, the value from the merge_hash takes precedence. Note that in the case
351of a key conflict where the values are of different types, the value from the
352merge_hash will be used and no attempt is made to merge or otherwise convert
353them.
354
355=head2 Advanced merging
356
357Whether you are using C<merge_hash> or C<new_hash> as the method, you have access
358to the values from the original authinfo hash. In your new or merged hash, you
359can use values from anywhere within the original hash. You do this by setting
360the value for the key you want to set to a special string indicating the key
361path in the original hash. The string is formatted as follows:
362C<<'+(key1.key2.key3)'>> This will grab the hash associated with key1, retrieve the hash
363associated with key2, and finally obtain the value associated with key3. This is easier to
364show 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
384Given the example above, the value for the userage key would be 27, (obtained
385via C<<'+(user.details.age)'>>) and the value for bestnumber would be 42. Note
386that you can traverse both hashes and arrays using this method. This can be
387quite useful when you need the values that were passed in, but you need to put
388them under different keys.
389
390When using the C<merge_hash> method, you sometimes may want to remove an item
391from the original hash. You can do this by providing a key in your merge_hash
392at the same point, but setting it's value to '-()'. This will remove the key
393entirely from the resultant hash. This works better than simply setting the
394value to undef in some cases.
395
396=head1 NOTES and CAVEATS
397
398The authentication system for Catalyst is quite flexible. In most cases this
399module is not needed. Evidence of this fact is that the Catalyst auth system
400was substantially unchanged for 2+ years prior to this modules first release.
401If you are looking at this module, then there is a good chance your problem would
402be better solved by adjusting your credential or store directly.
403
404That said, there are some areas where this module can be particularly useful.
405For example, this module allows for global application of additional arguments
406to authinfo for a certain realm via your config. It also allows for preliminary
407testing of alternate configs before you adjust every C<<$c->authenticate()>> call
408within your application.
409
410It is also useful when combined with the various external authentication
411modules available, such as OpenID, OAuth or Facebook. These modules expect to
412store their user information in the Hash provided by the Minimal user store.
413Often, however, you want to store user information locally in a database or
414other storage mechanism. Doing this lies somewhere between difficult and
415impossible normally. With the Adapter realm, you can massage the authinfo hash
416between the credential's verification and the creation of the local user, and
417instead use the information returned to look up a user instead.
418
419Using the external auth mechanisms and the C<action> method, you can actually
420trigger an action to create a user record on the fly when the user has
421authenticated via an external method. These are just some of the possibilities
422that Adaptor provides that would otherwise be very difficult to accomplish,
423even with Catalyst's flexible authentication system.
424
425With all of that said, caution is warranted when using this module. It modifies
426the behavior of the application in ways that are not obvious and can therefore
427lead to extremely hard to track-down bugs. This is especially true when using
428the C<action> filter method. When a developer calls C<<$c->authenticate()>>
429they are not expecting any actions to be called before it returns.
430
431If you use the C<action> method, I strongly recommend that you use it only as a
432filter routine and do not do other catalyst dispatch related activities (such as
433further forwards, detach's or redirects). Also note that it is B<EXTREMELY
434DANGEROUS> to call authentication routines from within a filter action. It is
435extremely easy to accidentally create an infinite recursion bug which can crash
436your Application. In short - B<DON'T DO IT>.
437
438=head1 AUTHOR
439
440Jay Kuri, C<< <jayk at cpan.org> >>
441
442=head1 BUGS
443
444Please report any bugs or feature requests to C<bug-catalyst-authentication-realm-adaptor at rt.cpan.org>, or through
445the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Catalyst-Authentication-Realm-Adaptor>. I will be notified, and then you'll
446automatically be notified of progress on your bug as I make changes.
447
448
449=head1 SUPPORT
450
451You can find documentation for this module with the perldoc command.
452
453 perldoc Catalyst::Authentication::Realm::Adaptor
454
455You can also look for information at:
456
457=over 4
458
459=item * Search CPAN
460
461L<http://search.cpan.org/dist/Catalyst-Authentication-Realm-Adaptor/>
462
463=item * Catalyzed.org Wiki
464
465L<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
475Copyright 2009 Jay Kuri, all rights reserved.
476
477This program is free software; you can redistribute it and/or modify it
478under the same terms as Perl itself.
479
480
481=cut
482
4831; # End of Catalyst::Authentication::Realm::Adaptor