X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2FPassword_Mixin.pm;h=b29cf571365fdae310c4e59d85816752c939185b;hp=990f73595721f64020160b15725347cb66733508;hb=3adb46fccf9f631e188ea5383bd147b340477639;hpb=8248d1c6ba608044c8f66a53daab254f476d5c6d diff --git a/FS/FS/Password_Mixin.pm b/FS/FS/Password_Mixin.pm index 990f73595..b29cf5713 100644 --- a/FS/FS/Password_Mixin.pm +++ b/FS/FS/Password_Mixin.pm @@ -6,8 +6,13 @@ use FS::password_history; use Authen::Passphrase; use Authen::Passphrase::BlowfishCrypt; # https://rt.cpan.org/Ticket/Display.html?id=72743 +use Data::Password qw(:all); our $DEBUG = 0; +our $conf; +FS::UID->install_callback( sub { + $conf = FS::Conf->new; +}); our $me = '[' . __PACKAGE__ . ']'; @@ -30,7 +35,7 @@ message on failure, an empty string on success. This MUST NOT be called from check(). It should be called by the office UI, self-service ClientAPI, or other I code that processes a password change, and only if the user has taken some action with the intent -of changing the password. +of setting the password. =cut @@ -38,7 +43,79 @@ sub is_password_allowed { my $self = shift; my $password = shift; - # check length and complexity here + # basic checks using Data::Password; + # options for Data::Password + $DICTIONARY = 0; # minimum length of disallowed words, false value disables dictionary checking + $MINLEN = $conf->config('passwordmin') || 8; + $MAXLEN = $conf->config('passwordmax') || 12; + $GROUPS = 4; # must have all 4 'character groups': numbers, symbols, uppercase, lowercase + # other options use the defaults listed below: + # $FOLLOWING = 3; # disallows more than 3 chars in a row, by alphabet or keyboard (ie abcd or asdf) + # $SKIPCHAR = undef; # set to true to skip checking for bad characters + # # lists of disallowed words + # @DICTIONARIES = qw( /usr/share/dict/web2 /usr/share/dict/words /usr/share/dict/linux.words ); + + # first, no dictionary checking but require 4 char groups + my $error = IsBadPassword($password); + + # but they can get away with 3 char groups, so long as they're not using a word + if ($error eq 'contains less than 4 character groups') { + $DICTIONARY = 4; # default from Data::Password is 5 + $GROUPS = 3; + $error = IsBadPassword($password); + # take note--we never actually report dictionary word errors; + # 4 char groups is the rule, 3 char groups and no dictionary words is an acceptable exception + $error = 'should contain at least one each of numbers, symbols, lowercase and uppercase letters' + if $error; + } + + # maybe also at some point add an exception for any passwords of sufficient length, + # see https://xkcd.com/936/ + + $error = 'Invalid password - ' . $error if $error; + return $error if $error; + + #check against service fields + $error = $self->password_svc_check($password); + return $error if $error; + + return '' unless $self->get($self->primary_key); # for validating new passwords pre-insert + + #check against customer fields + my $cust_main = $self->table eq 'access_user' + ? $self->user_cust_main + : $self->cust_main; + if ($cust_main) { + my @words; + # words from cust_main + foreach my $field ( qw( last first daytime night fax mobile ) ) { + push @words, split(/\W/,$cust_main->get($field)); + } + # words from cust_location + foreach my $loc ($cust_main->cust_location) { + foreach my $field ( qw(address1 address2 city county state zip) ) { + push @words, split(/\W/,$loc->get($field)); + } + } + # words from cust_contact & contact_phone + foreach my $contact (map { $_->contact } $cust_main->cust_contact) { + foreach my $field ( qw(last first) ) { + push @words, split(/\W/,$contact->get($field)); + } + # not hugely useful right now, hyphenless stored values longer than password max, + # but max will probably be increased eventually... + foreach my $phone ( qsearch('contact_phone', {'contactnum' => $contact->contactnum}) ) { + push @words, split(/\W/,$phone->get('phonenum')); + } + } + # do the actual checking + foreach my $word (@words) { + next unless length($word) > 2; + if ($password =~ /$word/i) { + return qq(Password contains account information '$word'); + } + } + } my $no_reuse = 3; # allow override here if we really must @@ -75,6 +152,17 @@ sub is_password_allowed { ''; } +=item password_svc_check + +Override to run additional service-specific password checks. + +=cut + +sub password_svc_check { + my ($self, $password) = @_; + return ''; +} + =item password_history_key Returns the name of the field in L that's the foreign @@ -143,6 +231,26 @@ sub insert_password_history { } +=item delete_password_history; + +Removes all password history records attached to this object, in preparation +to delete the object. + +=cut + +sub delete_password_history { + my $self = shift; + my @records = qsearch('password_history', { + $self->password_history_key => $self->get($self->primary_key) + }); + my $error = ''; + foreach (@records) { + $error ||= $_->delete; + } + return $error . ' (clearing password history)' if $error; + ''; +} + =item _blowfishcrypt PASSWORD For internal use: takes PASSWORD and returns a new @@ -162,6 +270,29 @@ sub _blowfishcrypt { =back +=head1 CLASS METHODS + +=over 4 + +=item pw_set + +Returns the list of characters allowed in random passwords. This is now +hardcoded. + +=cut + +sub pw_set { + + # ASCII alphabet, minus easily confused stuff (l, o, O, 0, 1) + # and plus some "safe" punctuation + split('', + 'abcdefghijkmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ23456789#.,[]-_=+' + ); + +} + +=back + =head1 SEE ALSO L