[freeside-commits] branch 3.x updated. b5a9068479a38c2b901b1954a57c51f43e84be2d

Mark Wells mark at 420.am
Fri Nov 13 10:01:08 PST 2015


The branch, 3.x has been updated
       via  b5a9068479a38c2b901b1954a57c51f43e84be2d (commit)
       via  979f2b7488f2399fb2e4c7c98a57536b5cf1bff7 (commit)
       via  ed817ff4b918df398fa89835909723c7d8f311f8 (commit)
       via  b527fe6618343b4e973955c295d761127b06859e (commit)
       via  afd8bcfd6fbbc40f7fa72df58166afc6e42ddf6d (commit)
       via  4d5a5d18c44bcd5b6b7d970434d6d271503e5442 (commit)
       via  0000bd1322932cf4d6035391ed41f12d62441b02 (commit)
       via  be4e0c51a534945f6782d2b9c48a2b1e88292642 (commit)
       via  ee7e3cc614c860c8d0aa2fdc9b112c57fe548ff2 (commit)
       via  7a7776bc564aa2fb03c7c217d9e6f5fd39d8eb5f (commit)
       via  71852b4b08791527398656c42dcfc980891ca0b2 (commit)
       via  f4123bf14ef927f1184d247edccbf50f1e9691d4 (commit)
       via  4e091a88af83ba9293a1747aaf754439dcc4cd39 (commit)
       via  09bdc89b2cf12d7b4f4dd1376975b4c9164b4249 (commit)
       via  a7cdcc2e497eb862d36becb93a14fb32037260be (commit)
       via  e2d16c850d8857567023f10b8c57daf0570c6a8e (commit)
       via  f8c4741390077c1448d627949d19c9220cc8e610 (commit)
       via  96e1e42b99847bfc2ea9a56281b77fa5a4ac6f55 (commit)
       via  3399a25eba1da01491ede8a86cc99b5be0b1dfe8 (commit)
       via  2b3eb91eaa841175eeb9f998a4457427670e7df0 (commit)
       via  0953ca94fb6402195f511929afd3327d72e91377 (commit)
       via  26240874c78539a882db57782a0199a8eb92c1e0 (commit)
       via  ab9786fe9ea49e7a00459da99f1b8dc1e2b023d3 (commit)
       via  e2b9b80255731ec51fae4941e564f63166336a63 (commit)
       via  3aa784b1324ac14026eb900cf598ad9aec49179a (commit)
       via  a5464ff6f8f41685a3a325558805751c159b67e7 (commit)
       via  b7bb288198488f202ff1075a28631f27fc1bab97 (commit)
       via  bc03aa6e1cebc712789dfc254901c6f8d8935057 (commit)
       via  4fdc60587141db78b4baf67168cb5ab2122210d4 (commit)
       via  83ac7e044f6ca98c57207537e9c7b0a4d469162a (commit)
       via  f08ff59915dc9e5478e97e10824e268b0b9c3371 (commit)
       via  2df5397554b352e5b4133d33849fba9edfa9ef4e (commit)
       via  efbbb5771b443200ab25b46b8051fa9c0767bc4b (commit)
       via  655bd24478012c6d4e9d76152754897c26e4c910 (commit)
       via  3afbcad2c1bcc67d242839f2696cb14e380b4a6a (commit)
       via  338b3a1cc10549a57a491af5dc4ff85686b916d2 (commit)
       via  d793ab5b5050e29654183b6d0a645f934bb87df0 (commit)
       via  249f7d6cf45f8df5679ac36fc28dd8376e695496 (commit)
       via  9527575801494bc501c555928e4bf922049b0abe (commit)
       via  59dabb780e7fa25290392bb3c40d3f87b404463d (commit)
       via  3a2b8908272ca64add0522d1e6536fa011b3ef9b (commit)
       via  018b487f771a0713a5ee2b8b3f0658d87261dbec (commit)
       via  638a28282f1b87c59f7e11b4da8ce0754307edc9 (commit)
       via  b74b684d4af6c4bbdfcafb2d99e737550962c9bc (commit)
       via  2bc72d758fc28a4a8beb24d7a93d5502d0355ca6 (commit)
       via  275239995f6b9888e470e4128287fba4d56df790 (commit)
       via  05959a9336936ec1d6e0ad8641a47d2f8dd515cf (commit)
       via  e13ecda5349caa3ee1bb29ced790d8e7a4e7ea28 (commit)
       via  749d1025a324b97b53042b77065ff96c90fcf2dd (commit)
       via  1b6c91b4566e35d4a084c8704f258e33d409003c (commit)
       via  c0556fd6e20de3b3867d9c627b5928f0b7a0540c (commit)
       via  a5a3bd7c762a49add6bcb2cd10c44d9c26a8df88 (commit)
       via  5e6b981ccafac97a17d64924ef770c471ca28ab0 (commit)
       via  bdbda2196d839c13a25da3ec5569b93d121d10f3 (commit)
       via  d1f18bfcf62f379dc28679481149be31eded4c9b (commit)
       via  cf17ad2a9c82d07412409b6dfea9bfb9be7d830c (commit)
       via  ac07dfa491181046e8dd2881d6093da9e8d3616f (commit)
       via  69c2f42ce788c1793b72bab9ff9d21c4e19a098e (commit)
       via  0c07b9072cd4da266d4e56f888bf33512af7d17f (commit)
       via  27ad9a232bbd0ac64091f3d0796fb836928bdb92 (commit)
       via  8db72ccbae1214657269f0eb8aceefe6e349b52c (commit)
       via  56a6c2354d5b02b89f48af3d5ddf78dd053ed01a (commit)
       via  a2dbfca9b10917eba6e8ec2231eb2cc764b886b2 (commit)
       via  5b27ffc34056dbd340f83db34f00defc7cccae24 (commit)
       via  28061a709c2d15f481fce9787b32b39b347d747d (commit)
       via  453d0509426c36f94868c06823327bb58871a763 (commit)
       via  ddfa1b3070f48aacfeb380dd2624040d2daaac8e (commit)
       via  7e8171aede9c416c8b8380dfd081fd48e2fa1615 (commit)
       via  0e1465cd691cb631528e7a7f83c641a18b6cef2f (commit)
       via  8ed07b4b7ed0927893ade664139e58d28eb91c74 (commit)
       via  ab9aa5b5d55eb7330b3435819663ef03538278e2 (commit)
       via  61e8e75f45382cfa5ad6a52be58c9744dee8cba8 (commit)
       via  ef9491e6007b7aed7ae0e0486a6abb237ca08519 (commit)
       via  c68e2488e326a85451b16a046691edcd3f58add5 (commit)
       via  ddbea80d28f423a9e5ad3f879545cff4434a4ed8 (commit)
       via  70054fbcf1d7172deba9d5aca04beab37bbe53e6 (commit)
       via  8268e49167b44071b498339265b5f5761cdfeaeb (commit)
       via  6fca5c09b6a2c0251b106e7d1b7f6770cdf49a93 (commit)
       via  38472d1494a3c0e7c39ba7433a23de25159350d7 (commit)
       via  1f0d6d4ce0c0dd6fffb87d5ead4a6843e671c792 (commit)
       via  f7ae793c0a72474666cac1aa4b6f4ef6291712d5 (commit)
       via  d9c0257b3b900192a618943407e1c007c63b21ad (commit)
       via  8e4a09fe86c531059bfea4b3d49599e417580198 (commit)
       via  76dff99173f25f38002fbb5236d8141a8781c3eb (commit)
       via  c878f595e11f510058671b146e08c250766cd1bf (commit)
       via  fe6ddc9940cf3a55b4c37f360a31b90e92e4157d (commit)
       via  2581d8f2341ac8def1a712cfe70543ec5f7b0959 (commit)
       via  8cb529e14f605eeb4bf9bf0faa3534e864d55746 (commit)
       via  62b4e6df5b5cf4bb38a8a65c9e298221c448abe1 (commit)
       via  312493fcefb373f7dc456c5d39edafc5af54d8b7 (commit)
       via  9790fb2c499f1b003bf34bae102b3e855d15aada (commit)
       via  ee51c7264d30040110d1da6f1cc0fbbdcbd04fbb (commit)
       via  5223392618e4025cddb42bbaf709d3269b456970 (commit)
       via  1dd6898c05a298171fba7f38ef100036541d387e (commit)
       via  b3bcf26c00ae0a5685e1b5db82669c03321532ce (commit)
       via  cc767315b0b128af86831baa29cac9ccc5549a12 (commit)
       via  e0dd3887a47d291f804d20fb5a47bf1587663160 (commit)
       via  aaecb642260b19488b469e57e5b1a11abcd87620 (commit)
       via  75d07cc54b2e54e50bff80203c6065b215ee5d1c (commit)
       via  1d8cf0f25e468b871d18962bda022c94d2934f02 (commit)
       via  7ac9115109599df55f55a52c83204f0ac4e0b00c (commit)
       via  3220d99fa3ef70ca60cc7218533401da1990f4e0 (commit)
       via  9dc9789e61324f46e482107420fd27db55b846ef (commit)
       via  29a5430518ba80f1431277b5dbb28da524eedcbb (commit)
       via  6776904eea418b12bec7f1aa0296e9384981d07a (commit)
       via  91fe0dc3740da80dfdae9a291f4ff11bab11df0b (commit)
       via  343eff0feb81094bf3e47ab994caae126fa8dfe8 (commit)
       via  fd62e5c7a94361229febd28bf84cdd0e65a7028a (commit)
       via  8cf56e1f1031f0a3bb1cebde402d14e98f204588 (commit)
       via  5044079396b9bef09dd498a0e64a4f20046318b8 (commit)
       via  0db7afaa7079e7a997d1309715d371a0d8ad279b (commit)
       via  407b4dbfd15fb120a06a9c2da4fe77137aebdacf (commit)
       via  21c0c6fb8fef10843651a81152b02f1c174b3893 (commit)
       via  f3738a9fcd588d32eea9b0af6a3e884293c0c6e5 (commit)
       via  b4e2bdc5477a8318da25914c7aedfc5901e3975d (commit)
       via  829793a9f5e8628a1b51e84b8fc03c2f54e118e7 (commit)
       via  af6c7cfb1670812193223561fa3cfe7e6ba53acc (commit)
       via  0b9f4fda54241fef861fe64ed4970f6bf752d733 (commit)
       via  dd268209494ce9fc3491d02b8c3034a7dffc84e4 (commit)
       via  8550a95443b6e3ddc9d8438d65b5e38e45b638c0 (commit)
       via  fec109316e1aa49651b72fd31cc938d2ea6ee5cf (commit)
       via  147d171069905e80a47442ee4b35515534989909 (commit)
       via  48daaf7dd667869e033ce9463829cbfec47fab4e (commit)
       via  4c14d549f0a6c15fb5715877c1827b978f04dee6 (commit)
       via  ab67c00c04f6e50045789ecf38f9f89a71dca0a4 (commit)
       via  94c8f1ae6c162ce49dc59cb8edb46ce63e25555a (commit)
       via  1228954b61d149e6c890365ce3555002209f5e62 (commit)
       via  98ed783317fe098b221dda211ef6264cf6334c43 (commit)
       via  0d678bcd0d18621ede25d0465b71e919e1c5212e (commit)
       via  471305e3debe7bd92742e63439a21f1905a91737 (commit)
       via  3fa17959680a3181b652f2af160904210d45e1ef (commit)
       via  f33a1eaf968dbdbfbc55d7fb05cd2e36237c7676 (commit)
       via  e8c2e9a2fe84ad1dd5c4bc02d8837fdd3549bd18 (commit)
       via  7f5568b5d0b1f37b0a82f4a9809070a33381f8e8 (commit)
       via  56cfb20bc9c9771b66007fabe1e15cbaa153624c (commit)
       via  f888466b118f5f433cc123d455e611291f596466 (commit)
       via  0c76afbb717e1716e6126bc4a120b8d9471614a0 (commit)
       via  7beec7068e00be5ae1b2599fdf2b494bc19e31d0 (commit)
       via  9e8d2a5bafb21c42f54cdd9b3703e2f88a36e0a8 (commit)
      from  3e2c2ad8aff1bd361ca07495b2255538c8231079 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit b5a9068479a38c2b901b1954a57c51f43e84be2d
Author: Mark Wells <mark at freeside.biz>
Date:   Thu Nov 12 16:49:39 2015 -0800

    limit password reuse, core and svc_acct, #29354

diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm
index 10b2652..9bbde88 100644
--- a/FS/FS/ClientAPI/MyAccount.pm
+++ b/FS/FS/ClientAPI/MyAccount.pm
@@ -2947,13 +2947,15 @@ sub myaccount_passwd {
         )
     && ! $svc_acct->check_password($p->{'old_password'});
 
+    # should move password length checks into is_password_allowed
   $error = 'Password too short.'
     if length($p->{'new_password'}) < ($conf->config('passwordmin') || 6);
   $error = 'Password too long.'
     if length($p->{'new_password'}) > ($conf->config('passwordmax') || 8);
 
-  $svc_acct->set_password($p->{'new_password'});
-  $error ||= $svc_acct->replace();
+  $error ||= $svc_acct->is_password_allowed($p->{'new_password'})
+         ||  $svc_acct->set_password($p->{'new_password'})
+         ||  $svc_acct->replace();
 
   #regular pw change in self-service should change contact pw too, otherwise its
   #way too confusing.  hell its confusing they're separate at all, but alas.
@@ -3217,8 +3219,9 @@ sub process_reset_passwd {
 
   if ( $svc_acct ) {
 
-    $svc_acct->set_password($p->{'new_password'});
-    my $error = $svc_acct->replace();
+    my $error ||= $svc_acct->is_password_allowed($p->{'new_password'})
+              ||  $svc_acct->set_password($p->{'new_password'})
+              ||  $svc_acct->replace();
 
     return { %$info, 'error' => $error } if $error;
 
diff --git a/FS/FS/ClientAPI/Signup.pm b/FS/FS/ClientAPI/Signup.pm
index 3208396..a9678b0 100644
--- a/FS/FS/ClientAPI/Signup.pm
+++ b/FS/FS/ClientAPI/Signup.pm
@@ -698,6 +698,9 @@ sub new_customer {
         map { $_ => $packet->{$_} }
           qw( username _password sec_phrase popnum domsvc ),
       };
+      
+      my $error = $svc->is_password_allowed($packet->{_password});
+      return { error => $error } if $error;
 
       my @acct_snarf;
       my $snarfnum = 1;
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index fad786c..c259b7a 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -4252,6 +4252,13 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'password-no_reuse',
+    'section'     => 'password',
+    'description' => 'Minimum number of password changes before a password can be reused. By default, passwords can be reused without restriction.',
+    'type'        => 'text',
+  },
+
+  {
     'key'         => 'datavolume-forcemegabytes',
     'section'     => 'UI',
     'description' => 'All data volumes are expressed in megabytes',
diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index 9a4a892..ba56cff 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -384,6 +384,7 @@ if ( -e $addl_handler_use_file ) {
   use FS::report_batch;
   use FS::report_batch;
   use FS::report_batch;
+  use FS::password_history;
   # Sammath Naur
 
   if ( $FS::Mason::addl_handler_use ) {
diff --git a/FS/FS/Password_Mixin.pm b/FS/FS/Password_Mixin.pm
new file mode 100644
index 0000000..af4c5e2
--- /dev/null
+++ b/FS/FS/Password_Mixin.pm
@@ -0,0 +1,165 @@
+package FS::Password_Mixin;
+
+use FS::Record qw(qsearch);
+use FS::Conf;
+use FS::password_history;
+use Authen::Passphrase;
+# use Authen::Passphrase::BlowfishCrypt; # ha ha, no.
+# https://rt.cpan.org/Ticket/Display.html?id=72743
+
+our $DEBUG = 1;
+our $conf;
+FS::UID->install_callback( sub {
+    $conf = FS::Conf->new;
+    # this is safe
+    eval "use Authen::Passphrase::BlowfishCrypt;";
+});
+
+our $me = '[' . __PACKAGE__ . ']';
+
+our $BLOWFISH_COST = 10;
+
+=head1 NAME
+
+FS::Password_Mixin - Object methods for accounts that have passwords governed
+by the password policy.
+
+=head1 METHODS
+
+=over 4
+
+=item is_password_allowed PASSWORD
+
+Checks the password against the system password policy. Returns an error
+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<user-interactive> code that processes a
+password change, and only if the user has taken some action with the intent
+of changing the password.
+
+=cut
+
+sub is_password_allowed {
+  my $self = shift;
+  my $password = shift;
+
+  # check length and complexity here
+
+  if ( $conf->config('password-no_reuse') =~ /^(\d+)$/ ) {
+
+    my $no_reuse = $1;
+
+    # "the last N" passwords includes the current password and the N-1
+    # passwords before that.
+    warn "$me checking password reuse limit of $no_reuse\n" if $DEBUG;
+    my @latest = qsearch({
+        'table'     => 'password_history',
+        'hashref'   => { $self->password_history_key => $self->get($self->primary_key) },
+        'order_by'  => " ORDER BY created DESC LIMIT $no_reuse",
+    });
+
+    # don't check the first one; reusing the current password is allowed.
+    shift @latest;
+
+    foreach my $history (@latest) {
+      warn "$me previous password created ".$history->created."\n" if $DEBUG;
+      if ( $history->password_equals($password) ) {
+        my $message;
+        if ( $no_reuse == 1 ) {
+          $message = "This password is the same as your previous password.";
+        } else {
+          $message = "This password was one of the last $no_reuse passwords on this account.";
+        }
+        return $message;
+      }
+    } #foreach $history
+
+  } # end of no_reuse checking
+
+  '';
+}
+
+=item password_history_key
+
+Returns the name of the field in L<FS::password_history> that's the foreign
+key to this table.
+
+=cut
+
+sub password_history_key {
+  my $self = shift;
+  $self->table . '__' . $self->primary_key;
+}
+
+=item insert_password_history
+
+Creates a L<FS::password_history> record linked to this object, with its
+current password.
+
+=cut
+
+sub insert_password_history {
+  my $self = shift;
+  my $encoding = $self->_password_encoding;
+  my $password = $self->_password;
+  my $auth;
+
+  if ( $encoding eq 'bcrypt' or $encoding eq 'crypt' ) {
+
+    # it's smart enough to figure this out
+    $auth = Authen::Passphrase->from_crypt($password);
+
+  } elsif ( $encoding eq 'ldap' ) {
+
+    $password =~ s/^{PLAIN}/{CLEARTEXT}/i; # normalize
+    $auth = Authen::Passphrase->from_rfc2307($password);
+    if ( $auth->isa('Authen::Passphrase::Clear') ) {
+      # then we've been given the password in cleartext
+      $auth = $self->_blowfishcrypt( $auth->passphrase );
+    }
+  
+  } elsif ( $encoding eq 'plain' ) {
+
+    $auth = $self->_blowfishcrypt( $password );
+
+  }
+
+  my $password_history = FS::password_history->new({
+      _password => $auth->as_rfc2307,
+      created   => time,
+      $self->password_history_key => $self->get($self->primary_key),
+  });
+
+  my $error = $password_history->insert;
+  return "recording password history: $error" if $error;
+  '';
+
+}
+
+=item _blowfishcrypt PASSWORD
+
+For internal use: takes PASSWORD and returns a new
+L<Authen::Passphrase::BlowfishCrypt> object representing it.
+
+=cut
+
+sub _blowfishcrypt {
+  my $class = shift;
+  my $passphrase = shift;
+  return Authen::Passphrase::BlowfishCrypt->new(
+    cost => $BLOWFISH_COST,
+    salt_random => 1,
+    passphrase => $passphrase,
+  );
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::password_history>
+
+=cut
+
+1;
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index d0b69ca..e55a8f1 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -4904,6 +4904,52 @@ sub tables_hashref {
                         ],
     },
 
+    'password_history' => {
+      'columns' => [
+        'passwordnum',        'serial',  '',          '', '', '',
+        '_password',          'varchar', 'NULL', $char_d, '', '',
+        'encryption_method',  'varchar', 'NULL', $char_d, '', '',
+        'created',   @date_type,   '', '',
+        # each table that needs password history gets a column here, and
+        # an entry in foreign_keys.
+        'svc_acct__svcnum',     'int', 'NULL', '', '', '',
+        'svc_dsl__svcnum',      'int', 'NULL', '', '', '',
+        'svc_alarm__svcnum',    'int', 'NULL', '', '', '',
+        'agent__agentnum',      'int', 'NULL', '', '', '',
+        'contact__contactnum',  'int', 'NULL', '', '', '',
+        'access_user__usernum', 'int', 'NULL', '', '', '',
+      ],
+      'primary_key' => 'passwordnum',
+      'unique' => [],
+      'index'  => [],
+      'foreign_keys' => [
+                          { columns     => [ 'svc_acct__svcnum' ],
+                            table       => 'svc_acct',
+                            references  => [ 'svcnum' ],
+                          },
+                          { columns     => [ 'svc_dsl__svcnum' ],
+                            table       => 'svc_dsl',
+                            references  => [ 'svcnum' ],
+                          },
+                          { columns     => [ 'svc_alarm__svcnum' ],
+                            table       => 'svc_alarm',
+                            references  => [ 'svcnum' ],
+                          },
+                          { columns    => [ 'agent__agentnum' ],
+                            table      => 'agent',
+                            references => [ 'agentnum' ],
+                          },
+                          { columns    => [ 'contact__contactnum' ],
+                            table      => 'contact',
+                            references => [ 'contactnum' ],
+                          },
+                          { columns    => [ 'access_user__usernum' ],
+                            table      => 'access_user',
+                            references => [ 'usernum' ],
+                          },
+                        ],
+    },
+
     # name type nullability length default local
 
     #'new_table' => {
diff --git a/FS/FS/password_history.pm b/FS/FS/password_history.pm
new file mode 100644
index 0000000..dd527b9
--- /dev/null
+++ b/FS/FS/password_history.pm
@@ -0,0 +1,174 @@
+package FS::password_history;
+use base qw( FS::Record );
+
+use strict;
+use FS::Record qw( qsearch qsearchs );
+use Authen::Passphrase;
+
+# the only bit of autogenerated magic in here
+our @foreign_keys;
+FS::UID->install_callback(sub {
+    @foreign_keys = grep /__/, __PACKAGE__->dbdef_table->columns;
+});
+
+=head1 NAME
+
+FS::password_history - Object methods for password_history records
+
+=head1 SYNOPSIS
+
+  use FS::password_history;
+
+  $record = new FS::password_history \%hash;
+  $record = new FS::password_history { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::password_history object represents a current or past password used
+by a login account, employee, or other account managed within Freeside.  
+FS::password_history inherits from FS::Record.  The following fields are
+currently supported:
+
+=over 4
+
+=item passwordnum - primary key
+
+=item _password - the encrypted password, as an RFC2307-style string
+("{CRYPT}$2a$08$..." or "{MD5}1ab201f..." or similar). This is a serialized
+L<Authen::Passphrase> object.
+
+=item created - the date the password was set to this value. The record with
+the most recent created time is the current password.
+
+=back
+
+Plus one of the following foreign keys:
+
+=over 4
+
+=item svc_acct__svcnum
+
+=item svc_dsl__svcnum
+
+=item svc_alarm__svcnum
+
+=item agent__agentnum
+
+=item contact__contactnum
+
+=item access_user__usernum
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new password history record.  To add the record to the database,
+see L<"insert">.
+
+=cut
+
+sub table { 'password_history'; }
+
+=item insert
+
+=item delete
+
+=item replace OLD_RECORD
+
+=item check
+
+Checks all fields to make sure this is a valid password history record.  If
+there is an error, returns the error, otherwise returns false.  Called by the
+insert and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('passwordnum')
+    || $self->ut_anything('_password')
+    || $self->ut_numbern('create')
+    || $self->ut_numbern('create')
+  ;
+  return $error if $error;
+
+  # FKs are mutually exclusive
+  my $fk_in_use;
+  foreach my $fk ( @foreign_keys ) {
+    if ( $self->get($fk) ) {
+      $self->ut_numbern($fk);
+      return "multiple records linked to this password_history" if $fk_in_use;
+      $fk_in_use = $fk;
+    }
+  }
+
+  $self->SUPER::check;
+}
+
+=item linked_acct
+
+Returns the object that's using this password.
+
+=cut
+
+sub linked_acct {
+  my $self = shift;
+
+  foreach my $fk ( @foreign_keys ) {
+    if ( my $val = $self->get($fk) ) {
+      my ($table, $key) = split(/__/, $fk);
+      return qsearchs($table, { $key => $val });
+    }
+  }
+}
+
+=item password_equals PASSWORD
+
+Returns true if PASSWORD (plaintext) is the same as the one stored in the 
+history record, false if not.
+
+=cut
+
+sub password_equals {
+
+  my ($self, $check_password) = @_;
+
+  # _password here is always LDAP-style.
+  try {
+    my $auth = Authen::Passphrase->from_rfc2307($self->_password);
+    return $auth->match($check_password);
+  } catch {
+    # if there's somehow bad data in the _password field, then it doesn't
+    # match anything. much better than having it match _everything_.
+    warn "password_history #" . $self->passwordnum . ": $_";
+    return '';
+  }
+
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>
+
+=cut
+
+1;
+
diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm
index 9636b3e..a8cb7ba 100644
--- a/FS/FS/svc_acct.pm
+++ b/FS/FS/svc_acct.pm
@@ -7,7 +7,11 @@ use base qw( FS::svc_Domain_Mixin
              FS::svc_Radius_Mixin
              FS::svc_Tower_Mixin
              FS::svc_IP_Mixin
-             FS::svc_Common );
+             FS::Password_Mixin
+             FS::svc_Common
+           );
+
+use strict;
 use vars qw( $DEBUG $me $conf $skip_fuzzyfiles
              $dir_prefix @shells $usernamemin
              $usernamemax $passwordmin $passwordmax
@@ -701,6 +705,9 @@ sub insert {
     'child_objects' => $self->child_objects,
     %options,
   );
+
+  $error ||= $self->insert_password_history;
+
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -985,6 +992,12 @@ sub replace {
   my $dbh = dbh;
 
   $error = $new->SUPER::replace($old, @_); # usergroup here
+
+  # don't need to record this unless the password was changed
+  if ( $old->_password ne $new->_password ) {
+    $error ||= $new->insert_password_history;
+  }
+
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error if $error;
diff --git a/FS/MANIFEST b/FS/MANIFEST
index 4f3b6aa..b3679b5 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -798,3 +798,5 @@ FS/cust_pkg_reason_fee.pm
 t/cust_pkg_reason_fee.t
 FS/report_batch.pm
 t/report_batch.t
+FS/password_history.pm
+t/password_history.t
diff --git a/FS/t/password_history.t b/FS/t/password_history.t
new file mode 100644
index 0000000..b7a05fd
--- /dev/null
+++ b/FS/t/password_history.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::password_history;
+$loaded=1;
+print "ok 1\n";
diff --git a/httemplate/edit/process/svc_acct.cgi b/httemplate/edit/process/svc_acct.cgi
index 9cac2c5..d75ff92 100755
--- a/httemplate/edit/process/svc_acct.cgi
+++ b/httemplate/edit/process/svc_acct.cgi
@@ -81,7 +81,12 @@ if (     $cgi->param('clear_password') eq '*HIDDEN*'
       || $cgi->param('clear_password') =~ /^\(.* encrypted\)$/ ) {
   die "fatal: no previous account to recall hidden password from!" unless $old;
 } else {
-  $error ||= $new->set_password($cgi->param('clear_password'));
+  my $newpass = $cgi->param('clear_password');
+  if ( ! $old->check_password($newpass) ) {
+    # then the password is being changed
+    $error ||= $new->is_password_allowed($newpass)
+           ||  $new->set_password($newpass);
+  }
 }
 
 if ( ! $error ) {
diff --git a/httemplate/misc/process/change-password.html b/httemplate/misc/process/change-password.html
index 7cab9c4..d58ce54 100644
--- a/httemplate/misc/process/change-password.html
+++ b/httemplate/misc/process/change-password.html
@@ -11,7 +11,9 @@ die "access denied" unless (
   ( $curuser->access_right('Edit password') and 
     ! $part_svc->restrict_edit_password )
   );
-my $error = $svc_acct->set_password($cgi->param('password'))
+my $newpass = $cgi->param('password');
+my $error = $svc_acct->is_password_allowed($newpass)
+        ||  $svc_acct->set_password($newpass)
         ||  $svc_acct->replace;
 
 # annoyingly specific to view/svc_acct.cgi, for now...

commit 979f2b7488f2399fb2e4c7c98a57536b5cf1bff7
Author: Mark Wells <mark at freeside.biz>
Date:   Thu Nov 12 15:34:55 2015 -0800

    fix selfservice display when there are term discounts defined, looks like fallout from #15539

diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm
index b7837c5..333b07f 100644
--- a/FS/FS/cust_main/Billing.pm
+++ b/FS/FS/cust_main/Billing.pm
@@ -1357,7 +1357,8 @@ sub _make_lines {
       } else {
         # the normal case
       $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0);
-      return "unparsable frequency: ". $part_pkg->freq
+      return "unparsable frequency: ".
+        ($options{freq_override} || $part_pkg->freq)
         if $next_bill == -1;
       }  
   
diff --git a/FS/FS/cust_main/Billing_Discount.pm b/FS/FS/cust_main/Billing_Discount.pm
index 9dda389..e64c761 100644
--- a/FS/FS/cust_main/Billing_Discount.pm
+++ b/FS/FS/cust_main/Billing_Discount.pm
@@ -90,8 +90,11 @@ sub discount_terms {
 
   my @discount_pkgs = $self->_discount_pkgs_and_bill;
   shift @discount_pkgs; #discard bill;
-  
-  map { $terms{$_->months} = 1 }
+
+  # convert @discount_pkgs (the list of packages that have available discounts)
+  # to a list of distinct term lengths in months, and strip any decimal places
+  # from the number of months, not that it should have any 
+  map { $terms{sprintf('%.0f', $_->months)} = 1 }
     grep { $_->months && $_->months > 1 }
     map { $_->discount }
     map { $_->part_pkg->part_pkg_discount }

-----------------------------------------------------------------------

Summary of changes:
 FS-Test/Makefile.PL                                |   34 -
 FS-Test/README                                     |   38 +-
 FS-Test/bin/freeside-test-fetch                    |    8 +-
 FS-Test/bin/freeside-test-run                      |   14 +-
 FS-Test/bin/freeside-test-start                    |    9 +-
 FS-Test/bin/freeside-test-stop                     |    3 -
 FS-Test/lib/FS/Test.pm                             |   11 +-
 FS-Test/share/output/browse/part_pkg.cgi/active=1  |    4 +-
 .../output/browse/part_svc.cgi/orderby=active      |  266 +--
 FS-Test/share/output/edit/cust_main.cgi/135        |   88 +-
 FS-Test/share/output/edit/part_pkg.cgi/2           |  910 +++++++++--
 .../{date => keywords=OPEN90_date:order_by=invnum} |  726 ++++-----
 .../{OPEN90_date => keywords=date:order_by=invnum} | 1721 ++++++++++----------
 ...:beginning=01%2F01%2F2016:ending=01%2F31%2F2016 |    4 +-
 ...um=0:classnum=1:begin=1438412400:end=1441090800 |    4 +-
 ...um=0:classnum=1:begin=1438412400:end=1441090800 |    4 +-
 ...um=0:classnum=1:begin=1438412400:end=1441090800 |    4 +-
 .../cust_pkg.cgi/keywords=pkgnum:order_by=pkgnum   |  200 +--
 ...2F31%2F2015:pkgpart=5:pkgpart=2:order_by=pkgnum |  202 +--
 .../search/cust_pkg.cgi/magic=bill:custnum=135     |    8 +-
 ...atus=active,suspended:date=1454313600:pkgpart=2 | 1050 ++++++------
 ...ecords=100:_type=html:offset=0:order_by=ip_addr |    2 +-
 .../invnum=681:notice_name=Invoice                 |    6 +-
 FS-Test/share/output/view/svc_acct.cgi/406         |    4 +-
 FS-Test/share/output/view/svc_broadband.cgi/401    |    4 +-
 FS-Test/share/output/view/svc_domain.cgi/402       |    2 +-
 FS-Test/share/output/view/svc_phone.cgi/403        |    4 +-
 FS-Test/share/ui_tests                             |    4 +-
 FS/FS/API.pm                                       |    4 +-
 FS/FS/AccessRight.pm                               |    1 +
 FS/FS/CGI.pm                                       |    6 +-
 FS/FS/ClientAPI/MasonComponent.pm                  |    1 +
 FS/FS/ClientAPI/MyAccount.pm                       |  117 +-
 FS/FS/ClientAPI/Signup.pm                          |   19 +-
 FS/FS/ClientAPI_XMLRPC.pm                          |   10 +-
 FS/FS/Conf.pm                                      |   28 +-
 FS/FS/Mason.pm                                     |    5 +
 FS/FS/Misc/DateTime.pm                             |   22 +-
 FS/FS/Password_Mixin.pm                            |  165 ++
 FS/FS/Record.pm                                    |    8 +-
 FS/FS/Report/Table.pm                              |   59 +-
 FS/FS/Report/Tax.pm                                |  230 ++-
 FS/FS/Schema.pm                                    |   99 +-
 FS/FS/TemplateItem_Mixin.pm                        |    7 +-
 FS/FS/Template_Mixin.pm                            |   86 +-
 FS/FS/TicketSystem/RT_External.pm                  |    8 +-
 FS/FS/UI/Web.pm                                    |    1 +
 FS/FS/access_user.pm                               |   37 +
 FS/FS/cdr.pm                                       |    2 +-
 FS/FS/cdr/aapt.pm                                  |    2 +-
 FS/FS/cdr/amcom.pm                                 |   28 +-
 FS/FS/cdr/earthlink.pm                             |   32 +-
 FS/FS/cust_bill.pm                                 |   28 +-
 FS/FS/cust_credit.pm                               |    1 -
 FS/FS/cust_event.pm                                |   20 +-
 FS/FS/cust_main.pm                                 |    9 +-
 FS/FS/cust_main/Billing.pm                         |   96 +-
 FS/FS/cust_main/Billing_Discount.pm                |    7 +-
 FS/FS/cust_main/Billing_Realtime.pm                |   19 +-
 FS/FS/cust_msg.pm                                  |    1 +
 FS/FS/cust_pay.pm                                  |  118 +-
 FS/FS/cust_refund.pm                               |   50 +
 FS/FS/cust_svc.pm                                  |    3 +-
 FS/FS/deploy_zone.pm                               |  187 ++-
 FS/FS/deploy_zone_block.pm                         |    5 -
 FS/FS/discount.pm                                  |    8 +-
 FS/FS/log_context.pm                               |    2 +
 FS/FS/msg_template.pm                              |   34 +-
 FS/FS/msg_template/InitialData.pm                  |   17 +
 FS/FS/o2m_Common.pm                                |   18 +-
 FS/FS/part_event.pm                                |    2 +
 FS/FS/part_event/Action/http.pm                    |   85 +
 FS/FS/part_event/Condition.pm                      |    1 +
 FS/FS/part_event_option.pm                         |    3 +-
 FS/FS/part_pkg/agent.pm                            |    7 +-
 FS/FS/part_pkg/agent_invoice.pm                    |  225 +++
 FS/FS/part_pkg/discount_Mixin.pm                   |  203 ++-
 FS/FS/part_pkg/prorate_calendar.pm                 |    2 +-
 FS/FS/part_pkg_fcc_option.pm                       |    2 +-
 FS/FS/password_history.pm                          |  174 ++
 FS/FS/pay_batch.pm                                 |   40 +-
 FS/FS/pay_batch/RBC.pm                             |   35 +-
 FS/FS/payinfo_Mixin.pm                             |    6 +-
 FS/FS/qual.pm                                      |    2 +-
 FS/FS/queue.pm                                     |   21 +
 FS/FS/quotation_pkg.pm                             |   80 +-
 ...lientapi_session.pm => quotation_pkg_detail.pm} |   44 +-
 FS/FS/rate.pm                                      |   39 +-
 FS/FS/rate_detail.pm                               |    5 +-
 FS/FS/report_batch.pm                              |  353 ++++
 FS/FS/svc_Common.pm                                |    3 +
 FS/FS/svc_Tower_Mixin.pm                           |    8 +-
 FS/FS/svc_acct.pm                                  |   15 +-
 FS/FS/svc_circuit.pm                               |   16 +-
 FS/FS/tax_rate.pm                                  |    2 +-
 FS/MANIFEST                                        |    4 +
 .../bin/freeside-cdr-a2billing-import              |   61 +-
 FS/bin/freeside-paymentech-download                |   42 +-
 FS/bin/freeside-paymentech-upload                  |   47 +-
 FS/bin/freeside-queued                             |    6 +-
 FS/bin/freeside-rbc-download                       |    9 +-
 FS/bin/freeside-reexport                           |   17 +-
 FS/t/{AccessRight.t => password_history.t}         |    2 +-
 FS/t/{AccessRight.t => report_batch.t}             |    2 +-
 bin/cust_main-bulk_change                          |   13 +-
 conf/invoice_html                                  |   42 +-
 conf/invoice_latex                                 |   36 +-
 .../cgi/misc/{states.cgi => cities.cgi}            |    2 +-
 fs_selfservice/FS-SelfService/cgi/myaccount.html   |    3 +-
 htetc/freeside-base2.conf                          |   45 +-
 httemplate/browse/deploy_zone.html                 |    6 +-
 httemplate/browse/part_pkg.cgi                     |    9 +
 httemplate/browse/part_svc.cgi                     |    2 +-
 httemplate/browse/pkg_class.html                   |    3 +
 httemplate/browse/router.cgi                       |    4 +-
 httemplate/docs/license.html                       |    6 +
 httemplate/edit/cust_pkg_detail.html               |    3 +-
 httemplate/edit/cust_pkg_discount.html             |    2 +
 httemplate/edit/cust_refund.cgi                    |   10 +-
 httemplate/edit/deploy_zone-fixed.html             |   85 +-
 httemplate/edit/deploy_zone-mobile.html            |   21 +-
 httemplate/edit/elements/rate_detail.html          |    8 +-
 httemplate/edit/msg_template.html                  |   10 +
 httemplate/edit/part_pkg.cgi                       |   21 +-
 httemplate/edit/process/cust_refund.cgi            |   31 +-
 httemplate/edit/process/deploy_zone-fixed.html     |   35 +-
 httemplate/edit/process/deploy_zone-mobile.html    |   19 +-
 httemplate/edit/process/elements/process.html      |   23 +-
 ...t_pkg_detail.html => quotation_pkg_detail.html} |   32 +-
 httemplate/edit/process/rate_detail.html           |   17 +-
 httemplate/edit/process/svc_acct.cgi               |    7 +-
 ...t_pkg_detail.html => quotation_pkg_detail.html} |   62 +-
 httemplate/edit/rate.cgi                           |   14 +-
 httemplate/elements/contact.html                   |    2 +-
 httemplate/elements/email-link.html                |    3 +-
 httemplate/elements/form-create_ticket.html        |    2 +-
 httemplate/elements/menu.html                      |    2 +
 httemplate/elements/polygon.html                   |  127 ++
 .../elements/popup_link-make_appointment.html      |    2 +-
 .../elements/popup_link-send_report_batch.html     |   28 +
 httemplate/elements/popup_link.html                |    4 +
 httemplate/elements/popup_link_onclick.html        |   14 +-
 httemplate/elements/progress-init.html             |    2 +-
 httemplate/elements/select-terms.html              |    2 +-
 httemplate/elements/select-tower_sector.html       |    7 +-
 httemplate/elements/standardize_locations.js       |   16 +-
 httemplate/elements/tr-cust_svc.html               |   10 +
 httemplate/elements/tr-pkg_svc.html                |   64 +-
 httemplate/elements/tr-polygon.html                |    5 +
 httemplate/elements/tr-select-discount.html        |    8 +
 httemplate/graph/cust_bill_pkg.cgi                 |   18 +-
 httemplate/graph/elements/report.html              |    6 +-
 httemplate/graph/report_cust_bill_pkg.html         |    7 +-
 httemplate/images/Actions-document-edit-icon.png   |  Bin 0 -> 733 bytes
 httemplate/misc/batch-cust_pay.html                |   97 +-
 httemplate/misc/confirm-address_standardize.html   |   81 +-
 httemplate/{elements => misc}/jsrsServer.html      |    0
 .../{elements => misc}/make_appointment.html       |    2 +-
 httemplate/misc/process/change-password.html       |    4 +-
 .../misc/process/deploy_zone-block_lookup.cgi      |   13 +
 httemplate/misc/process/payment.cgi                |   82 +-
 httemplate/misc/process/send-report.html           |    7 +
 httemplate/misc/process/void-cust_bill.html        |    2 +-
 httemplate/{elements => misc}/progress-popup.html  |    2 +-
 httemplate/misc/sales.cgi                          |    2 +-
 .../{elements => misc}/schedule-appointment.html   |    0
 httemplate/misc/send-invoice.cgi                   |   10 +-
 httemplate/misc/send-report.html                   |  167 ++
 httemplate/misc/unapply-cust_pay.cgi               |    6 +-
 httemplate/search/cust_bill.html                   |    4 +-
 httemplate/search/cust_bill_pkg.cgi                |  250 ++-
 httemplate/search/cust_credit_bill_pkg.html        |  124 +-
 httemplate/search/cust_msg.html                    |    3 +-
 httemplate/search/cust_pay.html                    |    1 +
 httemplate/search/cust_pkg.cgi                     |    9 +-
 httemplate/search/elements/cust_pay_or_refund.html |   10 +
 httemplate/search/queue.html                       |    6 +
 httemplate/search/report_sales_commission_pkg.html |   10 +
 httemplate/search/report_tax-xls.cgi               |  121 +-
 httemplate/search/report_tax.cgi                   |  146 +-
 httemplate/search/sales_commission_pkg.html        |    7 +
 httemplate/search/tax_sales.cgi                    |  172 ++
 httemplate/search/tax_sales.html                   |   35 +
 httemplate/view/cust_main.cgi                      |    8 +
 httemplate/view/cust_main/billing.html             |   11 +
 httemplate/view/cust_main/payment_history.html     |    4 +-
 .../view/cust_main/payment_history/invoice.html    |   11 +-
 .../view/cust_main/payment_history/payment.html    |   22 +-
 .../view/cust_main/payment_history/refund.html     |    9 +-
 httemplate/view/cust_pay.html                      |   15 +-
 httemplate/view/cust_refund.html                   |   36 +-
 httemplate/view/cust_svc.cgi                       |    8 +-
 httemplate/view/quotation.html                     |   18 +-
 ng_selfservice/main.php                            |    8 +-
 torrus/configs/torrus-siteconfig.pl                |    2 +-
 torrus/doc/xmlconfig.pod.in                        |    4 +-
 196 files changed, 7542 insertions(+), 3384 deletions(-)
 delete mode 100644 FS-Test/Makefile.PL
 mode change 100644 => 100755 FS-Test/bin/freeside-test-run
 rename FS-Test/share/output/search/cust_bill.html/{date => keywords=OPEN90_date:order_by=invnum} (97%)
 rename FS-Test/share/output/search/cust_bill.html/{OPEN90_date => keywords=date:order_by=invnum} (84%)
 create mode 100644 FS/FS/Password_Mixin.pm
 create mode 100644 FS/FS/part_event/Action/http.pm
 create mode 100644 FS/FS/part_pkg/agent_invoice.pm
 create mode 100644 FS/FS/password_history.pm
 copy FS/FS/{clientapi_session.pm => quotation_pkg_detail.pm} (64%)
 create mode 100644 FS/FS/report_batch.pm
 rename bin/cdr-a2billing.import => FS/bin/freeside-cdr-a2billing-import (82%)
 copy FS/t/{AccessRight.t => password_history.t} (79%)
 copy FS/t/{AccessRight.t => report_batch.t} (82%)
 copy fs_selfservice/FS-SelfService/cgi/misc/{states.cgi => cities.cgi} (84%)
 copy httemplate/edit/process/{cust_pkg_detail.html => quotation_pkg_detail.html} (53%)
 copy httemplate/edit/{cust_pkg_detail.html => quotation_pkg_detail.html} (60%)
 create mode 100644 httemplate/elements/polygon.html
 create mode 100644 httemplate/elements/popup_link-send_report_batch.html
 create mode 100644 httemplate/elements/tr-polygon.html
 create mode 100644 httemplate/images/Actions-document-edit-icon.png
 rename httemplate/{elements => misc}/jsrsServer.html (100%)
 rename httemplate/{elements => misc}/make_appointment.html (92%)
 create mode 100644 httemplate/misc/process/deploy_zone-block_lookup.cgi
 create mode 100644 httemplate/misc/process/send-report.html
 rename httemplate/{elements => misc}/progress-popup.html (98%)
 rename httemplate/{elements => misc}/schedule-appointment.html (100%)
 create mode 100644 httemplate/misc/send-report.html
 create mode 100644 httemplate/search/tax_sales.cgi
 create mode 100755 httemplate/search/tax_sales.html




More information about the freeside-commits mailing list