File Coverage

blib/lib/PlugAuth/Plugin/FlatAuth.pm
Criterion Covered Total %
statement 120 126 95.2
branch 31 36 86.1
condition 18 22 81.8
subroutine 21 21 100.0
pod 6 7 85.7
total 196 212 92.4


line stmt bran cond sub pod time code
1             package PlugAuth::Plugin::FlatAuth;
2              
3             # ABSTRACT: Authentication using Flat Files for PlugAuth
4             our $VERSION = '0.38'; # VERSION
5              
6              
7 40     40   21758 use strict;
  40         117  
  40         1473  
8 40     40   303 use warnings;
  40         110  
  40         1461  
9 40     40   964 use 5.010001;
  40         262  
10 40     40   279 use Log::Log4perl qw( :easy );
  40         104  
  40         460  
11             # TODO: maybe optionally use Crypt::Passwd::XS instead
12 40     40   51128 use Crypt::PasswdMD5 qw( unix_md5_crypt apache_md5_crypt );
  40         60488  
  40         3158  
13 40     40   3357 use Role::Tiny::With;
  40         2318  
  40         33718  
14              
15             with 'PlugAuth::Role::Plugin';
16             with 'PlugAuth::Role::Auth';
17             with 'PlugAuth::Role::Refresh';
18             with 'PlugAuth::Role::Flat';
19              
20             our %Userpw; # Keys are usernames, values are lists of crypted passwords.
21              
22             sub init {
23 44     44 0 267 shift->flat_init;
24             }
25              
26              
27             sub refresh {
28             # Should be called with every request.
29 683     683 1 6608 my $config = __PACKAGE__->global_config;
30 683         5013 my @user_files = $config->user_file;
31 683 100       20005 if ( grep has_changed($_), @user_files )
32             {
33 63         627 my @users = map +{ __PACKAGE__->read_file($_, lc_keys => 1) }, @user_files;
34 63         393 %Userpw = ();
35 63         226 for my $list (@users)
36             {
37 72         350 for my $user (map { lc $_ } keys %$list)
  208         746  
38             {
39 208   100     1203 $Userpw{$user} //= [];
40 208         378 push @{ $Userpw{$user} }, $list->{$user};
  208         863  
41             }
42             }
43              
44             # if the user file has changed, then that may mean the
45             # group file has to be reloaded, for example, for groups
46             # with wildcards * need to be updated.
47 63         505 mark_changed($config->group_file);
48             }
49             }
50              
51              
52             sub _validate_pw
53             {
54 146     146   525 my($plain, $encrypted) = @_;
55 146 100       334 return 1 if do {
56             # crypt on an apache apr1 encrypted
57             # password seems to return undef
58             # on Debian 8 (probably others)
59 146         16679 my $ret = crypt($plain, $encrypted);
60 146 100       1916 (defined $ret) && ($ret eq $encrypted);
61             };
62            
63             # idea borrowed from Authen::Simple::Password
64 54 100       459 if($encrypted =~ /^\$(\w+)\$/)
65             {
66 38 100 66     470 return 1 if $1 eq 'apr1' && apache_md5_crypt( $plain, $encrypted ) eq $encrypted;
67              
68             # on at least modern Linux crypt will accept a UNIX
69             # MD5 password, so this may be redundant
70 9 50 33     173637 return 1 if $1 eq '1' && unix_md5_crypt ( $plain, $encrypted ) eq $encrypted;
71             }
72 25         190 return 0;
73             }
74              
75             sub check_credentials {
76 159     159 1 803 my ($self, $user,$pw) = @_;
77 159         591 $user = lc $user;
78              
79 159 100 100     1393 if($pw && $Userpw{$user})
80             {
81 138 100       374 return 1 if grep { _validate_pw($pw, $_) } @{ $Userpw{$user} };
  146         554  
  138         547  
82             }
83 38         324 return $self->deligate_check_credentials($user, $pw);
84             }
85              
86              
87             sub all_users {
88             return sort keys %Userpw;
89             }
90              
91              
92             sub _created_encrypted_password
93             {
94 31     31   227 my($plain) = @_;
95 31         314 my $salt = join '', ('.', '/', 0..9, 'A'..'Z', 'a'..'z')[rand 64, rand 64];
96 31         203 apache_md5_crypt($plain, $salt);
97             }
98              
99 23     23 1 159 sub create_user { goto &create_user_cb }
100              
101             sub create_user_cb
102             {
103 26     26 1 129 my($self, $user, $password, $cb) = @_;
104              
105 26 100 100     275 unless($user && $password)
106             {
107 2         10 WARN "User or password not provided";
108 2         2585 return 0;
109             }
110              
111 24         111 $user = lc $user;
112              
113 24 50       145 if(defined $Userpw{$user})
114             {
115 0         0 WARN "User $user already exists";
116 0         0 return 0;
117             }
118              
119 24         189 foreach my $filename ($self->global_config->user_file)
120             {
121 24 50       1492 next unless -w $filename;
122              
123 24         150 $password = _created_encrypted_password($password);
124              
125             my $ok = $self->lock_and_update_file($filename, sub {
126 40     40   400 use autodie;
  40         227  
  40         438  
127 24     24   97 my($fh) = @_;
128              
129 24         89 my $buffer = '';
130 24         461 while(! eof $fh)
131             {
132 47         142 my $line = <$fh>;
133 47         98 chomp $line;
134 47         197 $buffer .= "$line\n";
135             }
136 24         176 $buffer .= join(':', $user, $password) . "\n";
137            
138             # as a rule we don't update the data structure
139             # directly, we update the config files and let
140             # refresh do that on the next request, but in
141             # this case the callback is used to modify groups,
142             # and for that to work we need to update the
143             # userdatabase first.
144 24         123 $Userpw{$user} = $password;
145 24 100       123 $cb->() if defined $cb;
146            
147 24         466 $buffer;
148 24         460839 });
149              
150 24 50       315 return 0 unless $ok;
151              
152 24         234 INFO "created user $user";
153 24         41224 return 1;
154             }
155              
156 0         0 ERROR "None of the user files were writable";
157 0         0 return 0;
158             }
159              
160              
161             sub change_password
162             {
163 9     9 1 45 my($self, $user, $password) = @_;
164              
165 9 100 66     86 unless($user && $password)
166             {
167 1         8 WARN "User or password not provided";
168 1         1672 return 0;
169             }
170              
171 8         55 $user = lc $user;
172              
173 8 100       51 unless(defined $Userpw{$user})
174             {
175 1         12 WARN "User $user does not exist";
176 1         1837 return 0;
177             }
178              
179 7         38 $password = _created_encrypted_password($password);
180              
181 7         145456 foreach my $filename ($self->global_config->user_file)
182             {
183             $self->lock_and_update_file($filename, sub {
184 40     40   343708 use autodie;
  40         132  
  40         308  
185 7     7   29 my($fh) = @_;
186              
187 7         25 my $buffer = '';
188            
189 7         116 while(! eof $fh)
190             {
191 22         79 my $line = <$fh>;
192 22         52 chomp $line;
193 22         85 my($thisuser, $oldpassword) = split /:/, $line;
194 22 100 100     143 if(defined $thisuser && lc($thisuser) eq $user)
195             {
196 7         69 $buffer .= join(':', $user, $password) . "\n";
197             }
198             else
199             {
200 15         92 $buffer .= "$line\n";
201             }
202             }
203            
204 7         43 $buffer;
205 7         301 });
206             }
207              
208 7         77 INFO "user password changed $user";
209 7         11784 return 1;
210             }
211              
212              
213             sub delete_user
214             {
215 6     6 1 29 my($self, $user) = @_;
216              
217 6         35 $user = lc $user;
218              
219 6 50       44 unless(defined $Userpw{$user})
220             {
221 0         0 WARN "User $user does not exist";
222 0         0 return 0;
223             }
224              
225 6         43 foreach my $filename ($self->global_config->user_file)
226             {
227             $self->lock_and_update_file($filename, sub {
228 40     40   331825 use autodie;
  40         126  
  40         307  
229 8     8   32 my($fh) = @_;
230              
231 8         31 my $buffer = '';
232 8         106 while(! eof $fh)
233             {
234 63         152 my $line = <$fh>;
235 63         114 chomp $line;
236 63         184 my($thisuser, $password) = split /:/, $line;
237 63 100 100     229 next if ($thisuser//'') eq $user;
238 57         211 $buffer .= "$line\n";
239             }
240 8         39 $buffer;
241 8         225 });
242             }
243              
244 6         61 INFO "deleted user $user";
245 6         9630 return 1;
246             }
247              
248             1;
249              
250             __END__
251              
252             =pod
253              
254             =encoding UTF-8
255              
256             =head1 NAME
257              
258             PlugAuth::Plugin::FlatAuth - Authentication using Flat Files for PlugAuth
259              
260             =head1 VERSION
261              
262             version 0.38
263              
264             =head1 SYNOPSIS
265              
266             In your PlugAuth.conf file:
267              
268             ---
269             url: http://localhost:1234
270             user_file: /path/to/user.txt
271              
272             Touch the user file:
273              
274             % touch /path/to/user.txt
275              
276             Add users using htpasswd (comes with Apache):
277              
278             % htpasswd -m /path/to/user.txt newusername
279             New password:
280             Re-type new password:
281              
282             Start PlugAuth:
283              
284             % plugauth start
285              
286             =head1 DESCRIPTION
287              
288             This is the default Authentication plugin for L<PlugAuth>. It is designed to work closely
289             with L<PlugAuth::Plugin::FlatAuthz> which is the default Authorization plugin.
290              
291             This plugin provides storage and password verification for users. This plugin also provides
292             a mechanism for PlugAuth to change passwords, create and delete users. Although the user
293             information is stored in flat files, the entire user database is kept in memory and the
294             files are only re-read when a change is detected, so this plugin is relatively fast.
295              
296             =head1 CONFIGURATION
297              
298             =head2 user_file
299              
300             The user file is
301             specified in the PlugAuth.conf file using the user_file field. The format of the user
302             is a basic user:password comma separated list, which is compatible with Apache password
303             files. Either the UNIX crypt, Apache MD5 or UNIX MD5 format may be used for the passwords.
304              
305             foo:$apr1$F3VOmjio$O8dodh0VEljQvuzeruvsb0
306             bar:yOJEfNAE.gppk
307              
308             It is possible to have multiple user files if you specify a list:
309              
310             ---
311             user_file:
312             - /path/to/user1.txt
313             - /path/to/user2.txt
314              
315             =head1 METHODS
316              
317             =head2 PlugAuth::Plugin::FlatAuth-E<gt>refresh
318              
319             Refresh the data (checks the files, and re-reads if necessary).
320              
321             =head2 PlugAuth::Plugin::FlatAuth-E<gt>check_credentials( $user, $password )
322              
323             Given a user and password, check to see if the password is correct.
324              
325             =head2 PlugAuth::Plugin::FlatAuth-E<gt>all_users
326              
327             Returns a list of all users.
328              
329             =head2 PlugAuth::Plugin::FlatAuth-E<gt>create_user( $user, $password )
330              
331             =head2 PlugAuth::Plugin::FlatAuth-E<gt>create_user_cb( $user, $password, $callback)
332              
333             Create a new user with the given password.
334              
335             =head2 PlugAuth::Plugin::FlatAuth-E<gt>change_password( $user, $password )
336              
337             Change the password of the given user.
338              
339             =head2 PlugAuth::Plugin::FlatAuth-E<gt>delete_user( $user )
340              
341             Delete the given user.
342              
343             =head1 SEE ALSO
344              
345             L<PlugAuth>, L<PlugAuth::Plugin::FlatAuthz>
346              
347             =head1 AUTHOR
348              
349             Graham Ollis <gollis@sesda3.com>
350              
351             =head1 COPYRIGHT AND LICENSE
352              
353             This software is copyright (c) 2012 by NASA GSFC.
354              
355             This is free software; you can redistribute it and/or modify it under
356             the same terms as the Perl 5 programming language system itself.
357              
358             =cut