File Coverage

blib/lib/Dancer2/Plugin/Auth/Extensible/Provider/Database.pm
Criterion Covered Total %
statement 53 53 100.0
branch 19 24 79.1
condition 4 6 66.6
subroutine 9 9 100.0
pod 6 6 100.0
total 91 98 92.8


line stmt bran cond sub pod time code
1             package Dancer2::Plugin::Auth::Extensible::Provider::Database;
2              
3 2     2   1749978 use Carp;
  2         13  
  2         133  
4 2     2   535 use Moo;
  2         10863  
  2         12  
5             with "Dancer2::Plugin::Auth::Extensible::Role::Provider";
6 2     2   2303 use namespace::clean;
  2         10905  
  2         19  
7              
8             our $VERSION = '0.630';
9              
10             =head1 NAME
11              
12             Dancer2::Plugin::Auth::Extensible::Provider::Database - authenticate via a database
13              
14              
15             =head1 DESCRIPTION
16              
17             This class is an authentication provider designed to authenticate users against
18             a database, using L<Dancer2::Plugin::Database> to access a database.
19              
20             L<Crypt::SaltedHash> is used to handle hashed passwords securely; you wouldn't
21             want to store plain text passwords now, would you? (If your answer to that is
22             yes, please reconsider; you really don't want to do that, when it's so easy to
23             do things right!)
24              
25             See L<Dancer2::Plugin::Database> for how to configure a database connection
26             appropriately; see the L</CONFIGURATION> section below for how to configure this
27             authentication provider with database details.
28              
29             See L<Dancer2::Plugin::Auth::Extensible> for details on how to use the
30             authentication framework, including how to pick a more useful authentication
31             provider.
32              
33              
34             =head1 CONFIGURATION
35              
36             This provider tries to use sensible defaults, so you may not need to provide
37             much configuration if your database tables look similar to those in the
38             L</SUGGESTED SCHEMA> section below.
39              
40             The most basic configuration, assuming defaults for all options, and defining a
41             single authentication realm named 'users':
42              
43             plugins:
44             Auth::Extensible:
45             realms:
46             users:
47             provider: 'Database'
48              
49             You would still need to have provided suitable database connection details to
50             L<Dancer2::Plugin::Database>, of course; see the docs for that plugin for full
51             details, but it could be as simple as, e.g.:
52              
53             plugins:
54             Auth::Extensible:
55             realms:
56             users:
57             provider: 'Database'
58             Database:
59             driver: 'SQLite'
60             database: 'test.sqlite'
61             on_connect_do: ['PRAGMA foreign_keys = ON']
62             dbi_params:
63             PrintError: 0
64             RaiseError: 1
65              
66              
67             A full example showing all options:
68              
69             plugins:
70             Auth::Extensible:
71             realms:
72             users:
73             provider: 'Database'
74             # optionally set DB connection name to use (see named
75             # connections in Dancer2::Plugin::Database docs)
76             db_connection_name: 'foo'
77              
78             # Optionally disable roles support, if you only want to check
79             # for successful logins but don't need to use role-based access:
80             disable_roles: 1
81              
82             # optionally specify names of tables if they're not the defaults
83             # (defaults are 'users', 'roles' and 'user_roles')
84             users_table: 'users'
85             roles_table: 'roles'
86             user_roles_table: 'user_roles'
87              
88             # optionally set the column names (see the SUGGESTED SCHEMA
89             # section below for the default names; if you use them, they'll
90             # Just Work)
91             users_id_column: 'id'
92             users_username_column: 'username'
93             users_password_column: 'password'
94             roles_id_column: 'id'
95             roles_role_column: 'role'
96             user_roles_user_id_column: 'user_id'
97             user_roles_role_id_column: 'roles_id'
98              
99             See the main L<Dancer2::Plugin::Auth::Extensible> documentation for how to
100             configure multiple authentication realms.
101              
102             =head1 SUGGESTED SCHEMA
103              
104             If you use a schema similar to the examples provided here, you should need
105             minimal configuration to get this authentication provider to work for you.
106              
107             The examples given here should be MySQL-compatible; minimal changes should be
108             required to use them with other database engines.
109              
110             =head2 users table
111              
112             You'll need a table to store user accounts in, of course. A suggestion is
113             something like:
114              
115             CREATE TABLE users (
116             id INTEGER AUTO_INCREMENT PRIMARY KEY,
117             username VARCHAR(32) NOT NULL UNIQUE KEY,
118             password VARCHAR(40) NOT NULL
119             );
120              
121             You will quite likely want other fields to store e.g. the user's name, email
122             address, etc; all columns from the users table will be returned by the
123             C<logged_in_user> keyword for your convenience.
124              
125             =head2 roles table
126              
127             You'll need a table to store a list of available roles in (unless you're not
128             using roles - in which case, disable role support (see the L</CONFIGURATION>
129             section).
130              
131             CREATE TABLE roles (
132             id INTEGER AUTO_INCREMENT PRIMARY KEY,
133             role VARCHAR(32) NOT NULL
134             );
135              
136             =head2 user_roles table
137              
138             Finally, (unless you've disabled role support) you'll need a table to store
139             user <-> role mappings (i.e. one row for every role a user has; so adding
140             extra roles to a user consists of adding a new role to this table). It's
141             entirely up to you whether you use an "id" column in this table; you probably
142             shouldn't need it.
143              
144             CREATE TABLE user_roles (
145             user_id INTEGER NOT NULL,
146             role_id INTEGER NOT NULL,
147             UNIQUE KEY user_role (user_id, role_id)
148             );
149              
150             If you're using InnoDB tables rather than the default MyISAM, you could add a
151             foreign key constraint for better data integrity; see the MySQL documentation
152             for details, but a table definition using foreign keys could look like:
153              
154             CREATE TABLE user_roles (
155             user_id INTEGER, FOREIGN KEY (user_id) REFERENCES users (id),
156             role_id INTEGER, FOREIGN KEY (role_id) REFERENCES roles (id),
157             UNIQUE KEY user_role (user_id, role_id)
158             ) ENGINE=InnoDB;
159              
160             =head1 ATTRIBUTES
161              
162             =head2 dancer2_plugin_database
163              
164             Lazy-loads the correct instance of L<Dancer2::Plugin::Database> which handles
165             the following methods:
166              
167             =over
168              
169             =item * plugin_database
170              
171             This corresponds to the C<database> keyword from L<Dancer2::Plugin::Database>.
172              
173             =back
174              
175             =cut
176              
177             has dancer2_plugin_database => (
178             is => 'ro',
179             lazy => 1,
180             default =>
181             sub { $_[0]->plugin->app->with_plugin('Dancer2::Plugin::Database') },
182             handles => { plugin_database => 'database' },
183             init_arg => undef,
184             );
185              
186             =head2 database
187              
188             The connected L</plugin_database> using L</db_connection_name>.
189              
190             =cut
191              
192             has database => (
193             is => 'ro',
194             lazy => 1,
195             default => sub {
196             my $self = shift;
197             $self->plugin_database($self->db_connection_name);
198             },
199             );
200              
201             =head2 db_connection_name
202              
203             Optional.
204              
205             =cut
206              
207             has db_connection_name => (
208             is => 'ro',
209             );
210              
211             =head2 users_table
212              
213             Defaults to 'users'.
214              
215             =cut
216              
217             has users_table => (
218             is => 'ro',
219             default => 'users',
220             );
221              
222             =head2 users_id_column
223              
224             Defaults to 'id'.
225              
226             =cut
227              
228             has users_id_column => (
229             is => 'ro',
230             default => 'id',
231             );
232              
233             =head2 users_username_column
234              
235             Defaults to 'username'.
236              
237             =cut
238              
239             has users_username_column => (
240             is => 'ro',
241             default => 'username',
242             );
243              
244             =head2 users_password_column
245              
246             Defaults to 'password'.
247              
248             =cut
249              
250             has users_password_column => (
251             is => 'ro',
252             default => 'password',
253             );
254              
255             =head2 roles_table
256              
257             Defaults to 'roles'.
258              
259             =cut
260              
261             has roles_table => (
262             is => 'ro',
263             default => 'roles',
264             );
265              
266             =head2 roles_id_column
267              
268             Defaults to 'id'.
269              
270             =cut
271              
272             has roles_id_column => (
273             is => 'ro',
274             default => 'id',
275             );
276              
277             =head2 roles_role_column
278              
279             Defaults to 'role'.
280              
281             =cut
282              
283             has roles_role_column => (
284             is => 'ro',
285             default => 'role',
286             );
287              
288             =head2 user_roles_table
289              
290             Defaults to 'user_roles'.
291              
292             =cut
293              
294             has user_roles_table => (
295             is => 'ro',
296             default => 'user_roles',
297             );
298              
299             =head2 user_roles_user_id_column
300              
301             Defaults to 'user_id'.
302              
303             =cut
304              
305             has user_roles_user_id_column => (
306             is => 'ro',
307             default => 'user_id',
308             );
309              
310             =head2 user_roles_role_id_column
311              
312             Defaults to 'role_id'.
313              
314             =cut
315              
316             has user_roles_role_id_column => (
317             is => 'ro',
318             default => 'role_id',
319             );
320              
321             =head1 METHODS
322              
323             =head2 authenticate_user $username, $password
324              
325             =cut
326              
327             sub authenticate_user {
328 61     61 1 1398171 my ($self, $username, $password) = @_;
329 61 100 100     754 croak "Both of username and password must be defined"
330             unless defined $username && defined $password;
331              
332             # Look up the user:
333 58         221 my $user = $self->get_user_details($username);
334 58 100       298 return unless $user;
335              
336             # OK, we found a user, let match_password (from our base class) take care of
337             # working out if the password is correct
338              
339 21         132 my $correct = $user->{ $self->users_password_column };
340              
341             # do NOT authenticate when password is empty/undef
342 21 50 33     149 return undef unless ( defined $correct && $correct ne '' );
343              
344 21         136 return $self->match_password( $password, $correct );
345             }
346              
347             =head2 create_user
348              
349             =cut
350              
351             sub create_user {
352 12     12 1 233277 my ( $self, %options ) = @_;
353              
354             # Prevent attempt to update wrong key
355             my $username = delete $options{username}
356 12 100       247 or croak "username needs to be specified for create_user";
357              
358             # password column might not be nullable so set to empty since we fail
359             # auth attempts for empty passwords anyway
360 10         230 my $ret = $self->database->quick_insert( $self->users_table,
361             { $self->users_username_column => $username, password => '', %options }
362             );
363 7 50       2766 return $ret ? $self->get_user_details($username) : undef;
364             }
365              
366             =head2 get_user_details $username
367              
368             =cut
369              
370             # Return details about the user. The user's row in the users table will be
371             # fetched and all columns returned as a hashref.
372             sub get_user_details {
373 158     158 1 643665 my ($self, $username) = @_;
374 158 100       736 croak "username must be defined"
375             unless defined $username;
376              
377             # Get our database handle and find out the table and column names:
378 156         3127 my $database = $self->database;
379              
380             # Look up the user,
381 156         2980 my $user = $database->quick_select(
382             $self->users_table, { $self->users_username_column => $username }
383             );
384 156 100       56833 if (!$user) {
385 48         513 $self->plugin->app->log("debug", "No such user $username");
386 48         27622 return;
387             } else {
388 108         584 return $user;
389             }
390             }
391              
392             =head2 get_user_roles $username
393              
394             =cut
395              
396             sub get_user_roles {
397 17     17 1 31047 my ($self, $username) = @_;
398              
399 17         342 my $database = $self->database;
400              
401             # Get details of the user first; both to check they exist, and so we have
402             # their ID to use.
403 17 100       153 my $user = $self->get_user_details($username)
404             or return;
405              
406             # Right, fetch the roles they have. There's currently no support for
407             # JOINs in Dancer2::Plugin::Database, so we'll need to do this query
408             # ourselves - so we'd better take care to quote the table & column names, as
409             # we're going to have to interpolate them. (They're coming from our config,
410             # so should be pretty trustable, but they might conflict with reserved
411             # identifiers or have unacceptable characters to not be quoted.)
412             # Because I've tried to be so flexible in allowing the user to configure
413             # table names, column names, etc, this is going to be fucking ugly.
414             # Seriously ugly. Clear bag of smashed arseholes territory.
415              
416              
417 14         82 my $roles_table = $database->quote_identifier(
418             $self->roles_table
419             );
420 14         422 my $roles_role_id_column = $database->quote_identifier(
421             $self->roles_id_column
422             );
423 14         334 my $roles_role_column = $database->quote_identifier(
424             $self->roles_role_column
425             );
426              
427 14         321 my $user_roles_table = $database->quote_identifier(
428             $self->user_roles_table
429             );
430 14         321 my $user_roles_user_id_column = $database->quote_identifier(
431             $self->user_roles_user_id_column
432             );
433 14         314 my $user_roles_role_id_column = $database->quote_identifier(
434             $self->user_roles_role_id_column
435             );
436              
437             # Yes, there's SQL interpolation here; yes, it makes me throw up a little.
438             # However, all the variables used have been quoted appropriately above, so
439             # although it might look like a camel's arsehole, at least it's safe.
440 14         343 my $sql = <<QUERY;
441             SELECT $roles_table.$roles_role_column
442             FROM $user_roles_table
443             JOIN $roles_table
444             ON $roles_table.$roles_role_id_column
445             = $user_roles_table.$user_roles_role_id_column
446             WHERE $user_roles_table.$user_roles_user_id_column = ?
447             QUERY
448              
449 14 50       71 my $sth = $database->prepare($sql)
450             or croak "Failed to prepare query - error: " . $database->err_str;
451              
452 14         1726 $sth->execute($user->{$self->users_id_column});
453              
454 14         47 my @roles;
455 14         135 while (my($role) = $sth->fetchrow_array) {
456 26         171 push @roles, $role;
457             }
458              
459 14         207 return \@roles;
460              
461             # If you read through this, I'm truly, truly sorry. This mess was the price
462             # of making things so configurable. Send me your address, and I'll send you
463             # a complementary fork to remove your eyeballs with as way of apology.
464             # If I can bear to look at this code again, I think I might seriously
465             # refactor it and use Template::Tiny or something on it. Or Acme::Bleach.
466             }
467              
468             =head2 set_user_details
469              
470             =cut
471              
472             sub set_user_details {
473 16     16 1 77811 my ($self, $username, %update) = @_;
474              
475 16 100       526 croak "Username to update needs to be specified" unless $username;
476              
477 11 50       52 my $user = $self->get_user_details($username) or return;
478              
479 11         291 my $ret = $self->database->quick_update( $self->users_table,
480             { $self->users_username_column => $username }, \%update );
481 11 50       4017 return $ret ? $self->get_user_details($username) : undef;
482             }
483              
484             =head2 set_user_password
485              
486             =cut
487              
488             sub set_user_password {
489 6     6 1 7151 my ( $self, $username, $password ) = @_;
490 6         27 my $encrypted = $self->encrypt_password($password);
491 6         1196 my %update = ( $self->users_password_column => $encrypted );
492 6         28 $self->set_user_details( $username, %update );
493             };
494              
495             =head1 COOKBOOK
496              
497             =head2 Handle locked or disabled user accounts
498              
499             I<(contributed by PerlDuck, Borodin and simbabque
500             L<via Stack Overflow|https://stackoverflow.com/questions/46746864>)>
501              
502             It's a good practice to not delete certain data, like user accounts. But what
503             do you do when you want to get rid of a user? Maybe an employee left or was
504             temporary suspended, or a user did not pay their subscription fee. In those cases you
505             would want the user data to stay around, but they should not be able to log in
506             any more.
507              
508             Let's say there is a column C<disabled> in an already existing user table.
509             It might hold a timestamp for when the user was disabled, and be C<NULL> if the
510             user is active. By default, L<Dancer2::Plugin::Auth::Extensible> will give you this
511             information as part of the user data, but to check if the user is allowed to proceed
512             would happen after the password has been checked and they have already been logged
513             in.
514              
515             The following sections will describe two different ways of implementing this. The
516             first one is easier to implement, but only allows read operations on the user
517             table, while the second one requires a little more effort, but will allow almost
518             all operations to work. If you need even more flexibility you will have to subclass
519             and add a bit more logic.
520              
521             =head3 ... without changing any code
522              
523             An easy way to achieve this is by adding a new view to your database that only
524             shows active users. Let's look at the following example database.
525              
526             -- user table
527             CREATE TABLE users (
528             id INTEGER PRIMARY KEY AUTOINCREMENT,
529             username VARCHAR(32) NOT NULL UNIQUE,
530             password VARCHAR(40) NOT NULL,
531             disabled TIMESTAMP NULL
532             );
533              
534             -- active user view
535             CREATE VIEW active_users (id, username, password) AS
536             SELECT id, username, password FROM users WHERE disabled IS NULL;
537              
538             -- some data
539             INSERT INTO users ( username, password, disabled )
540             VALUES ( 'Alice', 'test', null),
541             ( 'Bob', 'test', '2017-10-01 10:10:10');
542              
543             Now all you need to do is change the L</users_table> setting to point
544             to C<active_users> instead of C<users>.
545              
546             # config.yml
547             plugins:
548             Auth::Extensible:
549             realms:
550             users:
551             provider: 'Database'
552             users_table: 'active_users'
553              
554             That's it. Your application will now only let active users log in, because it
555             has no way of knowing about the others. Only I<Alice> will be able to log in,
556             but I<Bob> has been disabled and the application will not allow him to log in.
557              
558             But be aware that this comes with a few drawbacks. If you want to use
559             L<Dancer2::Plugin::Auth::Extensible> to also update user information, this is
560             now no longer possible because in most database engines you cannot write data
561             into a view.
562              
563             =head3 ... by creating a subclass of this database provider
564              
565             The alternative is to subclass this provider to add a little bit of logic.
566             You can add code to exclude users directly when the user data is fetched, even
567             before L<Dancer2::Plugin::Auth::Extensible> verifies the password. This way,
568             inactive users can easily be discarded.
569              
570             The following code is an example implementation specifically for the user table
571             outlined in the alternative solution above.
572              
573             package Provider::Database::ActiveOnly;
574              
575             use Moo;
576             extends 'Dancer2::Plugin::Auth::Extensible::Provider::Database';
577              
578             around 'get_user_details' => sub {
579             my $orig = shift;
580             my $self = shift;
581              
582             # do nothing if we there was no user
583             my $user = $self->$orig(@_) or return;
584              
585             # do nothing if the user is disabled
586             return if $user->{disabled};
587              
588             return $user;
589             };
590              
591             1;
592              
593             The code uses an L<C<around> modifier from Moo|Moo/around> to influence
594             the L<get_user_details> method, so users that are disabled are never
595             found.
596              
597             To enable this new provider, you need to change the C<provider> setting
598             in your configuration.
599              
600             # config.yml
601             plugins:
602             Auth::Extensible:
603             realms:
604             users:
605             provider: 'Provider::Database::ActiveOnly'
606             users_table: 'users' # this is the default
607              
608             With this custom subclass your application will be able to perform write
609             operations on active users, including making them inactive. However, inactive
610             users will be invisible to L<Dancer2::Plugin::Auth::Extensible>, so you
611             cannot use this to turn inactive users back on.
612              
613             If you want that functionality, you will have to add a bit more logic to
614             your subclass. A possible approach could be to replace the L</authenticate_user>
615             method.
616              
617             =head1 AUTHOR
618              
619             David Precious, C<< <davidp at preshweb.co.uk> >>
620              
621             Dancer2 port of Dancer::Plugin::Auth::Extensible by:
622              
623             Stefan Hornburg (Racke), C<< <racke at linuxia.de> >>
624              
625             Conversion to Dancer2's new plugin system in 2016 by:
626              
627             Peter Mottram (SysPete), C<< <peter at sysnix.com> >>
628              
629             =head1 BUGS / FEATURE REQUESTS
630              
631             This is an early version; there may still be bugs present or features missing.
632              
633             This is developed on GitHub - please feel free to raise issues or pull requests
634             against the repo at:
635             L<https://github.com/PerlDancer/Dancer2-Plugin-Auth-Extensible-Provider-Database>
636              
637             =head1 ACKNOWLEDGEMENTS
638              
639             From L<Dancer2::Plugin::Auth::Extensible>:
640              
641             Valuable feedback on the early design of this module came from many people,
642             including Matt S Trout (mst), David Golden (xdg), Damien Krotkine (dams),
643             Daniel Perrett, and others.
644              
645             Configurable login/logout URLs added by Rene (hertell)
646              
647             Regex support for require_role by chenryn
648              
649             Support for user_roles looking in other realms by Colin Ewen (casao)
650              
651             LDAP provider added by Mark Meyer (ofosos)
652              
653             Documentation fix by Vince Willems.
654              
655             Henk van Oers (GH #8, #13).
656              
657             Andrew Beverly (GH #6, #7, #10, #17, #22, #24, #25, #26).
658             This includes support for creating and editing users and manage user passwords.
659              
660             Gabor Szabo (GH #11, #16, #18).
661              
662             Evan Brown (GH #20, #32).
663              
664             Jason Lewis (Unix provider problem, typo fix).
665              
666             Yanick Champoux (typo fix).
667              
668             =head1 LICENSE AND COPYRIGHT
669              
670             Copyright 2012-16 David Precious.
671             Copyright 2017-19 Stefan Hornburg (Racke).
672              
673             This program is free software; you can redistribute it and/or modify it
674             under the terms of either: the GNU General Public License as published
675             by the Free Software Foundation; or the Artistic License.
676              
677             See http://dev.perl.org/licenses/ for more information.
678              
679             =cut
680              
681             1;