File Coverage

blib/lib/Password/Policy.pm
Criterion Covered Total %
statement 40 103 38.8
branch 0 20 0.0
condition 2 13 15.3
subroutine 13 24 54.1
pod 3 7 42.8
total 58 167 34.7


line stmt bran cond sub pod time code
1             package Password::Policy;
2             {
3             $Password::Policy::VERSION = '0.02';
4             }
5              
6             # ABSTRACT: Make managing multiple password strength profiles easy
7              
8 4     4   110086 use strict;
  4         11  
  4         164  
9 4     4   23 use warnings;
  4         8  
  4         114  
10              
11 4     4   3577 use Class::Load;
  4         158211  
  4         192  
12 4     4   3805 use Config::Any;
  4         52216  
  4         161  
13 4     4   47 use Try::Tiny;
  4         9  
  4         386  
14              
15 4     4   2895 use Password::Policy::Exception;
  4         12  
  4         95  
16 4     4   2307 use Password::Policy::Exception::EmptyPassword;
  4         12  
  4         129  
17 4     4   2757 use Password::Policy::Exception::InvalidAlgorithm;
  4         10  
  4         109  
18 4     4   2314 use Password::Policy::Exception::InvalidProfile;
  4         11  
  4         112  
19 4     4   2484 use Password::Policy::Exception::InvalidRule;
  4         11  
  4         128  
20 4     4   2512 use Password::Policy::Exception::NoAlgorithm;
  4         22  
  4         110  
21 4     4   2965 use Password::Policy::Exception::ReusedPassword;
  4         10  
  4         4170  
22              
23              
24              
25             sub new {
26 3     3 1 51 my ($class, %args) = @_;
27              
28 3         10 my $config_file = $args{config};
29 3   100     23 my $previous = $args{previous} || [];
30              
31 3         54 my $config = Config::Any->load_files({ files => [ $config_file ], use_ext => 1 });
32 0           my $rules = {};
33              
34 0           $config = $config->[0]->{$config_file};
35 0           my @profiles = keys(%{$config});
  0            
36              
37 0           my $self = bless {
38             _config => $config,
39             _rules => $rules,
40             _previous => $previous,
41             _profiles => \@profiles
42             } => $class;
43              
44 0           foreach my $key (@profiles) {
45 0           $rules->{$key} = $self->_parse_rules($key);
46             }
47              
48 0           $self->{_rules} = $rules;
49 0           return $self;
50             }
51              
52             sub _parse_rules {
53 0     0     my ($self, $profile_name) = @_;
54 0           my $rules;
55              
56 0           my $profile = $self->config->{$profile_name};
57 0 0         if(my $parent = delete $profile->{inherit}) {
58 0           $rules = $self->_parse_rules($parent);
59             }
60 0           foreach my $key (keys(%{$profile})) {
  0            
61 0 0         if($key eq 'algorithm') {
62 0           $rules->{algorithm} = $profile->{$key};
63 0           next;
64             }
65 0 0         if($rules->{$key}) {
66 0 0         $rules->{$key} = $profile->{$key} if($profile->{$key} > $rules->{$key});
67             } else {
68 0           $rules->{$key} = $profile->{$key};
69             }
70             }
71 0           return $rules;
72             }
73              
74             sub config {
75 0     0 0   return (shift)->{_config};
76             }
77              
78             sub profiles {
79 0     0 0   return (shift)->{_profiles};
80             }
81              
82             sub previous {
83 0     0 0   return (shift)->{_previous};
84             }
85              
86             sub rules {
87 0     0 0   my $self = shift;
88 0   0       my $profile = shift || 'default';
89 0           my $rules = $self->{_rules};
90 0   0       return $rules->{$profile} || Password::Policy::Exception::InvalidProfile->throw;
91             }
92              
93              
94             sub process {
95 0     0 1   my ($self, $args) = @_;
96 0   0       my $password = $args->{password} || Password::Policy::Exception::EmptyPassword->throw;
97              
98 0           my $rules = $self->rules($args->{profile});
99 0   0       my $algorithm = $rules->{algorithm} || Password::Policy::Exception::NoAlgorithm->throw;
100 0           foreach my $rule (keys(%{$rules})) {
  0            
101 0 0         next if($rule eq 'algorithm');
102              
103 0           my $rule_class = 'Password::Policy::Rule::' . ucfirst($rule);
104             try {
105 0     0     Class::Load::load_class($rule_class);
106             } catch {
107 0     0     Password::Policy::Exception::InvalidRule->throw;
108 0           };
109 0           my $rule_obj = $rule_class->new($rules->{$rule});
110 0           my $check = $rule_obj->check($password);
111 0 0         unless($check) {
112             # no idea what failed if we didn't get a more specific exception, so
113             # throw a generic error
114 0           Password::Policy::Exception->throw;
115             }
116             }
117 0           my $enc_password = $self->encrypt($algorithm, $password);
118              
119             # This is a post-encryption rule, so it's a special case.
120 0 0         if($self->previous) {
121 0           foreach my $previous_password (@{$self->previous}) {
  0            
122 0 0         Password::Policy::Exception::ReusedPassword->throw if($enc_password eq $previous_password);
123             }
124             }
125 0           return $enc_password;
126             }
127              
128              
129             sub encrypt {
130 0     0 1   my ($self, $algorithm, $password) = @_;
131              
132 0 0         unless($algorithm) { Password::Policy::Exception::NoAlgorithm->throw; }
  0            
133 0 0         unless($password) { Password::Policy::Exception::EmptyPassword->throw; }
  0            
134              
135 0           my $enc_class = 'Password::Policy::Encryption::' . $algorithm;
136             try {
137 0     0     Class::Load::load_class($enc_class);
138             } catch {
139 0     0     Password::Policy::Exception::InvalidAlgorithm->throw;
140 0           };
141 0           my $enc_obj = $enc_class->new;
142 0           my $new_password = $enc_obj->enc($password);
143 0           return $new_password;
144             }
145              
146             1;
147              
148              
149              
150             =pod
151              
152             =head1 NAME
153              
154             Password::Policy - Make managing multiple password strength profiles easy
155              
156             =head1 VERSION
157              
158             version 0.02
159              
160             =head1 UNICODE
161              
162             This module strives to handle Unicode characters in a sane way. The exception are the
163             uppercase and lowercase rules, which obviously don't make sense in a Unicode setting.
164             If you find a case where Unicode characters don't behave correctly, please let me know.
165              
166             =head1 EXCEPTIONS
167              
168             This module tries to throw a well defined exception object when it encounters an
169             error. Wrapping it in something like L is highly recommended, so that
170             you can handle errors intelligently.
171              
172             =head1 EXTENDING
173              
174             Password::Policy is a baseline - there's no conceivable way to plan for anything an
175             administrator would like to do. To add a rule, you need a package that looks like this:
176              
177             package Password::Policy::Rule::MyRule;
178              
179             use strict;
180             use warnings;
181              
182             use parent 'Password::Policy::Rule';
183              
184             sub default_arg { return 42; }
185              
186             sub check {
187             my $self = shift;
188             my $password = $self->prepare(shift);
189              
190             ...your code goes here, and either throws an exception or doesn't...
191              
192             return 1;
193             }
194              
195             1;
196              
197             To add a new encryption type, you need a package that looks like this:
198              
199             package Password::Policy::Encryption::MyEncryption;
200              
201             use strict;
202             use warnings;
203              
204             use parent 'Password::Policy::Encryption';
205              
206             sub enc {
207             my $self = shift;
208             my $password = $self->prepare(shift);
209              
210             ...your code goes here, and either throws an exception or doesn't...
211              
212             return $encrypted_password;
213             }
214              
215             1;
216              
217             =head1 SYNOPSIS
218              
219             use Password::Policy;
220              
221             my $pp = Password::Policy->new(config => '/path/to/config');
222             $pp->process({ password => 'mypassword to check', profile => 'profile to check' });
223              
224             =head1 DESCRIPTION
225              
226             Password::Policy is an easy way to manage multiple password strength/encryption profiles.
227             The two most obvious use cases are:
228              
229             - You are running multiple sites with a similar/shared backend, and they have different
230             policies for password strength
231              
232             - You have multiple types of users, and want different password strengths for each of them,
233             It's ok for a regular user to have 'i like cheese' as a password, but an administrator's
234             password should be made of stronger stuff.
235              
236             The whole thing is driven by a configuration file, passed in on instantiation. It uses
237             L internally, so the config file format can be whatever you would like. The
238             examples all use YAML, but anything Config::Any understands will work.
239              
240             Assuming a configuration file looks like this:
241              
242             ---
243             default:
244             length: 4
245             algorithm: "Plaintext"
246              
247             site_moderator:
248             inherit: "default"
249             length: 8
250             uppercase: 1
251              
252             site_admin:
253             inherit: "site_moderator"
254             length: 10
255             # will have uppercase: 1 from site_moderator
256             numbers: 2
257             algorithm: "ROT13"
258              
259             The default profile will encrypt with plaintext (no encryption!), and make sure the
260             password is at least four characters long. If a site moderator is attempting to change
261             his password, it will extend that length check to 8, and require at least one of
262             those characters to be an uppercase ASCII character.
263              
264             The site_admin profile will extend that length to 10, require two numbers, and
265             change the encryption method to ROT-13 (secure!). It also keeps the one uppercase
266             character requirement from site_moderator.
267              
268             =head1 METHODS
269              
270             =head2 new
271              
272             Creates a new Password::Policy object. Takes at least one argument, config. Optionally
273             can take a second argument, previous, that contains encypted passwords (the idea being
274             that it's the user's old passwords, that can't be re-used).
275              
276             =head2 process
277              
278             Process a password. Takes a hashref as an argument, with at least one argument,
279             'password', that is the plaintext password. It also takes 'profile', which will
280             refer to a profile in the configuration file.
281              
282             my $enc_passwd = $pp->process({ password => 'i like cheese', profile => 'site_admin' });
283              
284             =head2 encrypt
285              
286             Encrypt a password. Takes two arguments, the algorithm to use, and the plain text
287             password to encrypt.
288              
289             my $enc_passwd = $pp->encrypt('ROT13', 'i like cheese');
290              
291             =head1 AUTHOR
292              
293             Andrew Nelson
294              
295             =head1 COPYRIGHT AND LICENSE
296              
297             This software is copyright (c) 2012 by Andrew Nelson.
298              
299             This is free software; you can redistribute it and/or modify it under
300             the same terms as the Perl 5 programming language system itself.
301              
302             =cut
303              
304              
305             __END__