File Coverage

blib/lib/Dancer2/Plugin/Auth/Extensible/Provider/Database.pm
Criterion Covered Total %
statement 50 50 100.0
branch 11 18 61.1
condition 1 3 33.3
subroutine 9 9 100.0
pod 6 6 100.0
total 77 86 89.5


line stmt bran cond sub pod time code
1             package Dancer2::Plugin::Auth::Extensible::Provider::Database;
2              
3 2     2   646162 use Carp;
  2         3  
  2         136  
4 2     2   527 use Moo;
  2         9636  
  2         11  
5             with "Dancer2::Plugin::Auth::Extensible::Role::Provider";
6 2     2   2207 use namespace::clean;
  2         16279  
  2         7  
7              
8             our $VERSION = '0.600';
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 to access a database.
19              
20             L 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 for how to configure a database connection
26             appropriately; see the L section below for how to configure this
27             authentication provider with database details.
28              
29             See L 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 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, 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              
62              
63             A full example showing all options:
64              
65             plugins:
66             Auth::Extensible:
67             realms:
68             users:
69             provider: 'Database'
70             # optionally set DB connection name to use (see named
71             # connections in Dancer2::Plugin::Database docs)
72             db_connection_name: 'foo'
73              
74             # Optionally disable roles support, if you only want to check
75             # for successful logins but don't need to use role-based access:
76             disable_roles: 1
77              
78             # optionally specify names of tables if they're not the defaults
79             # (defaults are 'users', 'roles' and 'user_roles')
80             users_table: 'users'
81             roles_table: 'roles'
82             user_roles_table: 'user_roles'
83              
84             # optionally set the column names (see the SUGGESTED SCHEMA
85             # section below for the default names; if you use them, they'll
86             # Just Work)
87             users_id_column: 'id'
88             users_username_column: 'username'
89             users_password_column: 'password'
90             roles_id_column: 'id'
91             roles_role_column: 'role'
92             user_roles_user_id_column: 'user_id'
93             user_roles_role_id_column: 'roles_id'
94              
95             See the main L documentation for how to
96             configure multiple authentication realms.
97              
98             =head1 SUGGESTED SCHEMA
99              
100             If you use a schema similar to the examples provided here, you should need
101             minimal configuration to get this authentication provider to work for you.
102              
103             The examples given here should be MySQL-compatible; minimal changes should be
104             required to use them with other database engines.
105              
106             =head2 users table
107              
108             You'll need a table to store user accounts in, of course. A suggestion is
109             something like:
110              
111             CREATE TABLE users (
112             id INTEGER AUTO_INCREMENT PRIMARY KEY,
113             username VARCHAR(32) NOT NULL UNIQUE KEY,
114             password VARCHAR(40) NOT NULL
115             );
116              
117             You will quite likely want other fields to store e.g. the user's name, email
118             address, etc; all columns from the users table will be returned by the
119             C keyword for your convenience.
120              
121             =head2 roles table
122              
123             You'll need a table to store a list of available roles in (unless you're not
124             using roles - in which case, disable role support (see the L
125             section).
126              
127             CREATE TABLE roles (
128             id INTEGER AUTO_INCREMENT PRIMARY KEY,
129             role VARCHAR(32) NOT NULL
130             );
131              
132             =head2 user_roles table
133              
134             Finally, (unless you've disabled role support) you'll need a table to store
135             user <-> role mappings (i.e. one row for every role a user has; so adding
136             extra roles to a user consists of adding a new role to this table). It's
137             entirely up to you whether you use an "id" column in this table; you probably
138             shouldn't need it.
139              
140             CREATE TABLE user_roles (
141             user_id INTEGER NOT NULL,
142             role_id INTEGER NOT NULL,
143             UNIQUE KEY user_role (user_id, role_id)
144             );
145              
146             If you're using InnoDB tables rather than the default MyISAM, you could add a
147             foreign key constraint for better data integrity; see the MySQL documentation
148             for details, but a table definition using foreign keys could look like:
149              
150             CREATE TABLE user_roles (
151             user_id INTEGER, FOREIGN KEY (user_id) REFERENCES users (id),
152             role_id INTEGER, FOREIGN_KEY (role_id) REFERENCES roles (id),
153             UNIQUE KEY user_role (user_id, role_id)
154             ) ENGINE=InnoDB;
155              
156             =head1 ATTRIBUTES
157              
158             =head2 dancer2_plugin_database
159              
160             Lazy-loads the correct instance of L which handles
161             the following methods:
162              
163             =over
164              
165             =item * plugin_database
166              
167             This corresponds to the C keyword from L.
168              
169             =back
170              
171             =cut
172              
173             has dancer2_plugin_database => (
174             is => 'ro',
175             lazy => 1,
176             default =>
177             sub { $_[0]->plugin->app->with_plugin('Dancer2::Plugin::Database') },
178             handles => { plugin_database => 'database' },
179             init_arg => undef,
180             );
181              
182             =head2 database
183              
184             The connected L using L.
185              
186             =cut
187              
188             has database => (
189             is => 'ro',
190             lazy => 1,
191             default => sub {
192             my $self = shift;
193             $self->plugin_database($self->db_connection_name);
194             },
195             );
196              
197             =head2 db_connection_name
198              
199             Optional.
200              
201             =cut
202              
203             has db_connection_name => (
204             is => 'ro',
205             );
206              
207             =head2 users_table
208              
209             Defaults to 'users'.
210              
211             =cut
212              
213             has users_table => (
214             is => 'ro',
215             default => 'users',
216             );
217              
218             =head2 users_id_column
219              
220             Defaults to 'id'.
221              
222             =cut
223              
224             has users_id_column => (
225             is => 'ro',
226             default => 'id',
227             );
228              
229             =head2 users_username_column
230              
231             Defaults to 'username'.
232              
233             =cut
234              
235             has users_username_column => (
236             is => 'ro',
237             default => 'username',
238             );
239              
240             =head2 users_password_column
241              
242             Defaults to 'password'.
243              
244             =cut
245              
246             has users_password_column => (
247             is => 'ro',
248             default => 'password',
249             );
250              
251             =head2 roles_table
252              
253             Defaults to 'roles'.
254              
255             =cut
256              
257             has roles_table => (
258             is => 'ro',
259             default => 'roles',
260             );
261              
262             =head2 roles_id_column
263              
264             Defaults to 'id'.
265              
266             =cut
267              
268             has roles_id_column => (
269             is => 'ro',
270             default => 'id',
271             );
272              
273             =head2 roles_role_column
274              
275             Defaults to 'role'.
276              
277             =cut
278              
279             has roles_role_column => (
280             is => 'ro',
281             default => 'role',
282             );
283              
284             =head2 user_roles_table
285              
286             Defaults to 'user_roles'.
287              
288             =cut
289              
290             has user_roles_table => (
291             is => 'ro',
292             default => 'user_roles',
293             );
294              
295             =head2 user_roles_user_id_column
296              
297             Defaults to 'user_id'.
298              
299             =cut
300              
301             has user_roles_user_id_column => (
302             is => 'ro',
303             default => 'user_id',
304             );
305              
306             =head2 user_roles_role_id_column
307              
308             Defaults to 'role_id'.
309              
310             =cut
311              
312             has user_roles_role_id_column => (
313             is => 'ro',
314             default => 'role_id',
315             );
316              
317             =head1 METHODS
318              
319             =head2 authenticate_user $username, $password
320              
321             =cut
322              
323             sub authenticate_user {
324 23     23 1 365862 my ($self, $username, $password) = @_;
325              
326             # Look up the user:
327 23         85 my $user = $self->get_user_details($username);
328 23 100       79 return unless $user;
329              
330             # OK, we found a user, let match_password (from our base class) take care of
331             # working out if the password is correct
332              
333 14         80 my $correct = $user->{ $self->users_password_column };
334              
335             # do NOT authenticate when password is empty/undef
336 14 50 33     125 return undef unless ( defined $correct && $correct ne '' );
337              
338 14         84 return $self->match_password( $password, $correct );
339             }
340              
341             =head2 create_user
342              
343             =cut
344              
345             sub create_user {
346 2     2 1 18 my ( $self, %options ) = @_;
347              
348             # Prevent attempt to update wrong key
349             my $username = delete $options{username}
350 2 50       32 or croak "username needs to be specified for create_user";
351              
352             # password column might not be nullable so set to empty since we fail
353             # auth attempts for empty passwords anyway
354 2         38 $self->database->quick_insert( $self->users_table,
355             { $self->users_username_column => $username, password => '', %options }
356             );
357             }
358              
359             =head2 get_user_details $username
360              
361             =cut
362              
363             # Return details about the user. The user's row in the users table will be
364             # fetched and all columns returned as a hashref.
365             sub get_user_details {
366 62     62 1 181859 my ($self, $username) = @_;
367 62 50       244 return unless defined $username;
368              
369             # Get our database handle and find out the table and column names:
370 62         1218 my $database = $self->database;
371              
372             # Look up the user,
373 62         1325 my $user = $database->quick_select(
374             $self->users_table, { $self->users_username_column => $username }
375             );
376 62 100       21668 if (!$user) {
377 11         115 $self->plugin->app->log("debug", "No such user $username");
378 11         5133 return;
379             } else {
380 51         186 return $user;
381             }
382             }
383              
384             =head2 get_user_roles $username
385              
386             =cut
387              
388             sub get_user_roles {
389 13     13 1 28805 my ($self, $username) = @_;
390              
391 13         330 my $database = $self->database;
392              
393             # Get details of the user first; both to check they exist, and so we have
394             # their ID to use.
395 13 50       117 my $user = $self->get_user_details($username)
396             or return;
397              
398             # Right, fetch the roles they have. There's currently no support for
399             # JOINs in Dancer2::Plugin::Database, so we'll need to do this query
400             # ourselves - so we'd better take care to quote the table & column names, as
401             # we're going to have to interpolate them. (They're coming from our config,
402             # so should be pretty trustable, but they might conflict with reserved
403             # identifiers or have unacceptable characters to not be quoted.)
404             # Because I've tried to be so flexible in allowing the user to configure
405             # table names, column names, etc, this is going to be fucking ugly.
406             # Seriously ugly. Clear bag of smashed arseholes territory.
407              
408              
409 13         91 my $roles_table = $database->quote_identifier(
410             $self->roles_table
411             );
412 13         316 my $roles_role_id_column = $database->quote_identifier(
413             $self->roles_id_column
414             );
415 13         252 my $roles_role_column = $database->quote_identifier(
416             $self->roles_role_column
417             );
418              
419 13         240 my $user_roles_table = $database->quote_identifier(
420             $self->user_roles_table
421             );
422 13         234 my $user_roles_user_id_column = $database->quote_identifier(
423             $self->user_roles_user_id_column
424             );
425 13         231 my $user_roles_role_id_column = $database->quote_identifier(
426             $self->user_roles_role_id_column
427             );
428              
429             # Yes, there's SQL interpolation here; yes, it makes me throw up a little.
430             # However, all the variables used have been quoted appropriately above, so
431             # although it might look like a camel's arsehole, at least it's safe.
432 13         270 my $sql = <
433             SELECT $roles_table.$roles_role_column
434             FROM $user_roles_table
435             JOIN $roles_table
436             ON $roles_table.$roles_role_id_column
437             = $user_roles_table.$user_roles_role_id_column
438             WHERE $user_roles_table.$user_roles_user_id_column = ?
439             QUERY
440              
441 13 50       70 my $sth = $database->prepare($sql)
442             or croak "Failed to prepare query - error: " . $database->err_str;
443              
444 13         1381 $sth->execute($user->{$self->users_id_column});
445              
446 13         27 my @roles;
447 13         169 while (my($role) = $sth->fetchrow_array) {
448 24         152 push @roles, $role;
449             }
450              
451 13         208 return \@roles;
452              
453             # If you read through this, I'm truly, truly sorry. This mess was the price
454             # of making things so configurable. Send me your address, and I'll send you
455             # a complementary fork to remove your eyeballs with as way of apology.
456             # If I can bear to look at this code again, I think I might seriously
457             # refactor it and use Template::Tiny or something on it. Or Acme::Bleach.
458             }
459              
460             =head2 set_user_details
461              
462             =cut
463              
464             sub set_user_details {
465 4     4 1 25885 my ($self, $username, %update) = @_;
466              
467 4 50       17 croak "Username to update needs to be specified" unless $username;
468              
469 4 50       15 my $user = $self->get_user_details($username) or return;
470              
471 4         102 $self->database->quick_update( $self->users_table,
472             { $self->users_username_column => $username }, \%update );
473             }
474              
475             =head2 set_user_password
476              
477             =cut
478              
479             sub set_user_password {
480 2     2 1 598 my ( $self, $username, $password ) = @_;
481 2         13 my $encrypted = $self->encrypt_password($password);
482 2         376 my %update = ( $self->users_password_column => $encrypted );
483 2         58 $self->set_user_details( $username, %update );
484             };
485              
486             =head1 AUTHOR
487              
488             David Precious, C<< >>
489              
490             Dancer2 port of Dancer::Plugin::Auth::Extensible by:
491              
492             Stefan Hornburg (Racke), C<< >>
493              
494             Conversion to Dancer2's new plugin system in 2016 by:
495              
496             Peter Mottram (SysPete), C<< >>
497              
498             =head1 BUGS / FEATURE REQUESTS
499              
500             This is an early version; there may still be bugs present or features missing.
501              
502             This is developed on GitHub - please feel free to raise issues or pull requests
503             against the repo at:
504             L
505              
506             =head1 ACKNOWLEDGEMENTS
507              
508             From L:
509              
510             Valuable feedback on the early design of this module came from many people,
511             including Matt S Trout (mst), David Golden (xdg), Damien Krotkine (dams),
512             Daniel Perrett, and others.
513              
514             Configurable login/logout URLs added by Rene (hertell)
515              
516             Regex support for require_role by chenryn
517              
518             Support for user_roles looking in other realms by Colin Ewen (casao)
519              
520             LDAP provider added by Mark Meyer (ofosos)
521              
522             Documentation fix by Vince Willems.
523              
524             Henk van Oers (GH #8, #13).
525              
526             Andrew Beverly (GH #6, #7, #10, #17, #22, #24, #25, #26).
527             This includes support for creating and editing users and manage user passwords.
528              
529             Gabor Szabo (GH #11, #16, #18).
530              
531             Evan Brown (GH #20, #32).
532              
533             Jason Lewis (Unix provider problem).
534              
535             =head1 LICENSE AND COPYRIGHT
536              
537             Copyright 2012-16 David Precious.
538              
539             This program is free software; you can redistribute it and/or modify it
540             under the terms of either: the GNU General Public License as published
541             by the Free Software Foundation; or the Artistic License.
542              
543             See http://dev.perl.org/licenses/ for more information.
544              
545             =cut
546              
547             1;