Commit | Line | Data |
c6a2d572 |
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; |
924b3b67 |
27 | |
c6a2d572 |
28 | if (exists($self->config->{'credential_adaptor'})) { |
924b3b67 |
29 | |
c6a2d572 |
30 | if ($self->config->{'credential_adaptor'}{'method'} eq 'merge_hash') { |
924b3b67 |
31 | |
c6a2d572 |
32 | $newauthinfo = _munge_hash($authinfo, $self->config->{'credential_adaptor'}{'merge_hash'}, $authinfo); |
924b3b67 |
33 | |
c6a2d572 |
34 | } elsif ($self->config->{'credential_adaptor'}{'method'} eq 'new_hash') { |
924b3b67 |
35 | |
c6a2d572 |
36 | $newauthinfo = _munge_hash({}, $self->config->{'credential_adaptor'}{'new_hash'}, $authinfo); |
924b3b67 |
37 | |
c6a2d572 |
38 | } elsif ($self->config->{'credential_adaptor'}{'method'} eq 'action') { |
924b3b67 |
39 | |
c6a2d572 |
40 | my $controller = $c->controller($self->config->{'credential_adaptor'}{'controller'}); |
41 | if (!$controller) { |
924b3b67 |
42 | Catalyst::Exception->throw(__PACKAGE__ . " realm: " . $self->name . "'s credential_adaptor tried to use a controller that doesn't exist: " . |
c6a2d572 |
43 | $self->config->{'credential_adaptor'}{'controller'}); |
924b3b67 |
44 | } |
45 | |
c6a2d572 |
46 | my $action = $controller->action_for($self->config->{'credential_adaptor'}{'action'}); |
47 | if (!$action) { |
924b3b67 |
48 | Catalyst::Exception->throw(__PACKAGE__ . " realm: " . $self->name . "'s credential_adaptor tried to use an action that doesn't exist: " . |
c6a2d572 |
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'}); |
924b3b67 |
53 | |
c6a2d572 |
54 | } elsif ($self->config->{'credential_adaptor'}{'method'} eq 'code' ) { |
924b3b67 |
55 | |
c6a2d572 |
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; |
924b3b67 |
73 | |
c6a2d572 |
74 | if (exists($self->config->{'store_adaptor'})) { |
924b3b67 |
75 | |
c6a2d572 |
76 | if ($self->config->{'store_adaptor'}{'method'} eq 'merge_hash') { |
924b3b67 |
77 | |
c6a2d572 |
78 | $newauthinfo = _munge_hash($authinfo, $self->config->{'store_adaptor'}{'merge_hash'}, $authinfo); |
924b3b67 |
79 | |
c6a2d572 |
80 | } elsif ($self->config->{'store_adaptor'}{'method'} eq 'new_hash') { |
924b3b67 |
81 | |
c6a2d572 |
82 | $newauthinfo = _munge_hash({}, $self->config->{'store_adaptor'}{'new_hash'}, $authinfo); |
924b3b67 |
83 | |
c6a2d572 |
84 | } elsif ($self->config->{'store_adaptor'}{'method'} eq 'action') { |
924b3b67 |
85 | |
c6a2d572 |
86 | my $controller = $c->controller($self->config->{'store_adaptor'}{'controller'}); |
87 | if (!$controller) { |
924b3b67 |
88 | Catalyst::Exception->throw(__PACKAGE__ . " realm: " . $self->name . "'s store_adaptor tried to use a controller that doesn't exist: " . |
c6a2d572 |
89 | $self->config->{'store_adaptor'}{'controller'}); |
924b3b67 |
90 | } |
91 | |
c6a2d572 |
92 | my $action = $controller->action_for($self->config->{'store_adaptor'}{'action'}); |
93 | if (!$action) { |
924b3b67 |
94 | Catalyst::Exception->throw(__PACKAGE__ . " realm: " . $self->name . "'s store_adaptor tried to use an action that doesn't exist: " . |
c6a2d572 |
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'}); |
924b3b67 |
99 | |
c6a2d572 |
100 | } elsif ($self->config->{'store_adaptor'}{'method'} eq 'code' ) { |
924b3b67 |
101 | |
c6a2d572 |
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 | } |
b6ee801b |
109 | return $self->SUPER::find_user($newauthinfo, $c); |
c6a2d572 |
110 | } else { |
b6ee801b |
111 | return $self->SUPER::find_user($authinfo, $c); |
c6a2d572 |
112 | } |
113 | } |
114 | |
115 | sub _munge_hash { |
116 | my ($sourcehash, $modhash, $referencehash) = @_; |
924b3b67 |
117 | |
c6a2d572 |
118 | my $resulthash = { %{$sourcehash} }; |
924b3b67 |
119 | |
c6a2d572 |
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 | } |
924b3b67 |
131 | $resulthash->{$key} = _munge_value($modhash->{$key}, $referencehash); |
c6a2d572 |
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) = @_; |
924b3b67 |
143 | |
c6a2d572 |
144 | my $newvalue; |
145 | if ($modvalue =~ m/^([+-])\((.*)\)$/) { |
146 | my $action = $1; |
147 | my $keypath = $2; |
924b3b67 |
148 | ## do magic |
c6a2d572 |
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 '-') { |
924b3b67 |
179 | $newvalue->[$row] = $val; |
c6a2d572 |
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 |
d0102990 |
236 | realm's C<< $realm->authenticate($c,$authinfo) >> method is called, allowing for |
c6a2d572 |
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 |
924b3b67 |
247 | credential and store options. |
c6a2d572 |
248 | |
924b3b67 |
249 | This module can operate in two points during authentication processing. |
c6a2d572 |
250 | The first is prior the realm's C<authenticate> call (immediately after the call to |
d0102990 |
251 | C<< $c->authenticate() >>.) To operate here, your filter options should go in a hash |
c6a2d572 |
252 | under the key C<credential_adaptor>. |
253 | |
254 | The second point is after the call to credential's C<authenticate> method but |
924b3b67 |
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 |
c6a2d572 |
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 | |
924b3b67 |
264 | There are four ways to configure your filters. You specify which one you want by setting |
c6a2d572 |
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 | } |
924b3b67 |
279 | |
c6a2d572 |
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} |
924b3b67 |
292 | user_source => 'openid' |
c6a2d572 |
293 | } |
294 | } |
924b3b67 |
295 | |
296 | This causes the original authinfo hash to be set aside and replaced with a new hash provided under the |
c6a2d572 |
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 |
924b3b67 |
308 | return $newauthinfo; |
c6a2d572 |
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 | } |
924b3b67 |
326 | |
c6a2d572 |
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 |
924b3b67 |
330 | arguments passed to the action are the same as the code method as well, |
c6a2d572 |
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 |
924b3b67 |
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. |
c6a2d572 |
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 |
924b3b67 |
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 |
c6a2d572 |
360 | the value for the key you want to set to a special string indicating the key |
924b3b67 |
361 | path in the original hash. The string is formatted as follows: |
c6a2d572 |
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 | |
924b3b67 |
366 | my $originalhash = { |
c6a2d572 |
367 | user => { |
368 | details => { |
369 | age => 27, |
370 | haircolor => 'black', |
371 | favoritenumbers => [ 17, 42, 19 ] |
372 | } |
373 | } |
374 | }; |
924b3b67 |
375 | |
376 | my $newhash = { |
c6a2d572 |
377 | # would result in a value of 'black' |
924b3b67 |
378 | haircolor => '+(user.details.haircolor)', |
379 | |
c6a2d572 |
380 | # bestnumber would be 42. |
924b3b67 |
381 | bestnumber => '+(user.details.favoritenumbers.1)' |
c6a2d572 |
382 | } |
924b3b67 |
383 | |
c6a2d572 |
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 |
924b3b67 |
392 | at the same point, but setting it's value to '-()'. This will remove the key |
c6a2d572 |
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 | |
924b3b67 |
398 | The authentication system for Catalyst is quite flexible. In most cases this |
c6a2d572 |
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 |
924b3b67 |
402 | be better solved by adjusting your credential or store directly. |
c6a2d572 |
403 | |
924b3b67 |
404 | That said, there are some areas where this module can be particularly useful. |
c6a2d572 |
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 |
924b3b67 |
407 | testing of alternate configs before you adjust every C<< $c->authenticate() >> call |
c6a2d572 |
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 | |
924b3b67 |
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 |
c6a2d572 |
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 |
924b3b67 |
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. |
c6a2d572 |
430 | |
431 | If you use the C<action> method, I strongly recommend that you use it only as a |
924b3b67 |
432 | filter routine and do not do other catalyst dispatch related activities (such as |
c6a2d572 |
433 | further forwards, detach's or redirects). Also note that it is B<EXTREMELY |
924b3b67 |
434 | DANGEROUS> to call authentication routines from within a filter action. It is |
c6a2d572 |
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 |