Commit | Line | Data |
6727afe2 |
1 | package Catalyst::Authentication::Store::DBIx::Class::User; |
5000f545 |
2 | |
6d4bea88 |
3 | use Moose; |
4 | use namespace::autoclean; |
5 | extends 'Catalyst::Authentication::User'; |
6 | |
7 | use List::MoreUtils 'all'; |
51f40056 |
8 | use Try::Tiny; |
6d4bea88 |
9 | |
10 | has 'config' => (is => 'rw'); |
11 | has 'resultset' => (is => 'rw'); |
12 | has '_user' => (is => 'rw'); |
13 | has '_roles' => (is => 'rw'); |
5000f545 |
14 | |
15 | sub new { |
ff7203cb |
16 | my ( $class, $config, $c) = @_; |
5000f545 |
17 | |
9064f42f |
18 | $config->{user_model} = $config->{user_class} |
19 | unless defined $config->{user_model}; |
f55cb81e |
20 | |
ff7203cb |
21 | my $self = { |
f55cb81e |
22 | resultset => $c->model($config->{'user_model'}), |
ff7203cb |
23 | config => $config, |
078727e0 |
24 | _roles => undef, |
ff7203cb |
25 | _user => undef |
26 | }; |
4bebb973 |
27 | |
5000f545 |
28 | bless $self, $class; |
f55cb81e |
29 | |
9064f42f |
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}; |
f55cb81e |
34 | |
70462180 |
35 | $self->config->{'id_field'} = [$self->{'resultset'}->result_source->primary_columns] |
36 | unless exists $self->config->{'id_field'}; |
f55cb81e |
37 | |
70462180 |
38 | $self->config->{'id_field'} = [$self->config->{'id_field'}] |
39 | unless ref $self->config->{'id_field'} eq 'ARRAY'; |
40 | |
9064f42f |
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'} }; |
4bebb973 |
46 | |
5000f545 |
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. |
ff7203cb |
50 | $self->config->{lazyload} = 0; |
4bebb973 |
51 | |
ff7203cb |
52 | # if (!$self->config->{lazyload}) { |
53 | # return $self->load_user($authinfo, $c); |
54 | # } else { |
55 | # ## what do we do with a lazyload? |
4bebb973 |
56 | # ## presumably this is coming out of session storage. |
ff7203cb |
57 | # ## use $authinfo to fill in the user in that case? |
58 | # } |
59 | |
5000f545 |
60 | return $self; |
61 | } |
62 | |
63 | |
ff7203cb |
64 | sub load { |
5000f545 |
65 | my ($self, $authinfo, $c) = @_; |
4bebb973 |
66 | |
ff7203cb |
67 | my $dbix_class_config = 0; |
4bebb973 |
68 | |
ff7203cb |
69 | if (exists($authinfo->{'dbix_class'})) { |
70 | $authinfo = $authinfo->{'dbix_class'}; |
71 | $dbix_class_config = 1; |
72 | } |
4bebb973 |
73 | |
5000f545 |
74 | ## User can provide an arrayref containing the arguments to search on the user class. |
e2ecb860 |
75 | ## or even provide a prepared resultset, allowing maximum flexibility for user retrieval. |
4bebb973 |
76 | ## these options are only available when using the dbix_class authinfo hash. |
6bd97524 |
77 | if ($dbix_class_config && exists($authinfo->{'result'})) { |
78 | $self->_user($authinfo->{'result'}); |
79 | } elsif ($dbix_class_config && exists($authinfo->{'resultset'})) { |
ff7203cb |
80 | $self->_user($authinfo->{'resultset'}->first); |
81 | } elsif ($dbix_class_config && exists($authinfo->{'searchargs'})) { |
4bebb973 |
82 | $self->_user($self->resultset->search(@{$authinfo->{'searchargs'}})->first); |
5000f545 |
83 | } else { |
84 | ## merge the ignore fields array into a hash - so we can do an easy check while building the query |
4bebb973 |
85 | my %ignorefields = map { $_ => 1} @{$self->config->{'ignore_fields_in_find'}}; |
5000f545 |
86 | my $searchargs = {}; |
4bebb973 |
87 | |
5000f545 |
88 | # now we walk all the fields passed in, and build up a search hash. |
89 | foreach my $key (grep {!$ignorefields{$_}} keys %{$authinfo}) { |
ff7203cb |
90 | if ($self->resultset->result_source->has_column($key)) { |
5000f545 |
91 | $searchargs->{$key} = $authinfo->{$key}; |
92 | } |
ff7203cb |
93 | } |
87920e64 |
94 | if (keys %{$searchargs}) { |
95 | $self->_user($self->resultset->search($searchargs)->first); |
96 | } else { |
9064f42f |
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 | ); |
87920e64 |
103 | } |
ff7203cb |
104 | } |
105 | |
106 | if ($self->get_object) { |
93102ff5 |
107 | return $self; |
ff7203cb |
108 | } else { |
109 | return undef; |
5000f545 |
110 | } |
5000f545 |
111 | |
112 | } |
113 | |
114 | sub supported_features { |
115 | my $self = shift; |
5000f545 |
116 | |
117 | return { |
5000f545 |
118 | session => 1, |
b3c995e9 |
119 | roles => { |
120 | self_check => $self->config->{check_roles} || 0,, |
121 | self_check_any => $self->config->{check_roles_any} || 0, |
122 | }, |
5000f545 |
123 | }; |
124 | } |
125 | |
b3c995e9 |
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 | } |
5000f545 |
145 | |
146 | sub roles { |
b5c13b47 |
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. |
5000f545 |
149 | |
150 | ## shortcut if we have already retrieved them |
ff7203cb |
151 | if (ref $self->_roles eq 'ARRAY') { |
152 | return(@{$self->_roles}); |
5000f545 |
153 | } |
4bebb973 |
154 | |
5000f545 |
155 | my @roles = (); |
ff7203cb |
156 | if (exists($self->config->{'role_column'})) { |
ad93b3e9 |
157 | my $role_data = $self->get($self->config->{'role_column'}); |
4bebb973 |
158 | if ($role_data) { |
87920e64 |
159 | @roles = split /[\s,\|]+/, $self->get($self->config->{'role_column'}); |
ad93b3e9 |
160 | } |
078727e0 |
161 | $self->_roles(\@roles); |
ff7203cb |
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'})) { |
9064f42f |
165 | @roles = map { |
166 | $_->get_column($self->config->{role_field}) |
167 | } $self->_user->$relation->search(undef, { |
168 | columns => [ $self->config->{role_field} ] |
169 | })->all; |
078727e0 |
170 | $self->_roles(\@roles); |
5000f545 |
171 | } else { |
ff7203cb |
172 | Catalyst::Exception->throw("role table does not have a column called " . $self->config->{'role_field'}); |
5000f545 |
173 | } |
5000f545 |
174 | } else { |
175 | Catalyst::Exception->throw("user->roles accessed, but no role configuration found"); |
176 | } |
177 | |
ff7203cb |
178 | return @{$self->_roles}; |
5000f545 |
179 | } |
180 | |
181 | sub for_session { |
ff7203cb |
182 | my $self = shift; |
4bebb973 |
183 | |
f26005a7 |
184 | #return $self->get($self->config->{'id_field'}); |
4bebb973 |
185 | |
be7c0c30 |
186 | #my $frozenuser = $self->_user->result_source->schema->freeze( $self->_user ); |
187 | #return $frozenuser; |
4bebb973 |
188 | |
f26005a7 |
189 | my %userdata = $self->_user->get_columns(); |
e746d363 |
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 | } |
ff7203cb |
198 | } |
199 | |
200 | sub from_session { |
201 | my ($self, $frozenuser, $c) = @_; |
4bebb973 |
202 | |
be7c0c30 |
203 | #my $obj = $self->resultset->result_source->schema->thaw( $frozenuser ); |
204 | #$self->_user($obj); |
4bebb973 |
205 | |
be7c0c30 |
206 | #if (!exists($self->config->{'use_userdata_from_session'}) || $self->config->{'use_userdata_from_session'} == 0) { |
207 | # $self->_user->discard_changes(); |
208 | # } |
4bebb973 |
209 | # |
be7c0c30 |
210 | # return $self; |
4bebb973 |
211 | # |
be7c0c30 |
212 | ## if use_userdata_from_session is defined in the config, we fill in the user data from the session. |
70462180 |
213 | if (exists($self->config->{'use_userdata_from_session'}) && $self->config->{'use_userdata_from_session'} != 0) { |
48ca4fcf |
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 | |
f26005a7 |
225 | $obj->in_storage(1); |
226 | $self->_user($obj); |
227 | return $self; |
f26005a7 |
228 | } |
70462180 |
229 | |
230 | if (ref $frozenuser eq 'HASH') { |
231 | return $self->load({ |
232 | map { ($_ => $frozenuser->{$_}) } |
233 | @{ $self->config->{id_field} } |
9db54bcf |
234 | }, $c); |
70462180 |
235 | } |
236 | |
237 | return $self->load( { $self->config->{'id_field'} => $frozenuser }, $c); |
5000f545 |
238 | } |
239 | |
240 | sub get { |
241 | my ($self, $field) = @_; |
4bebb973 |
242 | |
c305eeac |
243 | if (my $code = $self->_user->can($field)) { |
244 | return $self->_user->$code; |
6d4bea88 |
245 | } |
51f40056 |
246 | elsif (my $accessor = |
247 | try { $self->_user->result_source->column_info($field)->{accessor} }) { |
6d4bea88 |
248 | return $self->_user->$accessor; |
5000f545 |
249 | } else { |
51f40056 |
250 | # XXX this should probably throw |
5000f545 |
251 | return undef; |
252 | } |
253 | } |
254 | |
c1d29ab7 |
255 | sub get_object { |
f26005a7 |
256 | my ($self, $force) = @_; |
4bebb973 |
257 | |
f26005a7 |
258 | if ($force) { |
259 | $self->_user->discard_changes; |
260 | } |
261 | |
c1d29ab7 |
262 | return $self->_user; |
5000f545 |
263 | } |
264 | |
c1d29ab7 |
265 | sub obj { |
f26005a7 |
266 | my ($self, $force) = @_; |
4bebb973 |
267 | |
f26005a7 |
268 | return $self->get_object($force); |
5000f545 |
269 | } |
270 | |
69100364 |
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 | |
28db23cf |
282 | sub can { |
283 | my $self = shift; |
284 | return $self->SUPER::can(@_) || do { |
a4523c16 |
285 | my ($method) = @_; |
c30ad9df |
286 | if (not ref $self) { |
287 | undef; |
288 | } elsif (not $self->_user) { |
9821fc6a |
289 | undef; |
290 | } elsif (my $code = $self->_user->can($method)) { |
28db23cf |
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 | |
5000f545 |
301 | sub AUTOLOAD { |
302 | my $self = shift; |
303 | (my $method) = (our $AUTOLOAD =~ /([^:]+)$/); |
304 | return if $method eq "DESTROY"; |
305 | |
c30ad9df |
306 | return unless ref $self; |
307 | |
8e553a46 |
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 | } |
5000f545 |
318 | } |
319 | |
6d4bea88 |
320 | __PACKAGE__->meta->make_immutable(inline_constructor => 0); |
321 | |
5000f545 |
322 | 1; |
323 | __END__ |
324 | |
325 | =head1 NAME |
326 | |
6727afe2 |
327 | Catalyst::Authentication::Store::DBIx::Class::User - The backing user |
328 | class for the Catalyst::Authentication::Store::DBIx::Class storage |
c1d29ab7 |
329 | module. |
5000f545 |
330 | |
331 | =head1 VERSION |
332 | |
a1c47a34 |
333 | This documentation refers to version 0.1503. |
5000f545 |
334 | |
335 | =head1 SYNOPSIS |
336 | |
c1d29ab7 |
337 | Internal - not used directly, please see |
6727afe2 |
338 | L<Catalyst::Authentication::Store::DBIx::Class> for details on how to |
c1d29ab7 |
339 | use this module. If you need more information than is present there, read the |
340 | source. |
93102ff5 |
341 | |
4bebb973 |
342 | |
5000f545 |
343 | |
344 | =head1 DESCRIPTION |
345 | |
6727afe2 |
346 | The Catalyst::Authentication::Store::DBIx::Class::User class implements user storage |
c1d29ab7 |
347 | connected to an underlying DBIx::Class schema object. |
5000f545 |
348 | |
349 | =head1 SUBROUTINES / METHODS |
350 | |
4bebb973 |
351 | =head2 new |
5000f545 |
352 | |
c1d29ab7 |
353 | Constructor. |
5000f545 |
354 | |
4bebb973 |
355 | =head2 load ( $authinfo, $c ) |
5000f545 |
356 | |
c1d29ab7 |
357 | Retrieves a user from storage using the information provided in $authinfo. |
5000f545 |
358 | |
c1d29ab7 |
359 | =head2 supported_features |
5000f545 |
360 | |
c1d29ab7 |
361 | Indicates the features supported by this class. These are currently Roles and Session. |
5000f545 |
362 | |
363 | =head2 roles |
364 | |
c1d29ab7 |
365 | Returns an array of roles associated with this user, if roles are configured for this user class. |
5000f545 |
366 | |
367 | =head2 for_session |
368 | |
4bebb973 |
369 | Returns a serialized user for storage in the session. |
5000f545 |
370 | |
fbe76043 |
371 | =head2 from_session |
372 | |
4bebb973 |
373 | Revives a serialized user from storage in the session. |
fbe76043 |
374 | |
c1d29ab7 |
375 | =head2 get ( $fieldname ) |
5000f545 |
376 | |
4bebb973 |
377 | Returns the value of $fieldname for the user in question. Roughly translates to a call to |
c1d29ab7 |
378 | the DBIx::Class::Row's get_column( $fieldname ) routine. |
5000f545 |
379 | |
4bebb973 |
380 | =head2 get_object |
5000f545 |
381 | |
c1d29ab7 |
382 | Retrieves the DBIx::Class object that corresponds to this user |
5000f545 |
383 | |
384 | =head2 obj (method) |
385 | |
c1d29ab7 |
386 | Synonym for get_object |
5000f545 |
387 | |
69100364 |
388 | =head2 auto_create |
389 | |
4bebb973 |
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. |
4117c46f |
392 | By default, this will call the C<auto_create()> method of the resultset associated |
69100364 |
393 | with this object. It is up to you to implement that method. |
394 | |
395 | =head2 auto_update |
396 | |
4117c46f |
397 | This is called when the auto_update_user option is turned on in |
cccbdd0a |
398 | Catalyst::Plugin::Authentication. Note that by default the DBIx::Class store |
4117c46f |
399 | uses every field in the authinfo hash to match the user. This means any |
50631330 |
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). |
4117c46f |
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) |
69100364 |
410 | |
9791ac85 |
411 | =head2 AUTOLOAD |
412 | |
e2ecb860 |
413 | Delegates method calls to the underlying user row. |
9791ac85 |
414 | |
415 | =head2 can |
416 | |
e2ecb860 |
417 | Delegates handling of the C<< can >> method to the underlying user row. |
9791ac85 |
418 | |
b3c995e9 |
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 | |
5000f545 |
433 | =head1 BUGS AND LIMITATIONS |
434 | |
435 | None known currently, please email the author if you find any. |
436 | |
437 | =head1 AUTHOR |
438 | |
fbe76043 |
439 | Jason Kuri (jayk@cpan.org) |
5000f545 |
440 | |
28db23cf |
441 | =head1 CONTRIBUTORS |
442 | |
443 | Matt S Trout (mst) <mst@shadowcat.co.uk> |
444 | |
a4523c16 |
445 | (fixes wrt can/AUTOLOAD sponsored by L<http://reask.com/>) |
446 | |
c1d29ab7 |
447 | =head1 LICENSE |
5000f545 |
448 | |
28db23cf |
449 | Copyright (c) 2007-2010 the aforementioned authors. All rights |
c1d29ab7 |
450 | reserved. This program is free software; you can redistribute |
451 | it and/or modify it under the same terms as Perl itself. |
5000f545 |
452 | |
453 | =cut |