File Coverage

blib/lib/Dancer2/Plugin/Auth/Extensible/Provider/Usergroup.pm
Criterion Covered Total %
statement 38 40 95.0
branch 15 18 83.3
condition 3 3 100.0
subroutine 7 7 100.0
pod 4 4 100.0
total 67 72 93.0


line stmt bran cond sub pod time code
1             package Dancer2::Plugin::Auth::Extensible::Provider::Usergroup;
2              
3 3     3   1517468 use Carp;
  3         14  
  3         181  
4 3     3   1719 use Moo;
  3         17296  
  3         15  
5             with "Dancer2::Plugin::Auth::Extensible::Role::Provider";
6 3     3   3797 use namespace::clean;
  3         20930  
  3         21  
7              
8             our $VERSION = '0.709';
9              
10             =head1 NAME
11              
12             Dancer2::Plugin::Auth::Extensible::Provider::Usergroup - authenticate as a member of a group
13              
14             =head1 SYNOPSIS
15              
16             Define that a user must be logged in and have the proper permissions to
17             access a route:
18              
19             get '/unsubscribe' => require_role forum => sub { ... };
20              
21              
22             =head1 DESCRIPTION
23              
24             This class is an authentication provider designed to authenticate users against
25             a DBIC schema, using L<Dancer2::Plugin::DBIC> to access a database.
26              
27             L<Dancer2::Plugin::Passphrase> is used to handle hashed passwords securely; you wouldn't
28             want to store plain text passwords now, would you? (If your answer to that is
29             yes, please reconsider; you really don't want to do that, when it's so easy to
30             do things right!)
31              
32             See L<Dancer2::Plugin::DBIC> for how to configure a database connection
33             appropriately; see the L</CONFIGURATION> section below for how to configure this
34             authentication provider with database details.
35              
36             See L<Dancer2::Plugin::Auth::Extensible> for details on how to use the
37             authentication framework, including how to use "require_login" and "require_role".
38              
39              
40             =head1 CONFIGURATION
41              
42             This provider tries to use sensible defaults, so you may not need to provide
43             much configuration if your database tables look similar to those in the
44             L</SUGGESTED SCHEMA> section below.
45              
46             The most basic configuration, assuming defaults for all options, and defining a
47             single authentication realm named 'usergroup':
48              
49             plugins:
50             Auth::Extensible:
51             realms:
52             usergroup:
53             provider: 'Usergroup'
54              
55             You would still need to have provided suitable database connection details to
56             L<Dancer2::Plugin::DBIC>, of course; see the docs for that plugin for full
57             details, but it could be as simple as, e.g.:
58              
59             plugins:
60             Auth::Extensible:
61             realms:
62             usergroup:
63             provider: 'Usergroup'
64             schema_name: 'usergroup'
65             DBIC:
66             usergroup:
67             chema_class: Usergroup::Schema
68             dsn: "dbi:SQLite:dbname=/path/to/usergroup.db"
69              
70              
71             A full example showing all options:
72              
73             plugins:
74             Auth::Extensible:
75             realms:
76             usergroup:
77             provider: 'Usergroup'
78            
79             # optional schema name for DBIC (default 'default')
80             schema_name: 'usergroup'
81              
82             # optionally specify names of result sets if they're not the defaults
83             # (defaults are 'User' and 'Role')
84             user_rset: 'User'
85             user_role_rset: 'Role'
86              
87             # optionally set the column names (see the SUGGESTED SCHEMA
88             # section below for the default names; if you use them, they'll
89             # Just Work)
90             user_login_name_column: 'login_name'
91             user_passphrase_column: 'passphrase'
92             user_role_column: 'role'
93            
94             # optionally set a column name that makes a user useable
95             # (not all login names can be used to login)
96             user_activated_column: 'activated'
97              
98             See the main L<Dancer2::Plugin::Auth::Extensible> documentation for how to
99             configure multiple authentication realms.
100              
101             =head1 ATTRIBUTES
102              
103             =cut
104              
105             has dancer2_plugin_dbic => (
106             is => 'ro',
107             lazy => 1,
108             default => sub { $_[0]->plugin->app->with_plugin('Dancer2::Plugin::DBIC') },
109             handles => { dbic_schema => 'schema' },
110             init_arg => undef,
111             );
112              
113             has dancer2_plugin_passphrase => (
114             is => 'ro',
115             lazy => 1,
116             default =>
117             sub { $_[0]->plugin->app->with_plugin('Dancer2::Plugin::Passphrase') },
118             handles => ['passphrase'],
119             init_arg => undef,
120             );
121              
122             =head2 schema_name
123              
124             Defaults to 'default',
125              
126             =cut
127              
128             has schema_name => (
129             is => 'ro',
130             );
131              
132             =head2 schema
133              
134             Defaults to a DBIC schema using L</schema_name>.
135              
136             =cut
137              
138             has schema => (
139             is => 'ro',
140             lazy => 1,
141             default => sub {
142             my $self = shift;
143             $self->schema_name
144             ? $self->dbic_schema( $self->schema_name )
145             : $self->dbic_schema;
146             },
147             );
148              
149             =head2 user_rset
150              
151             The name of the DBIC result class for the user table.
152              
153             Defaults to 'User'.
154              
155             =cut
156              
157             has user_rset => (
158             is => 'ro',
159             default => 'User',
160             );
161              
162             =head2 user_role_rset
163              
164             The name of the DBIC result class for the role view.
165              
166             Defaults to 'Role'.
167              
168             =cut
169              
170             has user_role_rset => (
171             is => 'ro',
172             default => 'Role',
173             );
174              
175             =head2 user_login_name_column
176              
177             The login_name column in L</user_rset>.
178              
179             Defaults to 'login_name'.
180              
181             =cut
182              
183             has user_login_name_column => (
184             is => 'ro',
185             default => 'login_name',
186             );
187              
188             =head2 user_passphrase_column
189              
190             The passphrase column in L</user_rset>.
191              
192             Defaults to 'passphrase'.
193              
194             =cut
195              
196             has user_passphrase_column => (
197             is => 'ro',
198             default => 'passphrase',
199             );
200              
201             =head2 user_role_column
202              
203             The role column in L</user_role_rset>.
204              
205             Defaults to 'role'.
206              
207             =cut
208              
209             has user_role_column => (
210             is => 'ro',
211             default => 'role',
212             );
213              
214             =head2 user_activated_column
215              
216             The user activated column in L</user_rset>.
217              
218             Defaults to 'activated'.
219              
220             =cut
221              
222             has user_activated_column => (
223             is => 'ro',
224             default => 'activated',
225             );
226              
227             =head1 SUGGESTED SCHEMA
228              
229             If you use a schema similar to the examples provided here, you should need
230             minimal configuration to get this authentication provider to work for you.
231              
232             The examples given here should be SQLite-compatible; minimal changes should be
233             required to use them with other database engines.
234              
235             =head2 user table
236              
237             You'll need a table to store user accounts in, of course. A suggestion is
238             something like:
239              
240             CREATE TABLE users (
241             id INTEGER PRIMARY KEY,
242             login_name TEXT UNIQUE NOT NULL,
243             passphrase TEXT NOT NULL,
244             activated INTEGER
245             );
246              
247             You will quite likely want other fields to store e.g. the user's name, email
248             address, etc; all columns from the users table will be returned by the
249             C<logged_in_user> keyword for your convenience.
250              
251             =head2 group table
252              
253             You'll need a table to store a list of available groups in.
254              
255             CREATE TABLE groups (
256             id INTEGER PRIMARY KEY,
257             group_name TEXT UNIQUE NOT NULL
258             );
259              
260             =head2 membership table
261              
262             To make users a member you'll need a table to store
263             user <-> group mappings.
264              
265             CREATE TABLE memberships (
266             id INTEGER PRIMARY KEY,
267             user_id INTEGER NOT NULL REFERENCES users (id),
268             group_id INTEGER NOT NULL REFERENCES groups (id)
269             );
270              
271             =head2 role view
272              
273             Map the user role by name.
274              
275             CREATE VIEW roles AS
276             SELECT login_name, group_name AS role
277             FROM users
278             LEFT JOIN memberships ON users.id = memberships.user_id
279             LEFT JOIN groups ON groups.id = memberships.group_id
280             ;
281              
282             =head2 indexes
283              
284             You want your data quickly.
285              
286             CREATE UNIQUE INDEX login_name ON users (login_name);
287             CREATE UNIQUE INDEX group_name ON groups (group_name);
288             CREATE UNIQUE INDEX user_group ON memberships (user_id, group_id);
289             CREATE INDEX member_user ON memberships (user_id);
290             CREATE INDEX member_group ON memberships (group_id);
291              
292             =head1 INTERNALS
293              
294             =head4 get_user_details
295              
296             Used by L<Dancer2::Plugin::Auth::Extensible>
297              
298             =cut
299              
300             sub get_user_details {
301 99     99 1 289839 my ($self, $login_name) = @_;
302 99 100       511 croak "username must be defined" unless defined $login_name;
303              
304             # Look up the user
305 97         1485 my $user_rset = $self->schema->resultset($self->user_rset)
306             ->search({ $self->user_login_name_column => $login_name });
307            
308 97         46570 my $user_row;
309 97 100       1748 unless ($user_row = $user_rset->next) {
310 45         92889 $self->plugin->app->log("debug", "No such user $login_name");
311 45         21796 return;
312             }
313              
314 52         87671 my %user = $user_row->get_columns;
315            
316             # Get the roles, if any
317 52         1531 my @roles = $self->schema->resultset($self->user_role_rset)
318             ->search({ $self->user_login_name_column => $login_name })
319             ->get_column($self->user_role_column)
320             ->all;
321            
322 52         130057 $user{roles} = \@roles;
323              
324 52         3752 return \%user;
325             }
326              
327             =head4 match_password
328              
329             Used by L<Dancer2::Plugin::Auth::Extensible>
330              
331             =cut
332              
333              
334             sub match_password {
335 14     14 1 41 my ($self, $given, $correct) = @_;
336            
337 14 50       67 return unless defined $correct;
338 14 100       75 if ($correct =~ /^\{.+}/) {
339             # Looks like a crypted password
340 13         211 return $self->passphrase($given)->matches($correct);
341             }
342            
343             #not crypted?
344 1         9 $self->plugin->app->log("debug", "Passphrase $correct not crypted");
345 1         411 return $given eq $correct;
346             }
347              
348             =head4 authenticate_user
349              
350             Used by L<Dancer2::Plugin::Auth::Extensible>
351              
352             =cut
353              
354             sub authenticate_user {
355 52     52 1 1424368 my ($self, $username, $password) = @_;
356 52 100 100     595 croak "username and password must be defined"
357             unless defined $username && defined $password;
358              
359             # Look up the user:
360 49         189 my $user = $self->get_user_details($username);
361 49 100       4477 return unless $user;
362 14         151 $self->plugin->app->log("debug", "User $username found");
363              
364 14         6482 my $must_be_activated = $self->user_activated_column;
365 14 50       59 if ($must_be_activated) {
366 14 50       50 unless ($user->{$must_be_activated}) {
367 0         0 $self->plugin->app->log("debug", "User $username not activated");
368 0         0 return;
369             }
370             }
371              
372             # OK, we found a user, let match_password take care of
373             # working out if the password is correct
374              
375             return $self->match_password( $password,
376 14         77 $user->{ $self->user_passphrase_column } );
377             }
378              
379             =head4 get_user_roles
380              
381             Used by L<Dancer2::Plugin::Auth::Extensible>
382              
383             =cut
384              
385             sub get_user_roles {
386 17     17 1 26522 my ($self, $login_name) = @_;
387              
388             # Get details of the user, including the roles
389 17 100       42 my $user = $self->get_user_details($login_name)
390             or return;
391              
392 14         1532 return $user->{roles};
393              
394             }
395              
396             =head1 COPYRIGHT
397              
398             Copyright (c) 2014 Henk van Oers
399              
400             =head1 LICENSE
401              
402             This library is free software and may be distributed under the same terms
403             as perl itself.
404              
405             =cut
406              
407             1;