File Coverage

blib/lib/PlugAuth/Plugin/FlatAuth.pm
Criterion Covered Total %
statement 120 126 95.2
branch 31 36 86.1
condition 17 22 77.2
subroutine 21 21 100.0
pod 6 7 85.7
total 195 212 91.9


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.35'; # VERSION
5              
6              
7 40     40   23654 use strict;
  40         89  
  40         1658  
8 40     40   215 use warnings;
  40         62  
  40         1482  
9 40     40   1319 use 5.010001;
  40         181  
10 40     40   302 use Log::Log4perl qw( :easy );
  40         72  
  40         465  
11             # TODO: maybe optionally use Crypt::Passwd::XS instead
12 40     40   37811 use Crypt::PasswdMD5 qw( unix_md5_crypt apache_md5_crypt );
  40         65331  
  40         3536  
13 40     40   4169 use Role::Tiny::With;
  40         2807  
  40         34040  
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 240 shift->flat_init;
24             }
25              
26              
27             sub refresh {
28             # Should be called with every request.
29 683     683 1 5629 my $config = __PACKAGE__->global_config;
30 683         3916 my @user_files = $config->user_file;
31 683 100       16095 if ( grep has_changed($_), @user_files )
32             {
33 63         485 my @users = map +{ __PACKAGE__->read_file($_, lc_keys => 1) }, @user_files;
34 63         266 %Userpw = ();
35 63         176 for my $list (@users)
36             {
37 72         265 for my $user (map { lc $_ } keys %$list)
  208         416  
38             {
39 208   100     874 $Userpw{$user} //= [];
40 208         203 push @{ $Userpw{$user} }, $list->{$user};
  208         585  
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         442 mark_changed($config->group_file);
48             }
49             }
50              
51              
52             sub _validate_pw
53             {
54 146     146   280 my($plain, $encrypted) = @_;
55 146 100       240 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         18302 my $ret = crypt($plain, $encrypted);
60 146 100       1934 (defined $ret) && ($ret eq $encrypted);
61             };
62            
63             # idea borrowed from Authen::Simple::Password
64 54 100       402 if($encrypted =~ /^\$(\w+)\$/)
65             {
66 38 100 66     359 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     100685 return 1 if $1 eq '1' && unix_md5_crypt ( $plain, $encrypted ) eq $encrypted;
71             }
72 25         163 return 0;
73             }
74              
75             sub check_credentials {
76 159     159 1 395 my ($self, $user,$pw) = @_;
77 159         384 $user = lc $user;
78              
79 159 100 66     1080 if($pw && $Userpw{$user})
80             {
81 138 100       241 return 1 if grep { _validate_pw($pw, $_) } @{ $Userpw{$user} };
  146         461  
  138         507  
82             }
83 38         269 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   63 my($plain) = @_;
95 31         249 my $salt = join '', ('.', '/', 0..9, 'A'..'Z', 'a'..'z')[rand 64, rand 64];
96 31         162 apache_md5_crypt($plain, $salt);
97             }
98              
99 23     23 1 101 sub create_user { goto &create_user_cb }
100              
101             sub create_user_cb
102             {
103 26     26 1 282 my($self, $user, $password, $cb) = @_;
104              
105 26 100 100     234 unless($user && $password)
106             {
107 2         8 WARN "User or password not provided";
108 2         1692 return 0;
109             }
110              
111 24         73 $user = lc $user;
112              
113 24 50       109 if(defined $Userpw{$user})
114             {
115 0         0 WARN "User $user already exists";
116 0         0 return 0;
117             }
118              
119 24         130 foreach my $filename ($self->global_config->user_file)
120             {
121 24 50       996 next unless -w $filename;
122              
123 24         96 $password = _created_encrypted_password($password);
124              
125             my $ok = $self->lock_and_update_file($filename, sub {
126 40     40   309 use autodie;
  40         97  
  40         499  
127 24     24   58 my($fh) = @_;
128              
129 24         55 my $buffer = '';
130 24         456 while(! eof $fh)
131             {
132 47         116 my $line = <$fh>;
133 47         61 chomp $line;
134 47         164 $buffer .= "$line\n";
135             }
136 24         127 $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         110 $Userpw{$user} = $password;
145 24 100       136 $cb->() if defined $cb;
146            
147 24         186 $buffer;
148 24         239514 });
149              
150 24 50       244 return 0 unless $ok;
151              
152 24         162 INFO "created user $user";
153 24         21427 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 25 my($self, $user, $password) = @_;
164              
165 9 100 66     79 unless($user && $password)
166             {
167 1         5 WARN "User or password not provided";
168 1         927 return 0;
169             }
170              
171 8         42 $user = lc $user;
172              
173 8 100       43 unless(defined $Userpw{$user})
174             {
175 1         7 WARN "User $user does not exist";
176 1         971 return 0;
177             }
178              
179 7         46 $password = _created_encrypted_password($password);
180              
181 7         72919 foreach my $filename ($self->global_config->user_file)
182             {
183             $self->lock_and_update_file($filename, sub {
184 40     40   249690 use autodie;
  40         100  
  40         392  
185 7     7   18 my($fh) = @_;
186              
187 7         22 my $buffer = '';
188            
189 7         128 while(! eof $fh)
190             {
191 22         58 my $line = <$fh>;
192 22         49 chomp $line;
193 22         57 my($thisuser, $oldpassword) = split /:/, $line;
194 22 100 100     132 if(defined $thisuser && lc($thisuser) eq $user)
195             {
196 7         62 $buffer .= join(':', $user, $password) . "\n";
197             }
198             else
199             {
200 15         84 $buffer .= "$line\n";
201             }
202             }
203            
204 7         56 $buffer;
205 7         302 });
206             }
207              
208 7         58 INFO "user password changed $user";
209 7         6032 return 1;
210             }
211              
212              
213             sub delete_user
214             {
215 6     6 1 14 my($self, $user) = @_;
216              
217 6         22 $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         27 foreach my $filename ($self->global_config->user_file)
226             {
227             $self->lock_and_update_file($filename, sub {
228 40     40   222468 use autodie;
  40         88  
  40         252  
229 8     8   15 my($fh) = @_;
230              
231 8         20 my $buffer = '';
232 8         92 while(! eof $fh)
233             {
234 63         78 my $line = <$fh>;
235 63         54 chomp $line;
236 63         102 my($thisuser, $password) = split /:/, $line;
237 63 100 100     210 next if ($thisuser//'') eq $user;
238 57         137 $buffer .= "$line\n";
239             }
240 8         27 $buffer;
241 8         132 });
242             }
243              
244 6         47 INFO "deleted user $user";
245 6         3656 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.35
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