$username_noperiod $username_nounderscore $username_nodash
$username_uppercase $username_percent
$password_noampersand $password_noexclamation
- $welcome_template $welcome_from $welcome_subject $welcome_mimetype
$warning_template $warning_from $warning_subject $warning_mimetype
$warning_cc
$smtpmachine
$radius_password $radius_ip
$dirhash
@saltset @pw_set );
+use Scalar::Util qw( blessed );
use Carp;
use Fcntl qw(:flock);
use Date::Format;
use Crypt::PasswdMD5 1.2;
use Data::Dumper;
-use FS::UID qw( datasrc );
+use Authen::Passphrase;
+use FS::UID qw( datasrc driver_name );
use FS::Conf;
use FS::Record qw( qsearch qsearchs fields dbh dbdef );
use FS::Msgcat qw(gettext);
+use FS::UI::bytecount;
use FS::svc_Common;
use FS::cust_svc;
use FS::part_svc;
$me = '[FS::svc_acct]';
#ask FS::UID to run this stuff for us later
-$FS::UID::callback{'FS::svc_acct'} = sub {
+FS::UID->install_callback( sub {
$conf = new FS::Conf;
$dir_prefix = $conf->config('home');
@shells = $conf->config('shells');
$password_noampersand = $conf->exists('password-noexclamation');
$password_noexclamation = $conf->exists('password-noexclamation');
$dirhash = $conf->config('dirhash') || 0;
- if ( $conf->exists('welcome_email') ) {
- $welcome_template = new Text::Template (
- TYPE => 'ARRAY',
- SOURCE => [ map "$_\n", $conf->config('welcome_email') ]
- ) or warn "can't create welcome email template: $Text::Template::ERROR";
- $welcome_from = $conf->config('welcome_email-from'); # || 'your-isp-is-dum'
- $welcome_subject = $conf->config('welcome_email-subject') || 'Welcome';
- $welcome_mimetype = $conf->config('welcome_email-mimetype') || 'text/plain';
- } else {
- $welcome_template = '';
- $welcome_from = '';
- $welcome_subject = '';
- $welcome_mimetype = '';
- }
if ( $conf->exists('warning_email') ) {
$warning_template = new Text::Template (
TYPE => 'ARRAY',
$smtpmachine = $conf->config('smtpmachine');
$radius_password = $conf->config('radius-password') || 'Password';
$radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
-};
+ @pw_set = ( 'A'..'Z' ) if $conf->exists('password-generated-allcaps');
+}
+);
@saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
@pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
=item _password - generated if blank
+=item _password_encoding - plain, crypt, ldap (or empty for autodetection)
+
=item sec_phrase - security phrase
=item popnum - Point of presence (see L<FS::svc_acct_pop>)
{
'name' => 'Account',
'longname_plural' => 'Access accounts and mailboxes',
- 'sorts' => [ 'username', 'uid', ],
+ 'sorts' => [ 'username', 'uid', 'seconds', 'last_login' ],
'display_weight' => 10,
'cancel_weight' => 50,
'fields' => {
select_table => 'svc_acct_pop',
select_key => 'popnum',
select_label => 'city',
+ disable_select => 1,
},
'username' => {
label => 'Username',
type => 'text',
disable_default => 1,
disable_fixed => 1,
+ disable_select => 1,
},
'quota' => {
label => 'Quota',
type => 'text',
disable_inventory => 1,
+ disable_select => 1,
},
'_password' => 'Password',
'gid' => {
label => 'Shell',
def_label=> 'Shell (set to blank for no shell tracking)',
type =>'select',
- select_list => [ $conf->config('shells') ],
+ #select_list => [ $conf->config('shells') ],
+ select_list => [ $conf ? $conf->config('shells') : () ],
disable_inventory => 1,
+ disable_select => 1,
},
'finger' => 'Real name (GECOS)',
'domsvc' => {
label => 'Domain',
- def_label => 'svcnum from svc_domain',
+ #def_label => 'svcnum from svc_domain',
type => 'select',
select_table => 'svc_domain',
select_key => 'svcnum',
select_label => 'domain',
disable_inventory => 1,
+
},
'usergroup' => {
label => 'RADIUS groups',
type => 'radius_usergroup_selector',
disable_inventory => 1,
+ disable_select => 1,
},
'seconds' => { label => 'Seconds',
+ label_sort => 'with Time Remaining',
+ type => 'text',
+ disable_inventory => 1,
+ disable_select => 1,
+ },
+ 'upbytes' => { label => 'Upload',
+ type => 'text',
+ disable_inventory => 1,
+ disable_select => 1,
+ 'format' => \&FS::UI::bytecount::display_bytecount,
+ 'parse' => \&FS::UI::bytecount::parse_bytecount,
+ },
+ 'downbytes' => { label => 'Download',
+ type => 'text',
+ disable_inventory => 1,
+ disable_select => 1,
+ 'format' => \&FS::UI::bytecount::display_bytecount,
+ 'parse' => \&FS::UI::bytecount::parse_bytecount,
+ },
+ 'totalbytes'=> { label => 'Total up and download',
type => 'text',
disable_inventory => 1,
+ disable_select => 1,
+ 'format' => \&FS::UI::bytecount::display_bytecount,
+ 'parse' => \&FS::UI::bytecount::parse_bytecount,
},
+ 'seconds_threshold' => { label => 'Seconds threshold',
+ type => 'text',
+ disable_inventory => 1,
+ disable_select => 1,
+ },
+ 'upbytes_threshold' => { label => 'Upload threshold',
+ type => 'text',
+ disable_inventory => 1,
+ disable_select => 1,
+ 'format' => \&FS::UI::bytecount::display_bytecount,
+ 'parse' => \&FS::UI::bytecount::parse_bytecount,
+ },
+ 'downbytes_threshold' => { label => 'Download threshold',
+ type => 'text',
+ disable_inventory => 1,
+ disable_select => 1,
+ 'format' => \&FS::UI::bytecount::display_bytecount,
+ 'parse' => \&FS::UI::bytecount::parse_bytecount,
+ },
+ 'totalbytes_threshold'=> { label => 'Total up and download threshold',
+ type => 'text',
+ disable_inventory => 1,
+ disable_select => 1,
+ 'format' => \&FS::UI::bytecount::display_bytecount,
+ 'parse' => \&FS::UI::bytecount::parse_bytecount,
+ },
+ 'last_login'=> {
+ label => 'Last login',
+ type => 'disabled',
+ },
+ 'last_logout'=> {
+ label => 'Last logout',
+ type => 'disabled',
+ },
},
};
}
sub table { 'svc_acct'; }
+sub table_dupcheck_fields { ( 'username', 'domsvc' ); }
+
sub _fieldhandlers {
{
#false laziness with edit/svc_acct.cgi
};
}
+sub last_login {
+ shift->_lastlog('in', @_);
+}
+
+sub last_logout {
+ shift->_lastlog('out', @_);
+}
+
+sub _lastlog {
+ my( $self, $op, $time ) = @_;
+
+ if ( defined($time) ) {
+ warn "$me last_log$op called on svcnum ". $self->svcnum.
+ ' ('. $self->email. "): $time\n"
+ if $DEBUG;
+
+ my $dbh = dbh;
+
+ my $sql = "UPDATE svc_acct SET last_log$op = ? WHERE svcnum = ?";
+ warn "$me $sql\n"
+ if $DEBUG;
+
+ my $sth = $dbh->prepare( $sql )
+ or die "Error preparing $sql: ". $dbh->errstr;
+ my $rv = $sth->execute($time, $self->svcnum);
+ die "Error executing $sql: ". $sth->errstr
+ unless defined($rv);
+ die "Can't update last_log$op for svcnum". $self->svcnum
+ if $rv == 0;
+
+ $self->{'Hash'}->{"last_log$op"} = $time;
+ }else{
+ $self->getfield("last_log$op");
+ }
+}
+
=item search_sql STRING
Class method which returns an SQL fragment to search for the given string.
$self->svcpart($cust_svc->svcpart);
}
- $error = $self->_check_duplicate;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
-
my @jobnums;
$error = $self->SUPER::insert(
'jobnums' => \@jobnums,
if ( $cust_pkg ) {
my $cust_main = $cust_pkg->cust_main;
+ my $agentnum = $cust_main->agentnum;
if ( $conf->exists('emailinvoiceautoalways')
|| $conf->exists('emailinvoiceauto')
}
#welcome email
- my $to = '';
+ my ($to,$welcome_template,$welcome_from,$welcome_subject,$welcome_subject_template,$welcome_mimetype)
+ = ('','','','','','');
+
+ if ( $conf->exists('welcome_email', $agentnum) ) {
+ $welcome_template = new Text::Template (
+ TYPE => 'ARRAY',
+ SOURCE => [ map "$_\n", $conf->config('welcome_email', $agentnum) ]
+ ) or warn "can't create welcome email template: $Text::Template::ERROR";
+ $welcome_from = $conf->config('welcome_email-from', $agentnum);
+ # || 'your-isp-is-dum'
+ $welcome_subject = $conf->config('welcome_email-subject', $agentnum)
+ || 'Welcome';
+ $welcome_subject_template = new Text::Template (
+ TYPE => 'STRING',
+ SOURCE => $welcome_subject,
+ ) or warn "can't create welcome email subject template: $Text::Template::ERROR";
+ $welcome_mimetype = $conf->config('welcome_email-mimetype', $agentnum)
+ || 'text/plain';
+ }
if ( $welcome_template && $cust_pkg ) {
my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
if ( $to ) {
+
+ my %hash = (
+ 'custnum' => $self->custnum,
+ 'username' => $self->username,
+ 'password' => $self->_password,
+ 'first' => $cust_main->first,
+ 'last' => $cust_main->getfield('last'),
+ 'pkg' => $cust_pkg->part_pkg->pkg,
+ );
my $wqueue = new FS::queue {
'svcnum' => $self->svcnum,
'job' => 'FS::svc_acct::send_email'
my $error = $wqueue->insert(
'to' => $to,
'from' => $welcome_from,
- 'subject' => $welcome_subject,
+ 'subject' => $welcome_subject_template->fill_in( HASH => \%hash, ),
'mimetype' => $welcome_mimetype,
- 'body' => $welcome_template->fill_in( HASH => {
- 'custnum' => $self->custnum,
- 'username' => $self->username,
- 'password' => $self->_password,
- 'first' => $cust_main->first,
- 'last' => $cust_main->getfield('last'),
- 'pkg' => $cust_pkg->part_pkg->pkg,
- } ),
+ 'body' => $welcome_template->fill_in( HASH => \%hash, ),
);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
}
}
+ my $error = $self->SUPER::delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
foreach my $radius_usergroup (
qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
) {
}
}
- my $error = $self->SUPER::delete;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
-
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
}
=cut
sub replace {
- my ( $new, $old ) = ( shift, shift );
- my $error;
+ my $new = shift;
+
+ my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
+ ? shift
+ : $new->replace_old;
+
warn "$me replacing $old with $new\n" if $DEBUG;
- # We absolutely have to have an old vs. new record to make this work.
- if (!defined($old)) {
- $old = qsearchs( 'svc_acct', { 'svcnum' => $new->svcnum } );
- }
+ my $error;
return "can't modify system account" if $old->_check_system;
}
- if ( $old->username ne $new->username || $old->domsvc != $new->domsvc ) {
- $new->svcpart( $new->cust_svc->svcpart ) unless $new->svcpart;
- $error = $new->_check_duplicate;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
- }
-
- $error = $new->SUPER::replace($old);
+ $error = $new->SUPER::replace($old, @_);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error if $error;
sub suspend {
my $self = shift;
return "can't suspend system account" if $self->_check_system;
- $self->SUPER::suspend;
+ $self->SUPER::suspend(@_);
}
=item unsuspend
return $error if $error;
}
- $self->SUPER::unsuspend;
+ $self->SUPER::unsuspend(@_);
}
=item cancel
}
}
- $self->SUPER::cancel;
+ $self->SUPER::cancel(@_);
}
|| $self->ut_snumbern('upbytes')
|| $self->ut_snumbern('downbytes')
|| $self->ut_snumbern('totalbytes')
+ || $self->ut_enum( '_password_encoding',
+ [ '', qw( plain crypt ldap ) ]
+ )
;
return $error if $error;
unless ( $username_ampersand ) {
$recref->{username} =~ /\&/ and return gettext('illegal_username');
}
- if ( $password_noampersand ) {
- $recref->{_password} =~ /\&/ and return gettext('illegal_password');
- }
- if ( $password_noexclamation ) {
- $recref->{_password} =~ /\!/ and return gettext('illegal_password');
- }
unless ( $username_percent ) {
$recref->{username} =~ /\%/ and return gettext('illegal_username');
}
$recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
} else {
return "Illegal shell \`". $self->shell. "\'; ".
- $conf->dir. "/shells contains: @shells";
+ "shells configuration value contains: @shells";
}
} else {
$recref->{shell} = '/bin/sync';
$self->ut_textn($_);
}
- #generate a password if it is blank
- $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
- unless ( $recref->{_password} );
-
- #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
- if ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
- $recref->{_password} = $1.$3;
- #uncomment this to encrypt password immediately upon entry, or run
- #bin/crypt_pw in cron to give new users a window during which their
- #password is available to techs, for faxing, etc. (also be aware of
- #radius issues!)
- #$recref->{password} = $1.
- # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
- #;
- } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/ ) {
- $recref->{_password} = $1.$3;
- } elsif ( $recref->{_password} eq '*' ) {
- $recref->{_password} = '*';
- } elsif ( $recref->{_password} eq '!' ) {
- $recref->{_password} = '!';
- } elsif ( $recref->{_password} eq '!!' ) {
- $recref->{_password} = '!!';
+ if ( $recref->{_password_encoding} eq 'ldap' ) {
+
+ if ( $recref->{_password} =~ /^(\{[\w\-]+\})(!?.{0,64})$/ ) {
+ $recref->{_password} = uc($1).$2;
+ } else {
+ return 'Illegal (ldap-encoded) password: '. $recref->{_password};
+ }
+
+ } elsif ( $recref->{_password_encoding} eq 'crypt' ) {
+
+ if ( $recref->{_password} =~
+ #/^(\$\w+\$.*|[\w\+\/]{13}|_[\w\+\/]{19}|\*)$/
+ /^(!!?)?(\$\w+\$.*|[\w\+\/\.]{13}|_[\w\+\/\.]{19}|\*)$/
+ ) {
+
+ $recref->{_password} = ( defined($1) ? $1 : '' ). $2;
+
+ } else {
+ return 'Illegal (crypt-encoded) password: '. $recref->{_password};
+ }
+
+ } elsif ( $recref->{_password_encoding} eq 'plain' ) {
+
+ #generate a password if it is blank
+ $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
+ unless length( $recref->{_password} );
+
+ if ( $recref->{_password} =~ /^([^\t\n]{$passwordmin,$passwordmax})$/ ) {
+ $recref->{_password} = $1;
+ } else {
+ return gettext('illegal_password'). " $passwordmin-$passwordmax ".
+ FS::Msgcat::_gettext('illegal_password_characters').
+ ": ". $recref->{_password};
+ }
+
+ if ( $password_noampersand ) {
+ $recref->{_password} =~ /\&/ and return gettext('illegal_password');
+ }
+ if ( $password_noexclamation ) {
+ $recref->{_password} =~ /\!/ and return gettext('illegal_password');
+ }
+
} else {
- #return "Illegal password";
- return gettext('illegal_password'). " $passwordmin-$passwordmax ".
- FS::Msgcat::_gettext('illegal_password_characters').
- ": ". $recref->{_password};
+
+ #carp "warning: _password_encoding unspecified\n";
+
+ #generate a password if it is blank
+ unless ( length( $recref->{_password} ) ) {
+
+ $recref->{_password} =
+ join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
+ $recref->{_password_encoding} = 'plain';
+
+ } else {
+
+ #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
+ if ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
+ $recref->{_password} = $1.$3;
+ $recref->{_password_encoding} = 'plain';
+ } elsif ( $recref->{_password} =~
+ /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/
+ ) {
+ $recref->{_password} = $1.$3;
+ $recref->{_password_encoding} = 'crypt';
+ } elsif ( $recref->{_password} eq '*' ) {
+ $recref->{_password} = '*';
+ $recref->{_password_encoding} = 'crypt';
+ } elsif ( $recref->{_password} eq '!' ) {
+ $recref->{_password_encoding} = 'crypt';
+ $recref->{_password} = '!';
+ } elsif ( $recref->{_password} eq '!!' ) {
+ $recref->{_password} = '!!';
+ $recref->{_password_encoding} = 'crypt';
+ } else {
+ #return "Illegal password";
+ return gettext('illegal_password'). " $passwordmin-$passwordmax ".
+ FS::Msgcat::_gettext('illegal_password_characters').
+ ": ". $recref->{_password};
+ }
+
+ }
+
}
$self->SUPER::check;
+
}
=item _check_system
=item _check_duplicate
-Internal function to check for duplicates usernames, username@domain pairs and
+Internal method to check for duplicates usernames, username@domain pairs and
uids.
If the I<global_unique-username> configuration value is set to B<username> or
my $global_unique = $conf->config('global_unique-username') || 'none';
return '' if $global_unique eq 'disabled';
- #this is Pg-specific. what to do for mysql etc?
- # ( mysql LOCK TABLES certainly isn't equivalent or useful here :/ )
- warn "$me locking svc_acct table for duplicate search" if $DEBUG;
- dbh->do("LOCK TABLE svc_acct IN SHARE ROW EXCLUSIVE MODE")
- or die dbh->errstr;
- warn "$me acquired svc_acct table lock for duplicate search" if $DEBUG;
+ $self->lock_table;
my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
unless ( $part_svc ) {
foreach my $dup_user ( @dup_user ) {
my $dup_svcpart = $dup_user->cust_svc->svcpart;
if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
- return "duplicate username: conflicts with svcnum ". $dup_user->svcnum.
+ return "duplicate username ". $self->username.
+ ": conflicts with svcnum ". $dup_user->svcnum.
" via exportnum ". $conflict_user_svcpart{$dup_svcpart};
}
}
foreach my $dup_userdomain ( @dup_userdomain ) {
my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
- return "duplicate username\@domain: conflicts with svcnum ".
- $dup_userdomain->svcnum. " via exportnum ".
- $conflict_userdomain_svcpart{$dup_svcpart};
+ return "duplicate username\@domain ". $self->email.
+ ": conflicts with svcnum ". $dup_userdomain->svcnum.
+ " via exportnum ". $conflict_userdomain_svcpart{$dup_svcpart};
}
}
my $dup_svcpart = $dup_uid->cust_svc->svcpart;
if ( exists($conflict_user_svcpart{$dup_svcpart})
|| exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
- return "duplicate uid: conflicts with svcnum ". $dup_uid->svcnum.
- " via exportnum ". $conflict_user_svcpart{$dup_svcpart}
- || $conflict_userdomain_svcpart{$dup_svcpart};
+ return "duplicate uid ". $self->uid.
+ ": conflicts with svcnum ". $dup_uid->svcnum.
+ " via exportnum ".
+ ( $conflict_user_svcpart{$dup_svcpart}
+ || $conflict_userdomain_svcpart{$dup_svcpart} );
}
}
( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
} grep { /^rc_/ && $self->getfield($_) } fields( $self->table );
- my $password = $self->_password;
- my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password'; $check{$pw_attrib} = $password;
+
+ my($pw_attrib, $password) = $self->radius_password;
+ $check{$pw_attrib} = $password;
my $cust_svc = $self->cust_svc;
die "FATAL: no cust_svc record for svc_acct.svcnum ". $self->svcnum. "\n"
}
+=item radius_password
+
+Returns a key/value pair containing the RADIUS attribute name and value
+for the password.
+
+=cut
+
+sub radius_password {
+ my $self = shift;
+
+ my($pw_attrib, $password);
+ if ( $self->_password_encoding eq 'ldap' ) {
+
+ $pw_attrib = 'Password-With-Header';
+ $password = $self->_password;
+
+ } elsif ( $self->_password_encoding eq 'crypt' ) {
+
+ $pw_attrib = 'Crypt-Password';
+ $password = $self->_password;
+
+ } elsif ( $self->_password_encoding eq 'plain' ) {
+
+ $pw_attrib = $radius_password; #Cleartext-Password? man rlm_pap
+ $password = $self->_password;
+
+ } else {
+
+ $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';
+ $password = $self->_password;
+
+ }
+
+ ($pw_attrib, $password);
+
+}
+
=item snapshot
This method instructs the object to "snapshot" or freeze RADIUS check and
my $action = $op2action{$op};
+ if ( &{$op2condition{$op}}($self, $column, $amount) &&
+ ( $action eq 'suspend' && !$self->overlimit
+ || $action eq 'unsuspend' && $self->overlimit )
+ ) {
+ foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
+ if ($part_export->option('overlimit_groups')) {
+ my ($new,$old);
+ my $other = new FS::svc_acct $self->hashref;
+ my $groups = &{ $self->_fieldhandlers->{'usergroup'} }
+ ($self, $part_export->option('overlimit_groups'));
+ $other->usergroup( $groups );
+ if ($action eq 'suspend'){
+ $new = $other; $old = $self;
+ }else{
+ $new = $self; $old = $other;
+ }
+ my $error = $part_export->export_replace($new, $old);
+ $error ||= $self->overlimit($action);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Error replacing radius groups in export, ${op}: $error";
+ }
+ }
+ }
+ }
+
if ( $conf->exists("svc_acct-usage_$action")
&& &{$op2condition{$op}}($self, $column, $amount) ) {
#my $error = $self->$action();
my $error = $self->cust_svc->cust_pkg->$action();
+ # $error ||= $self->overlimit($action);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return "Error ${action}ing: $error";
my $dbh = dbh;
my $reset = 0;
+ my %handyhash = ();
foreach my $field (keys %$valueref){
$reset = 1 if $valueref->{$field};
$self->setfield($field, $valueref->{$field});
)
)
);
+ $handyhash{$field} = $self->getfield($field);
+ $handyhash{$field.'_threshold'} = $self->getfield($field.'_threshold');
}
- my $error = $self->replace;
- die $error if $error;
+ #my $error = $self->replace; #NO! we avoid the call to ->check for
+ #die $error if $error; #services not explicity changed via the UI
- if ( $conf->exists("svc_acct-usage_unsuspend") && $reset ) {
- my $error = $self->cust_svc->cust_pkg->unsuspend;
+ my $sql = "UPDATE svc_acct SET " .
+ join (',', map { "$_ = ?" } (keys %handyhash) ).
+ " WHERE svcnum = ?";
+
+ warn "$me $sql\n"
+ if $DEBUG;
+
+ if (scalar(keys %handyhash)) {
+ my $sth = $dbh->prepare( $sql )
+ or die "Error preparing $sql: ". $dbh->errstr;
+ my $rv = $sth->execute((values %handyhash), $self->svcnum);
+ die "Error executing $sql: ". $sth->errstr
+ unless defined($rv);
+ die "Can't update usage for svcnum ". $self->svcnum
+ if $rv == 0;
+ }
+
+ if ( $reset ) {
+ my $error;
+
+ if ($self->overlimit) {
+ $error = $self->overlimit('unsuspend');
+ foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
+ if ($part_export->option('overlimit_groups')) {
+ my $old = new FS::svc_acct $self->hashref;
+ my $groups = &{ $self->_fieldhandlers->{'usergroup'} }
+ ($self, $part_export->option('overlimit_groups'));
+ $old->usergroup( $groups );
+ $error ||= $part_export->export_replace($self, $old);
+ }
+ }
+ }
+
+ if ( $conf->exists("svc_acct-usage_unsuspend")) {
+ $error ||= $self->cust_svc->cust_pkg->unsuspend;
+ }
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return "Error unsuspending: $error";
$self->cust_svc->get_session_history(@_);
}
+=item last_login_text
+
+Returns text describing the time of last login.
+
+=cut
+
+sub last_login_text {
+ my $self = shift;
+ $self->last_login ? ctime($self->last_login) : 'unknown';
+}
+
=item get_cdrs TIMESTAMP_START TIMESTAMP_END [ 'OPTION' => 'VALUE ... ]
=cut
#self-service and pay up
( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
- #eventually should check a "password-encoding" field
- if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
- return 0;
- } elsif ( length($password) < 13 ) { #plaintext
- $check_password eq $password;
- } elsif ( length($password) == 13 ) { #traditional DES crypt
- crypt($check_password, $password) eq $password;
- } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
- unix_md5_crypt($check_password, $password) eq $password;
- } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
- warn "Can't check password: Blowfish encryption not yet supported, svcnum".
- $self->svcnum. "\n";
- 0;
+ if ( $self->_password_encoding eq 'ldap' ) {
+
+ my $auth = from_rfc2307 Authen::Passphrase $self->_password;
+ return $auth->match($check_password);
+
+ } elsif ( $self->_password_encoding eq 'crypt' ) {
+
+ my $auth = from_crypt Authen::Passphrase $self->_password;
+ return $auth->match($check_password);
+
+ } elsif ( $self->_password_encoding eq 'plain' ) {
+
+ return $check_password eq $password;
+
} else {
- warn "Can't check password: Unrecognized encryption for svcnum ".
- $self->svcnum. "\n";
- 0;
+
+ #XXX this could be replaced with Authen::Passphrase stuff
+
+ if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
+ return 0;
+ } elsif ( length($password) < 13 ) { #plaintext
+ $check_password eq $password;
+ } elsif ( length($password) == 13 ) { #traditional DES crypt
+ crypt($check_password, $password) eq $password;
+ } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
+ unix_md5_crypt($check_password, $password) eq $password;
+ } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
+ warn "Can't check password: Blowfish encryption not yet supported, ".
+ "svcnum ". $self->svcnum. "\n";
+ 0;
+ } else {
+ warn "Can't check password: Unrecognized encryption for svcnum ".
+ $self->svcnum. "\n";
+ 0;
+ }
+
}
}
sub crypt_password {
my $self = shift;
- #eventually should check a "password-encoding" field
- if ( length($self->_password) == 13
- || $self->_password =~ /^\$(1|2a?)\$/
- || $self->_password =~ /^(\*|NP|\*LK\*|!!?)$/
- )
- {
- $self->_password;
- } else {
+
+ if ( $self->_password_encoding eq 'ldap' ) {
+
+ if ( $self->_password =~ /^\{(PLAIN|CLEARTEXT)\}(.+)$/ ) {
+ my $plain = $2;
+
+ #XXX this could be replaced with Authen::Passphrase stuff
+
+ my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
+ if ( $encryption eq 'crypt' ) {
+ crypt(
+ $self->_password,
+ $saltset[int(rand(64))].$saltset[int(rand(64))]
+ );
+ } elsif ( $encryption eq 'md5' ) {
+ unix_md5_crypt( $self->_password );
+ } elsif ( $encryption eq 'blowfish' ) {
+ croak "unknown encryption method $encryption";
+ } else {
+ croak "unknown encryption method $encryption";
+ }
+
+ } elsif ( $self->_password =~ /^\{CRYPT\}(.+)$/ ) {
+ $1;
+ }
+
+ } elsif ( $self->_password_encoding eq 'crypt' ) {
+
+ return $self->_password;
+
+ } elsif ( $self->_password_encoding eq 'plain' ) {
+
+ #XXX this could be replaced with Authen::Passphrase stuff
+
my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
if ( $encryption eq 'crypt' ) {
crypt(
} else {
croak "unknown encryption method $encryption";
}
+
+ } else {
+
+ if ( length($self->_password) == 13
+ || $self->_password =~ /^\$(1|2a?)\$/
+ || $self->_password =~ /^(\*|NP|\*LK\*|!!?)$/
+ )
+ {
+ $self->_password;
+ } else {
+
+ #XXX this could be replaced with Authen::Passphrase stuff
+
+ my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
+ if ( $encryption eq 'crypt' ) {
+ crypt(
+ $self->_password,
+ $saltset[int(rand(64))].$saltset[int(rand(64))]
+ );
+ } elsif ( $encryption eq 'md5' ) {
+ unix_md5_crypt( $self->_password );
+ } elsif ( $encryption eq 'blowfish' ) {
+ croak "unknown encryption method $encryption";
+ } else {
+ croak "unknown encryption method $encryption";
+ }
+
+ }
+
}
+
}
=item ldap_password [ DEFAULT_ENCRYPTION_TYPE ]
Returns an encrypted password in "LDAP" format, with a curly-bracked prefix
-describing the format, for example, "{CRYPT}94pAVyK/4oIBk" or
-"{PLAIN-MD5}5426824942db4253f87a1009fd5d2d4f".
+describing the format, for example, "{PLAIN}himom", "{CRYPT}94pAVyK/4oIBk" or
+"{MD5}5426824942db4253f87a1009fd5d2d4".
The optional DEFAULT_ENCRYPTION_TYPE is not yet used, but the idea is for it
to work the same as the B</crypt_password> method.
sub ldap_password {
my $self = shift;
#eventually should check a "password-encoding" field
- if ( length($self->_password) == 13 ) { #crypt
- return '{CRYPT}'. $self->_password;
- } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
- return '{MD5}'. $1;
- } elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
- die "Blowfish encryption not supported in this context, svcnum ".
- $self->svcnum. "\n";
- } elsif ( $self->_password =~ /^(\w{48})$/ ) { #LDAP SSHA
- return '{SSHA}'. $1;
- } elsif ( $self->_password =~ /^(\w{64})$/ ) { #LDAP NS-MTA-MD5
- return '{NS-MTA-MD5}'. $1;
- } else { #plaintext
+
+ if ( $self->_password_encoding eq 'ldap' ) {
+
+ return $self->_password;
+
+ } elsif ( $self->_password_encoding eq 'crypt' ) {
+
+ if ( length($self->_password) == 13 ) { #crypt
+ return '{CRYPT}'. $self->_password;
+ } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
+ return '{MD5}'. $1;
+ #} elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
+ # die "Blowfish encryption not supported in this context, svcnum ".
+ # $self->svcnum. "\n";
+ } else {
+ warn "encryption method not (yet?) supported in LDAP context";
+ return '{CRYPT}*'; #unsupported, should not auth
+ }
+
+ } elsif ( $self->_password_encoding eq 'plain' ) {
+
return '{PLAIN}'. $self->_password;
- #my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
- #if ( $encryption eq 'crypt' ) {
- # return '{CRYPT}'. crypt(
- # $self->_password,
- # $saltset[int(rand(64))].$saltset[int(rand(64))]
- # );
- #} elsif ( $encryption eq 'md5' ) {
- # unix_md5_crypt( $self->_password );
- #} elsif ( $encryption eq 'blowfish' ) {
- # croak "unknown encryption method $encryption";
- #} else {
- # croak "unknown encryption method $encryption";
- #}
+
+ #return '{CLEARTEXT}'. $self->_password; #?
+
+ } else {
+
+ if ( length($self->_password) == 13 ) { #crypt
+ return '{CRYPT}'. $self->_password;
+ } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
+ return '{MD5}'. $1;
+ } elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
+ warn "Blowfish encryption not supported in this context, svcnum ".
+ $self->svcnum. "\n";
+ return '{CRYPT}*';
+
+ #are these two necessary anymore?
+ } elsif ( $self->_password =~ /^(\w{48})$/ ) { #LDAP SSHA
+ return '{SSHA}'. $1;
+ } elsif ( $self->_password =~ /^(\w{64})$/ ) { #LDAP NS-MTA-MD5
+ return '{NS-MTA-MD5}'. $1;
+
+ } else { #plaintext
+ return '{PLAIN}'. $self->_password;
+
+ #return '{CLEARTEXT}'. $self->_password; #?
+
+ #XXX this could be replaced with Authen::Passphrase stuff if it gets used
+ #my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
+ #if ( $encryption eq 'crypt' ) {
+ # return '{CRYPT}'. crypt(
+ # $self->_password,
+ # $saltset[int(rand(64))].$saltset[int(rand(64))]
+ # );
+ #} elsif ( $encryption eq 'md5' ) {
+ # unix_md5_crypt( $self->_password );
+ #} elsif ( $encryption eq 'blowfish' ) {
+ # croak "unknown encryption method $encryption";
+ #} else {
+ # croak "unknown encryption method $encryption";
+ #}
+ }
+
}
+
}
=item domain_slash_username
=cut
sub check_and_rebuild_fuzzyfiles {
- my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
+ my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
-e "$dir/svc_acct.username"
or &rebuild_fuzzyfiles;
}
use Fcntl qw(:flock);
- my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
+ my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
#username
=cut
sub all_username {
- my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
+ my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
open(USERNAMECACHE,"<$dir/svc_acct.username")
or die "can't open $dir/svc_acct.username: $!";
my @array = map { chomp; $_; } <USERNAMECACHE>;
use Fcntl qw(:flock);
- my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
+ my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
open(USERNAME,">>$dir/svc_acct.username")
or die "can't open $dir/svc_acct.username: $!";
my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ }
$cust_main->invoicing_list,
- $svc_acct->email,
($opt{'to'} ? $opt{'to'} : ())
);
'last' => $cust_main->getfield('last'),
'pkg' => $cust_pkg->part_pkg->pkg,
'column' => $opt{'column'},
- 'amount' => $svc_acct->getfield($opt{'column'}),
- 'threshold' => $threshold,
+ 'amount' => $opt{'column'} =~/bytes/
+ ? FS::UI::bytecount::display_bytecount($svc_acct->getfield($opt{'column'}))
+ : $svc_acct->getfield($opt{'column'}),
+ 'threshold' => $opt{'column'} =~/bytes/
+ ? FS::UI::bytecount::display_bytecount($threshold)
+ : $threshold,
} );
=cut
+=item domain_select_hash %OPTIONS
+
+Returns a hash SVCNUM => DOMAIN ... representing the domains this customer
+may at present purchase.
+
+Currently available options are: I<pkgnum> I<svcpart>
+
+=cut
+
+sub domain_select_hash {
+ my ($self, %options) = @_;
+ my %domains = ();
+ my $part_svc;
+ my $cust_pkg;
+
+ if (ref($self)) {
+ $part_svc = $self->part_svc;
+ $cust_pkg = $self->cust_svc->cust_pkg
+ if $self->cust_svc;
+ }
+
+ $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} })
+ if $options{'svcpart'};
+
+ $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} })
+ if $options{'pkgnum'};
+
+ if ($part_svc && ( $part_svc->part_svc_column('domsvc')->columnflag eq 'S'
+ || $part_svc->part_svc_column('domsvc')->columnflag eq 'F')) {
+ %domains = map { $_->svcnum => $_->domain }
+ map { qsearchs('svc_domain', { 'svcnum' => $_ }) }
+ split(',', $part_svc->part_svc_column('domsvc')->columnvalue);
+ }elsif ($cust_pkg && !$conf->exists('svc_acct-alldomains') ) {
+ %domains = map { $_->svcnum => $_->domain }
+ map { qsearchs('svc_domain', { 'svcnum' => $_->svcnum }) }
+ map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
+ qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum });
+ }else{
+ %domains = map { $_->svcnum => $_->domain } qsearch('svc_domain', {} );
+ }
+
+ if ($part_svc && $part_svc->part_svc_column('domsvc')->columnflag eq 'D') {
+ my $svc_domain = qsearchs('svc_domain',
+ { 'svcnum' => $part_svc->part_svc_column('domsvc')->columnvalue } );
+ if ( $svc_domain ) {
+ $domains{$svc_domain->svcnum} = $svc_domain->domain;
+ }else{
+ warn "unknown svc_domain.svcnum for part_svc_column domsvc: ".
+ $part_svc->part_svc_column('domsvc')->columnvalue;
+
+ }
+ }
+
+ (%domains);
+}
+
1;