added role self_check and self_check_any to User store
[catagits/Catalyst-Authentication-Store-DBIx-Class.git] / lib / Catalyst / Authentication / Store / DBIx / Class / User.pm
1 package Catalyst::Authentication::Store::DBIx::Class::User;
2
3 use Moose;
4 use namespace::autoclean;
5 extends 'Catalyst::Authentication::User';
6
7 use List::MoreUtils 'all';
8 use Try::Tiny;
9
10 has 'config'    => (is => 'rw');
11 has 'resultset' => (is => 'rw');
12 has '_user'     => (is => 'rw');
13 has '_roles'    => (is => 'rw');
14
15 sub new {
16     my ( $class, $config, $c) = @_;
17
18         $config->{user_model} = $config->{user_class}
19         unless defined $config->{user_model};
20
21     my $self = {
22         resultset => $c->model($config->{'user_model'}),
23         config => $config,
24         _roles => undef,
25         _user => undef
26     };
27
28     bless $self, $class;
29
30     Catalyst::Exception->throw(
31         "\$c->model('${ \$self->config->{user_model} }') did not return a resultset."
32           . " Did you set user_model correctly?"
33     ) unless $self->{resultset};
34
35     $self->config->{'id_field'} = [$self->{'resultset'}->result_source->primary_columns]
36         unless exists $self->config->{'id_field'};
37
38     $self->config->{'id_field'} = [$self->config->{'id_field'}]
39         unless ref $self->config->{'id_field'} eq 'ARRAY';
40
41     Catalyst::Exception->throw(
42         "id_field set to "
43           . join(q{,} => @{ $self->config->{'id_field'} })
44           . " but user table has no column by that name!"
45     ) unless all { $self->{'resultset'}->result_source->has_column($_) } @{ $self->config->{'id_field'} };
46
47     ## if we have lazyloading turned on - we should not query the DB unless something gets read.
48     ## that's the idea anyway - still have to work out how to manage that - so for now we always force
49     ## lazyload to off.
50     $self->config->{lazyload} = 0;
51
52 #    if (!$self->config->{lazyload}) {
53 #        return $self->load_user($authinfo, $c);
54 #    } else {
55 #        ## what do we do with a lazyload?
56 #        ## presumably this is coming out of session storage.
57 #        ## use $authinfo to fill in the user in that case?
58 #    }
59
60     return $self;
61 }
62
63
64 sub load {
65     my ($self, $authinfo, $c) = @_;
66
67     my $dbix_class_config = 0;
68
69     if (exists($authinfo->{'dbix_class'})) {
70         $authinfo = $authinfo->{'dbix_class'};
71         $dbix_class_config = 1;
72     }
73
74     ## User can provide an arrayref containing the arguments to search on the user class.
75     ## or even provide a prepared resultset, allowing maximum flexibility for user retrieval.
76     ## these options are only available when using the dbix_class authinfo hash.
77     if ($dbix_class_config && exists($authinfo->{'result'})) {
78         $self->_user($authinfo->{'result'});
79     } elsif ($dbix_class_config && exists($authinfo->{'resultset'})) {
80         $self->_user($authinfo->{'resultset'}->first);
81     } elsif ($dbix_class_config && exists($authinfo->{'searchargs'})) {
82         $self->_user($self->resultset->search(@{$authinfo->{'searchargs'}})->first);
83     } else {
84         ## merge the ignore fields array into a hash - so we can do an easy check while building the query
85         my %ignorefields = map { $_ => 1} @{$self->config->{'ignore_fields_in_find'}};
86         my $searchargs = {};
87
88         # now we walk all the fields passed in, and build up a search hash.
89         foreach my $key (grep {!$ignorefields{$_}} keys %{$authinfo}) {
90             if ($self->resultset->result_source->has_column($key)) {
91                 $searchargs->{$key} = $authinfo->{$key};
92             }
93         }
94         if (keys %{$searchargs}) {
95             $self->_user($self->resultset->search($searchargs)->first);
96         } else {
97             Catalyst::Exception->throw(
98                 "Failed to load user data.  You passed [" . join(',', keys %{$authinfo}) . "]"
99                   . " to authenticate() but your user source (" .  $self->config->{'user_model'} . ")"
100                   . " only has these columns: [" . join( ",", $self->resultset->result_source->columns ) . "]"
101                   . "   Check your authenticate() call."
102             );
103         }
104     }
105
106     if ($self->get_object) {
107         return $self;
108     } else {
109         return undef;
110     }
111
112 }
113
114 sub supported_features {
115     my $self = shift;
116
117     return {
118         session         => 1,
119         roles           => {
120             self_check      => $self->config->{check_roles} || 0,,
121             self_check_any  => $self->config->{check_roles_any} || 0,
122         },
123     };
124 }
125
126 #will only be used if $config->{check_roles} is set
127 sub check_roles {
128     my ( $self, @wanted_roles ) = @_;
129
130     my @roles = $self->roles;
131     my $name = $self->config->{check_roles};
132
133     return $self->_user->$name( \@roles, \@wanted_roles );
134 }
135
136 #will only be used if $config->{check_roles_any} is set
137 sub check_roles_any {
138     my ( $self, @wanted_roles ) = @_;
139
140     my @roles = $self->roles;
141     my $name = $self->config->{check_roles_any};
142
143     return $self->_user->$name( \@roles, \@wanted_roles );
144 }
145
146 sub roles {
147     my ( $self ) = shift;
148     ## this used to load @wantedroles - but that doesn't seem to be used by the roles plugin, so I dropped it.
149
150     ## shortcut if we have already retrieved them
151     if (ref $self->_roles eq 'ARRAY') {
152         return(@{$self->_roles});
153     }
154
155     my @roles = ();
156     if (exists($self->config->{'role_column'})) {
157         my $role_data = $self->get($self->config->{'role_column'});
158         if ($role_data) {
159             @roles = split /[\s,\|]+/, $self->get($self->config->{'role_column'});
160         }
161         $self->_roles(\@roles);
162     } elsif (exists($self->config->{'role_relation'})) {
163         my $relation = $self->config->{'role_relation'};
164         if ($self->_user->$relation->result_source->has_column($self->config->{'role_field'})) {
165             @roles = map {
166                 $_->get_column($self->config->{role_field})
167             } $self->_user->$relation->search(undef, {
168                 columns => [ $self->config->{role_field} ]
169             })->all;
170             $self->_roles(\@roles);
171         } else {
172             Catalyst::Exception->throw("role table does not have a column called " . $self->config->{'role_field'});
173         }
174     } else {
175         Catalyst::Exception->throw("user->roles accessed, but no role configuration found");
176     }
177
178     return @{$self->_roles};
179 }
180
181 sub for_session {
182     my $self = shift;
183
184     #return $self->get($self->config->{'id_field'});
185
186     #my $frozenuser = $self->_user->result_source->schema->freeze( $self->_user );
187     #return $frozenuser;
188
189     my %userdata = $self->_user->get_columns();
190
191     # If use_userdata_from_session is set, then store all of the columns of the user obj in the session
192     if (exists($self->config->{'use_userdata_from_session'}) && $self->config->{'use_userdata_from_session'} != 0) {
193         return \%userdata;
194     } else { # Otherwise, we just need the PKs for load() to use.
195         my %pk_fields = map { ($_ => $userdata{$_}) } @{ $self->config->{id_field} };
196         return \%pk_fields;
197     }
198 }
199
200 sub from_session {
201     my ($self, $frozenuser, $c) = @_;
202
203     #my $obj = $self->resultset->result_source->schema->thaw( $frozenuser );
204     #$self->_user($obj);
205
206     #if (!exists($self->config->{'use_userdata_from_session'}) || $self->config->{'use_userdata_from_session'} == 0) {
207 #        $self->_user->discard_changes();
208 #    }
209 #
210 #    return $self;
211 #
212 ## if use_userdata_from_session is defined in the config, we fill in the user data from the session.
213     if (exists($self->config->{'use_userdata_from_session'}) && $self->config->{'use_userdata_from_session'} != 0) {
214
215         # We need to use inflate_result here since we -are- inflating a
216         # result object from cached data, not creating a fresh one.
217         # Components such as EncodedColumn wrap new() to ensure that a
218         # provided password is hashed on the way in, and re-running the
219         # hash function on data being restored is expensive and incorrect.
220
221         my $class = $self->resultset->result_class;
222         my $source = $self->resultset->result_source;
223         my $obj = $class->inflate_result($source, { %$frozenuser });
224
225         $obj->in_storage(1);
226         $self->_user($obj);
227         return $self;
228     }
229
230     if (ref $frozenuser eq 'HASH') {
231         return $self->load({
232             map { ($_ => $frozenuser->{$_}) }
233             @{ $self->config->{id_field} }
234         }, $c);
235     }
236
237     return $self->load( { $self->config->{'id_field'} => $frozenuser }, $c);
238 }
239
240 sub get {
241     my ($self, $field) = @_;
242
243     if (my $code = $self->_user->can($field)) {
244         return $self->_user->$code;
245     }
246     elsif (my $accessor =
247          try { $self->_user->result_source->column_info($field)->{accessor} }) {
248         return $self->_user->$accessor;
249     } else {
250         # XXX this should probably throw
251         return undef;
252     }
253 }
254
255 sub get_object {
256     my ($self, $force) = @_;
257
258     if ($force) {
259         $self->_user->discard_changes;
260     }
261
262     return $self->_user;
263 }
264
265 sub obj {
266     my ($self, $force) = @_;
267
268     return $self->get_object($force);
269 }
270
271 sub auto_create {
272     my $self = shift;
273     $self->_user( $self->resultset->auto_create( @_ ) );
274     return $self;
275 }
276
277 sub auto_update {
278     my $self = shift;
279     $self->_user->auto_update( @_ );
280 }
281
282 sub can {
283     my $self = shift;
284     return $self->SUPER::can(@_) || do {
285         my ($method) = @_;
286         if (not ref $self) {
287             undef;
288         } elsif (not $self->_user) {
289             undef;
290         } elsif (my $code = $self->_user->can($method)) {
291             sub { shift->_user->$code(@_) }
292         } elsif (my $accessor =
293             try { $self->_user->result_source->column_info($method)->{accessor} }) {
294             sub { shift->_user->$accessor }
295         } else {
296             undef;
297         }
298     };
299 }
300
301 sub AUTOLOAD {
302     my $self = shift;
303     (my $method) = (our $AUTOLOAD =~ /([^:]+)$/);
304     return if $method eq "DESTROY";
305
306     return unless ref $self;
307
308     if (my $code = $self->_user->can($method)) {
309         return $self->_user->$code(@_);
310     }
311     elsif (my $accessor =
312          try { $self->_user->result_source->column_info($method)->{accessor} }) {
313         return $self->_user->$accessor(@_);
314     } else {
315         # XXX this should also throw
316         return undef;
317     }
318 }
319
320 __PACKAGE__->meta->make_immutable(inline_constructor => 0);
321
322 1;
323 __END__
324
325 =head1 NAME
326
327 Catalyst::Authentication::Store::DBIx::Class::User - The backing user
328 class for the Catalyst::Authentication::Store::DBIx::Class storage
329 module.
330
331 =head1 VERSION
332
333 This documentation refers to version 0.1503.
334
335 =head1 SYNOPSIS
336
337 Internal - not used directly, please see
338 L<Catalyst::Authentication::Store::DBIx::Class> for details on how to
339 use this module. If you need more information than is present there, read the
340 source.
341
342
343
344 =head1 DESCRIPTION
345
346 The Catalyst::Authentication::Store::DBIx::Class::User class implements user storage
347 connected to an underlying DBIx::Class schema object.
348
349 =head1 SUBROUTINES / METHODS
350
351 =head2 new
352
353 Constructor.
354
355 =head2 load ( $authinfo, $c )
356
357 Retrieves a user from storage using the information provided in $authinfo.
358
359 =head2 supported_features
360
361 Indicates the features supported by this class.  These are currently Roles and Session.
362
363 =head2 roles
364
365 Returns an array of roles associated with this user, if roles are configured for this user class.
366
367 =head2 for_session
368
369 Returns a serialized user for storage in the session.
370
371 =head2 from_session
372
373 Revives a serialized user from storage in the session.
374
375 =head2 get ( $fieldname )
376
377 Returns the value of $fieldname for the user in question.  Roughly translates to a call to
378 the DBIx::Class::Row's get_column( $fieldname ) routine.
379
380 =head2 get_object
381
382 Retrieves the DBIx::Class object that corresponds to this user
383
384 =head2 obj (method)
385
386 Synonym for get_object
387
388 =head2 auto_create
389
390 This is called when the auto_create_user option is turned on in
391 Catalyst::Plugin::Authentication and a user matching the authinfo provided is not found.
392 By default, this will call the C<auto_create()> method of the resultset associated
393 with this object. It is up to you to implement that method.
394
395 =head2 auto_update
396
397 This is called when the auto_update_user option is turned on in
398 Catalyst::Plugin::Authentication. Note that by default the DBIx::Class store
399 uses every field in the authinfo hash to match the user. This means any
400 information you provide with the intent to update must be ignored during the
401 user search process. Otherwise the information will most likely cause the user
402 record to not be found. To ignore fields in the search process, you
403 have to add the fields you wish to update to the 'ignore_fields_in_find'
404 authinfo element.  Alternately, you can use one of the advanced row retrieval
405 methods (searchargs or resultset).
406
407 By default, auto_update will call the C<auto_update()> method of the
408 DBIx::Class::Row object associated with the user. It is up to you to implement
409 that method (probably in your schema file)
410
411 =head2 AUTOLOAD
412
413 Delegates method calls to the underlying user row.
414
415 =head2 can
416
417 Delegates handling of the C<< can >> method to the underlying user row.
418
419 =head2 check_roles
420
421 Calls the specified check_roles method on the underlying user row.
422
423 Passes \@roles, \@wanted_roles, where @roles is the list of roles,
424 and @wanted_roles is the list of wanted roles
425
426 =head2 check_roles_any
427
428 Calls the specified check_roles_any method on the underlying user row.
429
430 Passes \@roles, \@wanted_roles, where @roles is the list of roles,
431 and @wanted_roles is the list of wanted roles
432
433 =head1 BUGS AND LIMITATIONS
434
435 None known currently, please email the author if you find any.
436
437 =head1 AUTHOR
438
439 Jason Kuri (jayk@cpan.org)
440
441 =head1 CONTRIBUTORS
442
443 Matt S Trout (mst) <mst@shadowcat.co.uk>
444
445 (fixes wrt can/AUTOLOAD sponsored by L<http://reask.com/>)
446
447 =head1 LICENSE
448
449 Copyright (c) 2007-2010 the aforementioned authors. All rights
450 reserved. This program is free software; you can redistribute
451 it and/or modify it under the same terms as Perl itself.
452
453 =cut