File Coverage

blib/lib/Dancer2/Plugin/Auth/Extensible/Provider/DBIC.pm
Criterion Covered Total %
statement 170 175 97.1
branch 83 102 81.3
condition 22 33 66.6
subroutine 21 21 100.0
pod 0 10 0.0
total 296 341 86.8


line stmt bran cond sub pod time code
1             package Dancer2::Plugin::Auth::Extensible::Provider::DBIC;
2              
3 3     3   3362630 use Carp;
  3         24  
  3         240  
4 3     3   481 use Dancer2::Core::Types qw/Bool Int Str/;
  3         153033  
  3         45  
5 3     3   7346 use DateTime;
  3         486249  
  3         109  
6 3     3   1557 use DBIx::Class::ResultClass::HashRefInflator;
  3         1001  
  3         115  
7 3     3   23 use Scalar::Util qw(blessed);
  3         13  
  3         182  
8 3     3   1321 use String::CamelCase qw(camelize);
  3         1806  
  3         195  
9              
10 3     3   574 use Moo;
  3         7083  
  3         24  
11             with "Dancer2::Plugin::Auth::Extensible::Role::Provider";
12 3     3   2270 use namespace::clean;
  3         9  
  3         30  
13              
14             our $VERSION = '0.624';
15              
16             =head1 NAME
17              
18             Dancer2::Plugin::Auth::Extensible::Provider::DBIC - authenticate via the
19             L<Dancer2::Plugin::DBIC> plugin
20              
21              
22             =head1 DESCRIPTION
23              
24             This class is an authentication provider designed to authenticate users against
25             a database, using L<Dancer2::Plugin::DBIC> to access a database.
26              
27             See L<Dancer2::Plugin::DBIC> for how to configure a database connection
28             appropriately; see the L</CONFIGURATION> section below for how to configure this
29             authentication provider with database details.
30              
31             See L<Dancer2::Plugin::Auth::Extensible> for details on how to use the
32             authentication framework.
33              
34              
35             =head1 CONFIGURATION
36              
37             This provider tries to use sensible defaults, in the same manner as
38             L<Dancer2::Plugin::Auth::Extensible::Provider::Database>, so you may not need
39             to provide much configuration if your database tables look similar to those.
40              
41             The most basic configuration, assuming defaults for all options, and defining a
42             single authentication realm named 'users':
43              
44             plugins:
45             Auth::Extensible:
46             realms:
47             users:
48             provider: 'DBIC'
49              
50             You would still need to have provided suitable database connection details to
51             L<Dancer2::Plugin::DBIC>, of course; see the docs for that plugin for full
52             details, but it could be as simple as, e.g.:
53              
54             plugins:
55             Auth::Extensible:
56             realms:
57             users:
58             provider: 'DBIC'
59             users_resultset: 'User'
60             roles_resultset: Role
61             user_roles_resultset: UserRole
62             DBIC:
63             default:
64             dsn: dbi:mysql:database=mydb;host=localhost
65             schema_class: MyApp::Schema
66             user: user
67             pass: secret
68              
69             A full example showing all options:
70              
71             plugins:
72             Auth::Extensible:
73             realms:
74             users:
75             provider: 'DBIC'
76              
77             # Should get_user_details return an inflated DBIC row
78             # object? Defaults to false which will return a hashref
79             # inflated using DBIx::Class::ResultClass::HashRefInflator
80             # instead. This also affects what `logged_in_user` returns.
81             user_as_object: 1
82              
83             # Optionally specify the DBIC resultset names if you don't
84             # use the defaults (as shown). These and the column names are the
85             # only settings you might need. The relationships between
86             # these resultsets is automatically introspected by
87             # inspection of the schema.
88             users_resultset: User
89             roles_resultset: Role
90             user_roles_resultset: UserRole
91              
92             # optionally set the column names
93             users_username_column: username
94             users_password_column: password
95             roles_role_column: role
96              
97             # This plugin supports the DPAE record_lastlogin functionality.
98             # Optionally set the column name:
99             users_lastlogin_column: lastlogin
100              
101             # Optionally set columns for user_password functionality in
102             # Dancer2::Plugin::Auth::Extensible
103             users_pwresetcode_column: pw_reset_code
104             users_pwchanged_column: # Time of reset column. No default.
105              
106             # Days after which passwords expire. See logged_in_user_password_expired
107             # functionality in Dancer2::Plugin::Auth::Extensible
108             password_expiry_days: # No default
109              
110             # Optionally set the name of the DBIC schema
111             schema_name: myschema
112              
113             # Optionally set additional conditions when searching for the
114             # user in the database. These are the same format as required
115             # by DBIC, and are passed directly to the DBIC resultset search
116             user_valid_conditions:
117             deleted: 0
118             account_request:
119             "<": 1
120              
121             # Optionally specify a key for the user's roles to be returned in.
122             # Roles will be returned as role_name => 1 hashref pairs
123             roles_key: roles
124              
125             # Optionally specify the algorithm when encrypting new passwords
126             encryption_algorithm: SHA-512
127              
128             # Optional: To validate passwords using a method called
129             # 'check_password' in users_resultset result class
130             # which takes the password to check as a single argument:
131             users_password_check: check_password
132              
133              
134             =over
135              
136             =cut
137              
138             sub deprecated_setting {
139 6     6 0 18 my ( $setting, $replacement ) = @_;
140 6         67 carp __PACKAGE__, " config setting \"$setting\" is deprecated.",
141             " Use \"$replacement\" instead.";
142             }
143              
144             sub BUILDARGS {
145 8     8 0 15768 my $class = shift;
146 8 50       77 my %args = ref( $_[0] ) eq 'HASH' ? %{ $_[0] } : @_;
  0         0  
147              
148 8         50 my $app = $args{plugin}->app;
149              
150             # backwards compat
151              
152             # deprecate the *_source settings, but don't change anything yet
153             deprecated_setting('users_source', 'users_resultset')
154 8 100       181 if $args{users_source};
155              
156             deprecated_setting('roles_source', 'roles_resultset')
157 8 100       882 if $args{roles_source};
158              
159             deprecated_setting('user_roles_source', 'user_roles_resultset')
160 8 100       693 if $args{user_roles_source};
161              
162             # deprecate the *_table settings and move them into source, which
163             # will be used in the lazy build for the correct *_resultset settings
164 8 100       677 if ( $args{users_table} ) {
165 1         5 deprecated_setting( 'users_table', 'users_resultset' );
166             $args{users_source} = delete $args{users_table}
167 1 50       693 if !$args{users_source};
168             }
169              
170 8 100       23 if ( $args{roles_table} ) {
171 1         4 deprecated_setting( 'roles_table', 'roles_resultset' );
172             $args{roles_source} = delete $args{roles_table}
173 1 50       669 if !$args{roles_source};
174             }
175              
176 8 100       25 if ( $args{user_roles_table} ) {
177 1         5 deprecated_setting( 'user_roles_table', 'user_roles_resultset' );
178             $args{user_roles_source} = delete $args{user_roles_table}
179 1 50       681 if !$args{user_roles_source};
180             }
181              
182 8         194 return \%args;
183             }
184              
185             =item user_as_object
186              
187             Defaults to false.
188              
189             By default a row object is returned as a simple hash reference using
190             L<DBIx::Class::ResultClass::HashRefInflator>. Setting this to true
191             causes normal row objects to be returned instead.
192              
193             =item users_resultset
194              
195             Defaults to C<User>.
196              
197             Specifies the L<DBIx::Class::ResultSet> that contains the users.
198             The relationship to user_roles_source will be introspected from the schema.
199              
200             =item roles_resultset
201              
202             Defaults to C<Roles>.
203              
204             Specifies the L<DBIx::Class::ResultSet> that contains the roles.
205             The relationship to user_roles_source will be introspected from the schema.
206              
207             =item user_roles_resultset
208              
209             Defaults to C<User>.
210              
211             Specifies the L<DBIx::Class::ResultSet> that contains the user_roles joining table.
212             The relationship to the user and role source will be introspected from the schema.
213              
214             =item users_username_column
215              
216             Specifies the column name of the username column in the users table
217              
218             =item users_password_column
219              
220             Specifies the column name of the password column in the users table
221              
222             =item roles_role_column
223              
224             Specifies the column name of the role name column in the roles table
225              
226             =item schema_name
227              
228             Specfies the name of the L<Dancer2::Plugin::DBIC> schema to use. If not
229             specified, will default in the same manner as the DBIC plugin.
230              
231             =item user_valid_conditions
232              
233             Specifies additional search parameters when looking up a user in the users table.
234             For example, you might want to exclude any account this is flagged as deleted
235             or disabled.
236              
237             The value of this parameter will be passed directly to DBIC as a search condition.
238             It is therefore possible to nest parameters and use different operators for the
239             condition. See the example config above for an example.
240              
241             =item roles_key
242              
243             Specifies a key for the returned user hash to also return the user's roles in.
244             The value of this key will contain a hash ref, which will contain each
245             permission with a value of 1. In your code you might then have:
246              
247             my $user = logged_in_user;
248             return foo_bar($user);
249              
250             sub foo_bar
251             { my $user = shift;
252             if ($user->{roles}->{beer_drinker}) {
253             ...
254             }
255             }
256              
257             This isn't intended to replace the L<Dancer2::Plugin::Auth::Extensible/user_has_role>
258             keyword. Instead it is intended to make it easier to access a user's roles if the
259             user hash is being passed around (without requiring access to the user_has_role
260             keyword in other modules).
261              
262             =back
263              
264             =head1 DEPRECATED SETTINGS
265              
266             =over
267              
268             =item user_source
269              
270             =item user_table
271              
272             Specifies the source name that contains the users. This will be camelized to generate
273             the resultset name. The relationship to user_roles_source will be introspected from
274             the schema.
275              
276             =item role_source
277              
278             =item role_table
279              
280             Specifies the source name that contains the roles. This will be camelized to generate
281             the resultset name. The relationship to user_roles_source will be introspected from
282             the schema.
283              
284             =item user_roles_source
285              
286             =item user_roles_table
287              
288             =back
289              
290             Specifies the source name that contains the user_roles joining table. This will be
291             camelized to generate the resultset name. The relationship to the user and role
292             source will be introspected from the schema.
293              
294             =head1 SUGGESTED SCHEMA
295              
296             If you use a schema similar to the examples provided here, you should need minimal
297             configuration to get this authentication provider to work for you. The examples
298             given here should be MySQL-compatible; minimal changes should be required to use
299             them with other database engines.
300              
301             =head2 user Table
302              
303             You'll need a table to store user accounts in, of course. A suggestion is something
304             like:
305              
306             CREATE TABLE user (
307             id int(11) NOT NULL AUTO_INCREMENT,
308             username varchar(32) NOT NULL,
309             password varchar(40) DEFAULT NULL,
310             name varchar(128) DEFAULT NULL,
311             email varchar(255) DEFAULT NULL,
312             deleted tinyint(1) NOT NULL DEFAULT '0',
313             lastlogin datetime DEFAULT NULL,
314             pw_changed datetime DEFAULT NULL,
315             pw_reset_code varchar(255) DEFAULT NULL,
316             PRIMARY KEY (id)
317             );
318              
319             All columns from the users table will be returned by the C<logged_in_user> keyword
320             for your convenience.
321              
322             =head2 role Table
323              
324             You'll need a table to store a list of available groups in.
325              
326             CREATE TABLE role (
327             id int(11) NOT NULL AUTO_INCREMENT,
328             role varchar(32) NOT NULL,
329             PRIMARY KEY (id)
330             );
331              
332             =head2 user_role Table
333              
334             Also requred is a table mapping the users to the roles.
335              
336             CREATE TABLE user_role (
337             user_id int(11) NOT NULL,
338             role_id int(11) NOT NULL,
339             PRIMARY KEY (user_id, role_id),
340             FOREIGN KEY (user_id) REFERENCES user(id),
341             FOREIGN KEY (role_id) REFERENCES role(id)
342             );
343              
344             =head1 SEE ALSO
345              
346             L<Dancer2::Plugin::Auth::Extensible>
347              
348             L<Dancer2::Plugin::DBIC>
349              
350             L<Dancer2::Plugin::Auth::Extensible::Provider::Database>
351              
352             =head1 AUTHORS
353              
354             Andrew Beverley C<< <a.beverley@ctrlo.com> >>
355              
356             Rewrite for Plugin2:
357              
358             Peter Mottram, C<< <peter@sysnix.com> >>
359              
360             =head1 CONTRIBUTORS
361              
362             Ben Kaufman (whosgonna)
363              
364             =head1 LICENSE AND COPYRIGHT
365              
366             Copyright 2015-2016 Andrew Beverley
367              
368             This program is free software; you can redistribute it and/or modify it
369             under the terms of either: the GNU General Public License as published
370             by the Free Software Foundation; or the Artistic License.
371              
372             See http://dev.perl.org/licenses/ for more information.
373              
374             =cut
375              
376             has user_as_object => (
377             is => 'ro',
378             isa => Bool,
379             default => 0,
380             );
381              
382             has dancer2_plugin_dbic => (
383             is => 'ro',
384             lazy => 1,
385             default => sub { $_[0]->plugin->app->with_plugin('Dancer2::Plugin::DBIC') },
386             handles => { dbic_schema => 'schema' },
387             init_arg => undef,
388             );
389              
390             has schema_name => ( is => 'ro', );
391              
392             has schema => (
393             is => 'ro',
394             lazy => 1,
395             default => sub {
396             my $self = shift;
397             $self->schema_name
398             ? $self->dbic_schema( $self->schema_name )
399             : $self->dbic_schema;
400             },
401             );
402              
403             has password_expiry_days => (
404             is => 'ro',
405             isa => Int,
406             );
407              
408             has roles_key => (
409             is => 'ro',
410             );
411              
412             has roles_resultset => (
413             is => 'ro',
414             lazy => 1,
415             default => sub { camelize( $_[0]->roles_source ) },
416             );
417              
418             has roles_role_column => (
419             is => 'ro',
420             default => 'role',
421             );
422              
423             has roles_source => (
424             is => 'ro',
425             default => 'role',
426             );
427              
428             has users_resultset => (
429             is => 'ro',
430             lazy => 1,
431             default => sub { camelize( $_[0]->users_source ) },
432             );
433              
434             has users_source => (
435             is => 'ro',
436             default => 'user',
437             );
438              
439             has users_lastlogin_column => (
440             is => 'ro',
441             default => 'lastlogin',
442             );
443              
444             has users_password_column => (
445             is => 'ro',
446             default => 'password',
447             );
448              
449             has users_pwchanged_column => (
450             is => 'ro',
451             );
452              
453             has users_pwresetcode_column => (
454             is => 'ro',
455             default => 'pw_reset_code',
456             );
457              
458             has users_password_check => (
459             is => 'ro',
460             );
461              
462             has users_username_column => (
463             is => 'ro',
464             default => 'username',
465             );
466              
467             has user_user_roles_relationship => (
468             is => 'ro',
469             lazy => 1,
470             default => sub { $_[0]->_build_user_roles_relationship('user') },
471             );
472              
473             has user_roles_resultset => (
474             is => 'ro',
475             lazy => 1,
476             default => sub { camelize( $_[0]->user_roles_source ) },
477             );
478              
479             has user_roles_source => (
480             is => 'ro',
481             default => 'user_roles',
482             );
483              
484             has user_valid_conditions => (
485             is => 'ro',
486             default => sub { {} },
487             );
488              
489             has role_user_roles_relationship => (
490             is => 'ro',
491             lazy => 1,
492             default => sub { $_[0]->_build_user_roles_relationship('role') },
493             );
494              
495             has user_roles_result_class => (
496             is => 'ro',
497             lazy => 1,
498             default => sub {
499             my $self = shift;
500             # undef if roles are disabled
501             return undef if $self->plugin->disable_roles;
502             return $self->schema->resultset( $self->user_roles_resultset )
503             ->result_source->result_class;
504             },
505             );
506              
507             sub _build_user_roles_relationship {
508 10     10   38 my ( $self, $name ) = @_;
509              
510 10 50       203 return undef if $self->plugin->disable_roles;
511              
512             # Introspect result sources to find relationships
513              
514 10         243 my $user_roles_class =
515             $self->schema->resultset( $self->user_roles_resultset )
516             ->result_source->result_class;
517              
518 10         3719 my $resultset_name = "${name}s_resultset";
519              
520 10         764 my $result_source =
521             $self->schema->resultset( $self->$resultset_name )->result_source;
522              
523 10         3551 foreach my $relname ( $result_source->relationships ) {
524 10         267 my $info = $result_source->relationship_info($relname);
525             # just check for a simple equality join condition. It could be other
526             # things (e.g. code ref) but for now this is unsupported.
527 10 50       70 next unless ref $info->{cond} eq 'HASH';
528 10         24 my %cond = %{ $info->{cond} };
  10         52  
529 10 50 33     141 if ( $info->{class} eq $user_roles_class
      33        
      33        
530             && $info->{attrs}->{accessor} eq 'multi'
531             && $info->{attrs}->{join_type} eq 'LEFT'
532             && scalar keys %cond == 1 )
533             {
534 10         87 return $relname;
535             }
536             }
537             }
538              
539             has role_relationship => (
540             is => 'ro',
541             lazy => 1,
542             default => sub { $_[0]->_build_relationship('role') },
543             );
544              
545             has user_relationship => (
546             is => 'ro',
547             lazy => 1,
548             default => sub { $_[0]->_build_relationship('user') },
549             );
550              
551             sub _build_relationship {
552 9     9   37 my ( $self, $name ) = @_;
553              
554 9 50       221 return undef if $self->plugin->disable_roles;
555              
556             # Introspect result sources to find relationships
557              
558 9         455 my $user_roles_class =
559             $self->schema->resultset( $self->user_roles_resultset )
560             ->result_source->result_class;
561              
562 9         3560 my $resultset_name = "${name}s_resultset";
563              
564 9         353 my $result_source =
565             $self->schema->resultset( $self->$resultset_name )->result_source;
566              
567 9         3184 my $user_roles_relationship = "${name}_user_roles_relationship";
568              
569             my ($relationship) = keys %{
570 9         182 $result_source->reverse_relationship_info(
  9         216  
571             $self->$user_roles_relationship
572             )
573             };
574              
575 9         5125 return $relationship;
576             }
577              
578             # Returns a DBIC rset for the user
579             sub _user_rset {
580 818     818   2490 my ($self, $column, $value, $options) = @_;
581 818         2803 my $username_column = $self->users_username_column;
582 818         2395 my $user_valid_conditions = $self->user_valid_conditions;
583              
584 818 50       2865 my $search_column = $column eq 'username'
    100          
585             ? $username_column
586             : $column eq 'pw_reset_code'
587             ? $self->users_pwresetcode_column
588             : $column;
589              
590             # Search based on standard username search, plus any additional
591             # conditions in ignore_user
592 818         4187 my $search = { %$user_valid_conditions, 'me.' . $search_column => $value };
593              
594             # Look up the user
595 818         17496 $self->schema->resultset($self->users_resultset)->search($search, $options);
596             }
597              
598             sub authenticate_user {
599 202     202 0 5575627 my ($self, $username, $password, %options) = @_;
600 202 100 100     2326 croak "username and password must be defined"
601             unless defined $username && defined $password;
602              
603 196         829 my ( $user ) = $self->_user_rset( 'username', $username )->all;
604 196 100       648172 return unless $user;
605              
606 76 50       4892 if ( my $password_check = $self->users_password_check ) {
607             # check password via result class method
608 0         0 return $user->$password_check($password);
609             }
610              
611             # OK, we found a user, let match_password (from our base class) take care of
612             # working out if the password is correct
613 76         361 my $password_column = $self->users_password_column;
614              
615 76 100       2347 if ( my $match =
616             $self->match_password( $password, $user->$password_column ) )
617             {
618 60 100       11339 if ( $options{lastlogin} ) {
619 56 100       1466 if ( my $lastlogin = $user->lastlogin ) {
620 36 50       781 if ( ref($lastlogin) eq '' ) {
621             # not inflated to DateTime
622 36         658 my $db_parser = $self->schema->storage->datetime_parser;
623 36         2400 $lastlogin = $db_parser->parse_datetime($lastlogin);
624             }
625             # Stash in session as epoch since we don't want to have to mess
626             # with with stringified data or perhaps session engine barfing
627             # when trying to serialize DateTime object.
628             $self->plugin->app->session->write(
629 36         30067 $options{lastlogin} => $lastlogin->epoch );
630             }
631 56         31574 $self->set_user_details( $username,
632             $self->users_lastlogin_column => DateTime->now, );
633             }
634 60         4056 return $match;
635             }
636 16         3659 return; # Make sure we return nothing
637             }
638              
639             sub set_user_password {
640 26     26 0 22378 my ( $self, $username, $password ) = @_;
641 26 100 100     972 croak "username and password must be defined"
642             unless defined $username && defined $password;
643              
644 20         142 my $encrypted = $self->encrypt_password($password);
645 20         5153 my $password_column = $self->users_password_column;
646 20         95 my %update = ( $password_column => $encrypted );
647 20 100       142 if ( my $pwchanged = $self->users_pwchanged_column ) {
648 18         206 $update{$pwchanged} = DateTime->now;
649             }
650 20         7402 $self->set_user_details( $username, %update );
651             }
652              
653             # Return details about the user. The user's row in the users table will be
654             # fetched and all columns returned as a hashref.
655             sub get_user_details {
656 280     280 0 2122988 my ($self, $username) = @_;
657 280 100       1426 croak "username must be defined"
658             unless defined $username;
659              
660             # Look up the user
661 278         1010 my $users_rs = $self->_user_rset(username => $username);
662              
663             # Inflate to a hashref, otherwise it's returned as a DBIC rset
664 278 100       166774 $users_rs->result_class('DBIx::Class::ResultClass::HashRefInflator')
665             unless $self->user_as_object;
666              
667 278         12757 my ($user) = $users_rs->all;
668            
669 278 100       705840 if (!$user) {
670 30         342 $self->plugin->app->log( 'debug', "No such user $username" );
671 30         22049 return;
672             }
673              
674 248 100       1453 if ( !$self->user_as_object ) {
675 124 50       699 if ( my $roles_key = $self->roles_key ) {
676 124         282 my @roles = @{ $self->get_user_roles($username) };
  124         566  
677 124         8459 my %roles = map { $_ => 1 } @roles;
  110         488  
678 124         588 $user->{$roles_key} = \%roles;
679             }
680             }
681 248         1151 return $user;
682             }
683              
684             # Find a user based on a password reset code
685             sub get_user_by_code {
686 44     44 0 420275 my ($self, $code) = @_;
687 44 100 66     834 croak "code needs to be specified"
688             unless $code && $code ne '';
689              
690 40         174 my ($user) = $self->_user_rset( pw_reset_code => $code )->all;
691 40 100       127140 return unless $user;
692              
693 8         516 my $username_column = $self->users_username_column;
694 8         248 return $user->$username_column;
695             }
696              
697             sub create_user {
698 32     32 0 498057 my ($self, %user) = @_;
699 32         156 my $username_column = $self->users_username_column;
700 32         107 my $username = delete $user{username}; # Prevent attempt to update wrong key
701 32 100 100     835 croak "Username not supplied in args"
702             unless defined $username && $username ne '';
703              
704 28         714 $self->schema->resultset($self->users_resultset)->create({
705             $username_column => $username
706             });
707 22         52327 $self->set_user_details($username, %user);
708             }
709              
710             # Update a user. Username is provided in the update details
711             sub set_user_details {
712 140     140 0 396457 my ($self, $username, %update) = @_;
713              
714 140 100       1179 croak "Username to update needs to be specified"
715             unless $username;
716              
717             # Look up the user
718 136         686 my ($user) = $self->_user_rset(username => $username)->all;
719 136 100       422455 $user or return;
720              
721             # Are we expecting a user_roles key?
722 126 50       7903 if ( my $roles_key = $self->roles_key ) {
723 126 100       676 if ( my $new_roles = delete $update{$roles_key} ) {
724              
725 4         25 my $roles_role_column = $self->roles_role_column;
726 4         16 my $users_username_column = $self->users_username_column;
727              
728 4         120 my @all_roles =
729             $self->schema->resultset( $self->roles_resultset )->all;
730             my %existing_roles =
731 4         8443 map { $_ => 1 } @{ $self->get_user_roles($username) };
  0         0  
  4         181  
732              
733 4         141 foreach my $role (@all_roles) {
734 12         264 my $role_name = $role->$roles_role_column;
735              
736 12 100 66     297 if ( $new_roles->{$role_name}
    50 33        
737             && !$existing_roles{$role_name} )
738             {
739             # Needs to be added
740             $self->schema->resultset( $self->user_roles_resultset )
741             ->create(
742             {
743             $self->user_relationship => {
744             $users_username_column => $username,
745 4         81 %{ $self->user_valid_conditions }
  4         104  
746             },
747             $self->role_relationship => {
748             $roles_role_column => $role_name
749             },
750             }
751             );
752             }
753             elsif ( !$new_roles->{$role_name}
754             && $existing_roles{$role_name} )
755             {
756             # Needs to be removed
757 0         0 $self->schema->resultset( $self->user_roles_resultset )
758             ->search(
759             {
760             $self->user_relationship
761             . ".$users_username_column" => $username,
762             $self->role_relationship
763             . ".$roles_role_column" => $role_name,
764             },
765             {
766             join => [
767             $self->user_relationship,
768             $self->role_relationship
769             ],
770             }
771             )->delete;
772             }
773             }
774             }
775             }
776              
777             # Move password reset code between keys if required
778 126 50       69142 if (my $users_pwresetcode_column = $self->users_pwresetcode_column) {
779 126 100       561 if (exists $update{pw_reset_code}) {
780 10         38 my $pw_reset_code = delete $update{pw_reset_code};
781 10         44 $update{$users_pwresetcode_column} = $pw_reset_code;
782             }
783             }
784 126         1555 $user->update({%update});
785             # Update $username if it was submitted in update
786 126 50       147201 $username = $update{username} if $update{username};
787 126         681 return $self->get_user_details($username);
788             }
789              
790             sub get_user_roles {
791 170     170 0 77793 my ($self, $username) = @_;
792 170 100       1092 croak "username must be defined"
793             unless defined $username;
794              
795 168         4323 my $role_relationship = $self->role_relationship;
796 168         4818 my $user_user_roles_relationship = $self->user_user_roles_relationship;
797 168         1996 my $roles_role_column = $self->roles_role_column;
798              
799 168         812 my $options =
800             { prefetch => { $user_user_roles_relationship => $role_relationship } };
801              
802 168         636 my ($user) = $self->_user_rset(username => $username, $options)->all;
803              
804 168 100       2892360 if (!$user) {
805 4         380 $self->plugin->app->log( 'debug',
806             "No such user $username when looking for roles" );
807 4         2658 return;
808             }
809              
810 164         14958 my @roles;
811 164         4427 foreach my $ur ($user->$user_user_roles_relationship)
812             {
813 166         29392 my $role = $ur->$role_relationship->$roles_role_column;
814 166         10197 push @roles, $role;
815             }
816              
817 164         23421 \@roles;
818             }
819              
820             sub password_expired {
821 12     12 0 14921 my ($self, $user) = @_;
822 12 100 66     558 croak "user must be specified"
      100        
823             unless defined $user
824             && ( ref($user) eq 'HASH'
825             || ( blessed($user) && $user->isa('DBIx::Class::Row') ) );
826              
827 10 50       70 my $expiry = $self->password_expiry_days or return 0; # No expiry set
828              
829 10 50       62 if (my $pwchanged = $self->users_pwchanged_column) {
830             my $last_changed =
831 10 100       183 $self->user_as_object ? $user->$pwchanged : $user->{$pwchanged};
832              
833             # If not changed then report expired
834 10 100       127 return 1 unless $last_changed;
835              
836 8 50       37 if ( ref($last_changed) ne 'DateTime' ) {
837             # not inflated to DateTime by schema so do it now
838 8         208 my $db_parser = $self->schema->storage->datetime_parser;
839 8         151102 $last_changed = $db_parser->parse_datetime($last_changed);
840             }
841 8         6059 my $duration = $last_changed->delta_days(DateTime->now);
842 8 100       2937 $duration->in_units('days') > $expiry ? 1 : 0;
843             } else {
844 0           croak "users_pwchanged_column not configured";
845             }
846             }
847              
848             1;