X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2FPassword_Mixin.pm;h=2e400ec9d8cd0b9b25e719675edc088dd0264f0b;hb=0e1145ddaed12275fca87cd352ee5ce85aa62344;hp=ae82b96d3f6068534095132ffe9b3eb08d8dddf2;hpb=0375336c45591cb3354daafa2be5fdf8e905a3ea;p=freeside.git diff --git a/FS/FS/Password_Mixin.pm b/FS/FS/Password_Mixin.pm index ae82b96d3..2e400ec9d 100644 --- a/FS/FS/Password_Mixin.pm +++ b/FS/FS/Password_Mixin.pm @@ -16,6 +16,8 @@ FS::UID->install_callback( sub { #eval "use Authen::Passphrase::BlowfishCrypt;"; }); +our @pw_set; + our $me = '[' . __PACKAGE__ . ']'; our $BLOWFISH_COST = 10; @@ -37,7 +39,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 @@ -45,9 +47,20 @@ sub is_password_allowed { my $self = shift; my $password = shift; + my $cust_main = $self->cust_main; + + # workaround for non-inserted services + if ( !$cust_main and $self->get('pkgnum') ) { + my $cust_pkg = FS::cust_pkg->by_key($self->get('pkgnum')); + $cust_main = $cust_pkg->cust_main if $cust_pkg; + } + # selfservice signup invokes this without customer, but it checks this conf separately + warn "is_password_allowed: no customer could be identified" if !$cust_main; + return '' if $cust_main && $conf->config_bool('password-insecure', $cust_main->agentnum); + # basic checks using Data::Password; # options for Data::Password - $DICTIONARY = 4; # minimum length of disallowed words + $DICTIONARY = 0; # minimum length of disallowed words, false value disables dictionary checking $MINLEN = $conf->config('passwordmin') || 6; $MAXLEN = $conf->config('passwordmax') || 8; $GROUPS = 4; # must have all 4 'character groups': numbers, symbols, uppercase, lowercase @@ -57,23 +70,54 @@ sub is_password_allowed { # # 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); - $error = 'must contain at least one each of numbers, symbols, and lowercase and uppercase letters' - if $error eq 'contains less than 4 character groups'; # avoid confusion + + # 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 known usernames - my @disallowed_names = $self->password_disallowed_names; - foreach my $noname (@disallowed_names) { - if ($password =~ /$noname/i) { - #keeping message ambiguous to avoid leaking personal info - return 'Password contains a disallowed word'; - } - } + #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 + 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)); + } + } + # do the actual checking + foreach my $word (@words) { + next unless length($word) > 2; + if ($password =~ /$word/i) { + return qq(Password contains account information '$word'); + } + } + } + if ( $conf->config('password-no_reuse') =~ /^(\d+)$/ ) { my $no_reuse = $1; @@ -108,15 +152,15 @@ sub is_password_allowed { ''; } -=item password_disallowed_names +=item password_svc_check -Override to return a list additional words (eg usernames) not -to be used by passwords on this service. +Override to run additional service-specific password checks. =cut -sub password_disallowed_names { - return (); +sub password_svc_check { + my ($self, $password) = @_; + return ''; } =item password_history_key @@ -187,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 @@ -206,6 +270,37 @@ sub _blowfishcrypt { =back +=head1 CLASS METHODS + +=over 4 + +=item pw_set + +Returns the list of characters allowed in random passwords (from the +C config). + +=cut + +sub pw_set { + my $class = shift; + if (!@pw_set) { + my $pw_set = $conf->config('password-generated-characters'); + $pw_set =~ s/\s//g; # don't ever allow whitespace + if ( $pw_set =~ /[[:lower:]]/ + && $pw_set =~ /[[:upper:]]/ + && $pw_set =~ /[[:digit:]]/ + && $pw_set =~ /[[:punct:]]/ ) { + @pw_set = split('', $pw_set); + } else { + warn "password-generated-characters set is insufficient; using default."; + @pw_set = split('', 'abcdefghijkmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ23456789()#.,'); + } + } + return @pw_set; +} + +=back + =head1 SEE ALSO L