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   995972 use Carp;
  2         5  
  2         167  
4 2     2   971 use Moo;
  2         12935  
  2         13  
5             with "Dancer2::Plugin::Auth::Extensible::Role::Provider";
6 2     2   2145 use namespace::clean;
  2         9952  
  2         13  
7              
8             our $VERSION = '0.620';
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 1189828 my ($self, $username, $password) = @_;
329 61 100 100     823 croak "Both of username and password must be defined"
330             unless defined $username && defined $password;
331              
332             # Look up the user:
333 58         190 my $user = $self->get_user_details($username);
334 58 100       231 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         94 my $correct = $user->{ $self->users_password_column };
340              
341             # do NOT authenticate when password is empty/undef
342 21 50 33     142 return undef unless ( defined $correct && $correct ne '' );
343              
344 21         130 return $self->match_password( $password, $correct );
345             }
346              
347             =head2 create_user
348              
349             =cut
350              
351             sub create_user {
352 12     12 1 136969 my ( $self, %options ) = @_;
353              
354             # Prevent attempt to update wrong key
355             my $username = delete $options{username}
356 12 100       262 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         259 my $ret = $self->database->quick_insert( $self->users_table,
361             { $self->users_username_column => $username, password => '', %options }
362             );
363 7 50       2839 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 601910 my ($self, $username) = @_;
374 158 100       757 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         3224 my $database = $self->database;
379              
380             # Look up the user,
381 156         3498 my $user = $database->quick_select(
382             $self->users_table, { $self->users_username_column => $username }
383             );
384 156 100       50364 if (!$user) {
385 48         412 $self->plugin->app->log("debug", "No such user $username");
386 48         24711 return;
387             } else {
388 108         459 return $user;
389             }
390             }
391              
392             =head2 get_user_roles $username
393              
394             =cut
395              
396             sub get_user_roles {
397 17     17 1 29579 my ($self, $username) = @_;
398              
399 17         403 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       168 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         108 my $roles_table = $database->quote_identifier(
418             $self->roles_table
419             );
420 14         384 my $roles_role_id_column = $database->quote_identifier(
421             $self->roles_id_column
422             );
423 14         302 my $roles_role_column = $database->quote_identifier(
424             $self->roles_role_column
425             );
426              
427 14         287 my $user_roles_table = $database->quote_identifier(
428             $self->user_roles_table
429             );
430 14         324 my $user_roles_user_id_column = $database->quote_identifier(
431             $self->user_roles_user_id_column
432             );
433 14         282 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         324 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       80 my $sth = $database->prepare($sql)
450             or croak "Failed to prepare query - error: " . $database->err_str;
451              
452 14         1636 $sth->execute($user->{$self->users_id_column});
453              
454 14         31 my @roles;
455 14         206 while (my($role) = $sth->fetchrow_array) {
456 26         212 push @roles, $role;
457             }
458              
459 14         248 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 67162 my ($self, $username, %update) = @_;
474              
475 16 100       558 croak "Username to update needs to be specified" unless $username;
476              
477 11 50       36 my $user = $self->get_user_details($username) or return;
478              
479 11         299 my $ret = $self->database->quick_update( $self->users_table,
480             { $self->users_username_column => $username }, \%update );
481 11 50       3210 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 4820 my ( $self, $username, $password ) = @_;
490 6         40 my $encrypted = $self->encrypt_password($password);
491 6         1036 my %update = ( $self->users_password_column => $encrypted );
492 6         24 $self->set_user_details( $username, %update );
493             };
494              
495             =head1 AUTHOR
496              
497             David Precious, C<< <davidp at preshweb.co.uk> >>
498              
499             Dancer2 port of Dancer::Plugin::Auth::Extensible by:
500              
501             Stefan Hornburg (Racke), C<< <racke at linuxia.de> >>
502              
503             Conversion to Dancer2's new plugin system in 2016 by:
504              
505             Peter Mottram (SysPete), C<< <peter at sysnix.com> >>
506              
507             =head1 BUGS / FEATURE REQUESTS
508              
509             This is an early version; there may still be bugs present or features missing.
510              
511             This is developed on GitHub - please feel free to raise issues or pull requests
512             against the repo at:
513             L<https://github.com/PerlDancer/Dancer2-Plugin-Auth-Extensible-Provider-Database>
514              
515             =head1 ACKNOWLEDGEMENTS
516              
517             From L<Dancer2::Plugin::Auth::Extensible>:
518              
519             Valuable feedback on the early design of this module came from many people,
520             including Matt S Trout (mst), David Golden (xdg), Damien Krotkine (dams),
521             Daniel Perrett, and others.
522              
523             Configurable login/logout URLs added by Rene (hertell)
524              
525             Regex support for require_role by chenryn
526              
527             Support for user_roles looking in other realms by Colin Ewen (casao)
528              
529             LDAP provider added by Mark Meyer (ofosos)
530              
531             Documentation fix by Vince Willems.
532              
533             Henk van Oers (GH #8, #13).
534              
535             Andrew Beverly (GH #6, #7, #10, #17, #22, #24, #25, #26).
536             This includes support for creating and editing users and manage user passwords.
537              
538             Gabor Szabo (GH #11, #16, #18).
539              
540             Evan Brown (GH #20, #32).
541              
542             Jason Lewis (Unix provider problem).
543              
544             =head1 LICENSE AND COPYRIGHT
545              
546             Copyright 2012-16 David Precious.
547              
548             This program is free software; you can redistribute it and/or modify it
549             under the terms of either: the GNU General Public License as published
550             by the Free Software Foundation; or the Artistic License.
551              
552             See http://dev.perl.org/licenses/ for more information.
553              
554             =cut
555              
556             1;