4 use vars qw( @ISA $DEBUG $me $conf
5 $dir_prefix @shells $usernamemin
6 $usernamemax $passwordmin $passwordmax
7 $username_ampersand $username_letter $username_letterfirst
8 $username_noperiod $username_nounderscore $username_nodash
10 $password_noampersand $password_noexclamation
12 $welcome_template $welcome_from $welcome_subject $welcome_mimetype
14 $radius_password $radius_ip
20 use FS::UID qw( datasrc );
22 use FS::Record qw( qsearch qsearchs fields dbh dbdef );
29 use FS::cust_main_invoice;
33 use FS::radius_usergroup;
36 use FS::Msgcat qw(gettext);
40 @ISA = qw( FS::svc_Common );
44 $me = '[FS::svc_acct]';
46 #ask FS::UID to run this stuff for us later
47 $FS::UID::callback{'FS::svc_acct'} = sub {
49 $dir_prefix = $conf->config('home');
50 @shells = $conf->config('shells');
51 $usernamemin = $conf->config('usernamemin') || 2;
52 $usernamemax = $conf->config('usernamemax');
53 $passwordmin = $conf->config('passwordmin') || 6;
54 $passwordmax = $conf->config('passwordmax') || 8;
55 $username_letter = $conf->exists('username-letter');
56 $username_letterfirst = $conf->exists('username-letterfirst');
57 $username_noperiod = $conf->exists('username-noperiod');
58 $username_nounderscore = $conf->exists('username-nounderscore');
59 $username_nodash = $conf->exists('username-nodash');
60 $username_uppercase = $conf->exists('username-uppercase');
61 $username_ampersand = $conf->exists('username-ampersand');
62 $password_noampersand = $conf->exists('password-noexclamation');
63 $password_noexclamation = $conf->exists('password-noexclamation');
64 $mydomain = $conf->config('domain');
65 $dirhash = $conf->config('dirhash') || 0;
66 if ( $conf->exists('welcome_email') ) {
67 $welcome_template = new Text::Template (
69 SOURCE => [ map "$_\n", $conf->config('welcome_email') ]
70 ) or warn "can't create welcome email template: $Text::Template::ERROR";
71 $welcome_from = $conf->config('welcome_email-from'); # || 'your-isp-is-dum'
72 $welcome_subject = $conf->config('welcome_email-subject') || 'Welcome';
73 $welcome_mimetype = $conf->config('welcome_email-mimetype') || 'text/plain';
75 $welcome_template = '';
77 $welcome_subject = '';
78 $welcome_mimetype = '';
80 $smtpmachine = $conf->config('smtpmachine');
81 $radius_password = $conf->config('radius-password') || 'Password';
82 $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
85 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
86 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
90 my ( $hashref, $cache ) = @_;
91 if ( $hashref->{'svc_acct_svcnum'} ) {
92 $self->{'_domsvc'} = FS::svc_domain->new( {
93 'svcnum' => $hashref->{'domsvc'},
94 'domain' => $hashref->{'svc_acct_domain'},
95 'catchall' => $hashref->{'svc_acct_catchall'},
102 FS::svc_acct - Object methods for svc_acct records
108 $record = new FS::svc_acct \%hash;
109 $record = new FS::svc_acct { 'column' => 'value' };
111 $error = $record->insert;
113 $error = $new_record->replace($old_record);
115 $error = $record->delete;
117 $error = $record->check;
119 $error = $record->suspend;
121 $error = $record->unsuspend;
123 $error = $record->cancel;
125 %hash = $record->radius;
127 %hash = $record->radius_reply;
129 %hash = $record->radius_check;
131 $domain = $record->domain;
133 $svc_domain = $record->svc_domain;
135 $email = $record->email;
137 $seconds_since = $record->seconds_since($timestamp);
141 An FS::svc_acct object represents an account. FS::svc_acct inherits from
142 FS::svc_Common. The following fields are currently supported:
146 =item svcnum - primary key (assigned automatcially for new accounts)
150 =item _password - generated if blank
152 =item sec_phrase - security phrase
154 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
162 =item dir - set automatically if blank (and uid is not)
166 =item quota - (unimplementd)
168 =item slipip - IP address
172 =item domsvc - svcnum from svc_domain
174 =item radius_I<Radius_Attribute> - I<Radius-Attribute>
184 Creates a new account. To add the account to the database, see L<"insert">.
188 sub table { 'svc_acct'; }
190 =item insert [ , OPTION => VALUE ... ]
192 Adds this account to the database. If there is an error, returns the error,
193 otherwise returns false.
195 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
196 defined. An FS::cust_svc record will be created and inserted.
198 The additional field I<usergroup> can optionally be defined; if so it should
199 contain an arrayref of group names. See L<FS::radius_usergroup>.
201 The additional field I<child_objects> can optionally be defined; if so it
202 should contain an arrayref of FS::tablename objects. They will have their
203 svcnum fields set and will be inserted after this record, but before any
206 Currently available options are: I<depend_jobnum>
208 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
209 jobnums), all provisioning jobs will have a dependancy on the supplied
210 jobnum(s) (they will not run until the specific job(s) complete(s)).
212 (TODOC: L<FS::queue> and L<freeside-queued>)
214 (TODOC: new exports!)
223 local $SIG{HUP} = 'IGNORE';
224 local $SIG{INT} = 'IGNORE';
225 local $SIG{QUIT} = 'IGNORE';
226 local $SIG{TERM} = 'IGNORE';
227 local $SIG{TSTP} = 'IGNORE';
228 local $SIG{PIPE} = 'IGNORE';
230 my $oldAutoCommit = $FS::UID::AutoCommit;
231 local $FS::UID::AutoCommit = 0;
234 $error = $self->check;
235 return $error if $error;
237 if ( $self->svcnum && qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) ) {
238 my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
239 unless ( $cust_svc ) {
240 $dbh->rollback if $oldAutoCommit;
241 return "no cust_svc record found for svcnum ". $self->svcnum;
243 $self->pkgnum($cust_svc->pkgnum);
244 $self->svcpart($cust_svc->svcpart);
247 $error = $self->_check_duplicate;
249 $dbh->rollback if $oldAutoCommit;
254 $error = $self->SUPER::insert(
255 'jobnums' => \@jobnums,
256 'child_objects' => $self->child_objects,
260 $dbh->rollback if $oldAutoCommit;
264 if ( $self->usergroup ) {
265 foreach my $groupname ( @{$self->usergroup} ) {
266 my $radius_usergroup = new FS::radius_usergroup ( {
267 svcnum => $self->svcnum,
268 groupname => $groupname,
270 my $error = $radius_usergroup->insert;
272 $dbh->rollback if $oldAutoCommit;
278 #false laziness with sub replace (and cust_main)
279 my $queue = new FS::queue {
280 'svcnum' => $self->svcnum,
281 'job' => 'FS::svc_acct::append_fuzzyfiles'
283 $error = $queue->insert($self->username);
285 $dbh->rollback if $oldAutoCommit;
286 return "queueing job (transaction rolled back): $error";
289 my $cust_pkg = $self->cust_svc->cust_pkg;
292 my $cust_main = $cust_pkg->cust_main;
294 if ( $conf->exists('emailinvoiceauto') ) {
295 my @invoicing_list = $cust_main->invoicing_list;
296 push @invoicing_list, $self->email;
297 $cust_main->invoicing_list(\@invoicing_list);
302 if ( $welcome_template && $cust_pkg ) {
303 my $to = join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list );
305 my $wqueue = new FS::queue {
306 'svcnum' => $self->svcnum,
307 'job' => 'FS::svc_acct::send_email'
309 my $error = $wqueue->insert(
311 'from' => $welcome_from,
312 'subject' => $welcome_subject,
313 'mimetype' => $welcome_mimetype,
314 'body' => $welcome_template->fill_in( HASH => {
315 'custnum' => $self->custnum,
316 'username' => $self->username,
317 'password' => $self->_password,
318 'first' => $cust_main->first,
319 'last' => $cust_main->getfield('last'),
320 'pkg' => $cust_pkg->part_pkg->pkg,
324 $dbh->rollback if $oldAutoCommit;
325 return "error queuing welcome email: $error";
328 if ( $options{'depend_jobnum'} ) {
329 warn "$me depend_jobnum found; adding to welcome email dependancies"
331 if ( ref($options{'depend_jobnum'}) ) {
332 warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
333 "to welcome email dependancies"
335 push @jobnums, @{ $options{'depend_jobnum'} };
337 warn "$me adding job $options{'depend_jobnum'} ".
338 "to welcome email dependancies"
340 push @jobnums, $options{'depend_jobnum'};
344 foreach my $jobnum ( @jobnums ) {
345 my $error = $wqueue->depend_insert($jobnum);
347 $dbh->rollback if $oldAutoCommit;
348 return "error queuing welcome email job dependancy: $error";
358 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
364 Deletes this account from the database. If there is an error, returns the
365 error, otherwise returns false.
367 The corresponding FS::cust_svc record will be deleted as well.
369 (TODOC: new exports!)
376 if ( defined( $FS::Record::dbdef->table('svc_acct_sm') ) ) {
377 return "Can't delete an account which has (svc_acct_sm) mail aliases!"
378 if $self->uid && qsearch( 'svc_acct_sm', { 'domuid' => $self->uid } );
381 return "can't delete system account" if $self->_check_system;
383 return "Can't delete an account which is a (svc_forward) source!"
384 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
386 return "Can't delete an account which is a (svc_forward) destination!"
387 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
389 return "Can't delete an account with (svc_www) web service!"
390 if qsearch( 'svc_www', { 'usersvc' => $self->svcnum } );
392 # what about records in session ? (they should refer to history table)
394 local $SIG{HUP} = 'IGNORE';
395 local $SIG{INT} = 'IGNORE';
396 local $SIG{QUIT} = 'IGNORE';
397 local $SIG{TERM} = 'IGNORE';
398 local $SIG{TSTP} = 'IGNORE';
399 local $SIG{PIPE} = 'IGNORE';
401 my $oldAutoCommit = $FS::UID::AutoCommit;
402 local $FS::UID::AutoCommit = 0;
405 foreach my $cust_main_invoice (
406 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
408 unless ( defined($cust_main_invoice) ) {
409 warn "WARNING: something's wrong with qsearch";
412 my %hash = $cust_main_invoice->hash;
413 $hash{'dest'} = $self->email;
414 my $new = new FS::cust_main_invoice \%hash;
415 my $error = $new->replace($cust_main_invoice);
417 $dbh->rollback if $oldAutoCommit;
422 foreach my $svc_domain (
423 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
425 my %hash = new FS::svc_domain->hash;
426 $hash{'catchall'} = '';
427 my $new = new FS::svc_domain \%hash;
428 my $error = $new->replace($svc_domain);
430 $dbh->rollback if $oldAutoCommit;
435 foreach my $radius_usergroup (
436 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
438 my $error = $radius_usergroup->delete;
440 $dbh->rollback if $oldAutoCommit;
445 my $error = $self->SUPER::delete;
447 $dbh->rollback if $oldAutoCommit;
451 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
455 =item replace OLD_RECORD
457 Replaces OLD_RECORD with this one in the database. If there is an error,
458 returns the error, otherwise returns false.
460 The additional field I<usergroup> can optionally be defined; if so it should
461 contain an arrayref of group names. See L<FS::radius_usergroup>.
467 my ( $new, $old ) = ( shift, shift );
469 warn "$me replacing $old with $new\n" if $DEBUG;
471 return "can't modify system account" if $old->_check_system;
473 return "Username in use"
474 if $old->username ne $new->username &&
475 qsearchs( 'svc_acct', { 'username' => $new->username,
476 'domsvc' => $new->domsvc,
479 #no warnings 'numeric'; #alas, a 5.006-ism
481 return "Can't change uid!" if $old->uid != $new->uid;
484 #change homdir when we change username
485 $new->setfield('dir', '') if $old->username ne $new->username;
487 local $SIG{HUP} = 'IGNORE';
488 local $SIG{INT} = 'IGNORE';
489 local $SIG{QUIT} = 'IGNORE';
490 local $SIG{TERM} = 'IGNORE';
491 local $SIG{TSTP} = 'IGNORE';
492 local $SIG{PIPE} = 'IGNORE';
494 my $oldAutoCommit = $FS::UID::AutoCommit;
495 local $FS::UID::AutoCommit = 0;
498 # redundant, but so $new->usergroup gets set
499 $error = $new->check;
500 return $error if $error;
502 $old->usergroup( [ $old->radius_groups ] );
503 warn "old groups: ". join(' ',@{$old->usergroup}). "\n" if $DEBUG;
504 warn "new groups: ". join(' ',@{$new->usergroup}). "\n" if $DEBUG;
505 if ( $new->usergroup ) {
506 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
507 my @newgroups = @{$new->usergroup};
508 foreach my $oldgroup ( @{$old->usergroup} ) {
509 if ( grep { $oldgroup eq $_ } @newgroups ) {
510 @newgroups = grep { $oldgroup ne $_ } @newgroups;
513 my $radius_usergroup = qsearchs('radius_usergroup', {
514 svcnum => $old->svcnum,
515 groupname => $oldgroup,
517 my $error = $radius_usergroup->delete;
519 $dbh->rollback if $oldAutoCommit;
520 return "error deleting radius_usergroup $oldgroup: $error";
524 foreach my $newgroup ( @newgroups ) {
525 my $radius_usergroup = new FS::radius_usergroup ( {
526 svcnum => $new->svcnum,
527 groupname => $newgroup,
529 my $error = $radius_usergroup->insert;
531 $dbh->rollback if $oldAutoCommit;
532 return "error adding radius_usergroup $newgroup: $error";
538 if ( $old->username ne $new->username || $old->domsvc != $new->domsvc ) {
539 $new->svcpart( $new->cust_svc->svcpart ) unless $new->svcpart;
540 $error = $new->_check_duplicate;
542 $dbh->rollback if $oldAutoCommit;
547 $error = $new->SUPER::replace($old);
549 $dbh->rollback if $oldAutoCommit;
550 return $error if $error;
553 if ( $new->username ne $old->username ) {
554 #false laziness with sub insert (and cust_main)
555 my $queue = new FS::queue {
556 'svcnum' => $new->svcnum,
557 'job' => 'FS::svc_acct::append_fuzzyfiles'
559 $error = $queue->insert($new->username);
561 $dbh->rollback if $oldAutoCommit;
562 return "queueing job (transaction rolled back): $error";
566 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
572 Suspends this account by prefixing *SUSPENDED* to the password. If there is an
573 error, returns the error, otherwise returns false.
575 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
577 Calls any export-specific suspend hooks.
583 return "can't suspend system account" if $self->_check_system;
584 $self->SUPER::suspend;
589 Unsuspends this account by removing *SUSPENDED* from the password. If there is
590 an error, returns the error, otherwise returns false.
592 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
594 Calls any export-specific unsuspend hooks.
600 my %hash = $self->hash;
601 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
602 $hash{_password} = $1;
603 my $new = new FS::svc_acct ( \%hash );
604 my $error = $new->replace($self);
605 return $error if $error;
608 $self->SUPER::unsuspend;
613 Just returns false (no error) for now.
615 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
619 Checks all fields to make sure this is a valid service. If there is an error,
620 returns the error, otherwise returns false. Called by the insert and replace
623 Sets any fixed values; see L<FS::part_svc>.
630 my($recref) = $self->hashref;
632 my $x = $self->setfixed;
633 return $x unless ref($x);
636 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
638 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
641 my $error = $self->ut_numbern('svcnum')
642 #|| $self->ut_number('domsvc')
643 || $self->ut_foreign_key('domsvc', 'svc_domain', 'svcnum' )
644 || $self->ut_textn('sec_phrase')
646 return $error if $error;
648 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
649 if ( $username_uppercase ) {
650 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/i
651 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
652 $recref->{username} = $1;
654 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/
655 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
656 $recref->{username} = $1;
659 if ( $username_letterfirst ) {
660 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
661 } elsif ( $username_letter ) {
662 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
664 if ( $username_noperiod ) {
665 $recref->{username} =~ /\./ and return gettext('illegal_username');
667 if ( $username_nounderscore ) {
668 $recref->{username} =~ /_/ and return gettext('illegal_username');
670 if ( $username_nodash ) {
671 $recref->{username} =~ /\-/ and return gettext('illegal_username');
673 unless ( $username_ampersand ) {
674 $recref->{username} =~ /\&/ and return gettext('illegal_username');
676 if ( $password_noampersand ) {
677 $recref->{_password} =~ /\&/ and return gettext('illegal_password');
679 if ( $password_noexclamation ) {
680 $recref->{_password} =~ /\!/ and return gettext('illegal_password');
683 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
684 $recref->{popnum} = $1;
685 return "Unknown popnum" unless
686 ! $recref->{popnum} ||
687 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
689 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
691 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
692 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
694 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
695 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
696 #not all systems use gid=uid
697 #you can set a fixed gid in part_svc
699 return "Only root can have uid 0"
700 if $recref->{uid} == 0
701 && $recref->{username} ne 'root'
702 && $recref->{username} ne 'toor';
705 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
706 or return "Illegal directory: ". $recref->{dir};
708 return "Illegal directory"
709 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
710 return "Illegal directory"
711 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
712 unless ( $recref->{dir} ) {
713 $recref->{dir} = $dir_prefix . '/';
714 if ( $dirhash > 0 ) {
715 for my $h ( 1 .. $dirhash ) {
716 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
718 } elsif ( $dirhash < 0 ) {
719 for my $h ( reverse $dirhash .. -1 ) {
720 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
723 $recref->{dir} .= $recref->{username};
727 unless ( $recref->{username} eq 'sync' ) {
728 if ( grep $_ eq $recref->{shell}, @shells ) {
729 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
731 return "Illegal shell \`". $self->shell. "\'; ".
732 $conf->dir. "/shells contains: @shells";
735 $recref->{shell} = '/bin/sync';
739 $recref->{gid} ne '' ?
740 return "Can't have gid without uid" : ( $recref->{gid}='' );
741 $recref->{dir} ne '' ?
742 return "Can't have directory without uid" : ( $recref->{dir}='' );
743 $recref->{shell} ne '' ?
744 return "Can't have shell without uid" : ( $recref->{shell}='' );
747 # $error = $self->ut_textn('finger');
748 # return $error if $error;
749 if ( $self->getfield('finger') eq '' ) {
750 my $cust_pkg = $self->svcnum
751 ? $self->cust_svc->cust_pkg
752 : qsearchs('cust_pkg', { 'pkgnum' => $self->getfield('pkgnum') } );
754 my $cust_main = $cust_pkg->cust_main;
755 $self->setfield('finger', $cust_main->first.' '.$cust_main->get('last') );
758 $self->getfield('finger') =~
759 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
760 or return "Illegal finger: ". $self->getfield('finger');
761 $self->setfield('finger', $1);
763 $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota";
764 $recref->{quota} = $1;
766 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
767 if ( $recref->{slipip} eq '' ) {
768 $recref->{slipip} = '';
769 } elsif ( $recref->{slipip} eq '0e0' ) {
770 $recref->{slipip} = '0e0';
772 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
773 or return "Illegal slipip". $self->slipip;
774 $recref->{slipip} = $1;
779 #arbitrary RADIUS stuff; allow ut_textn for now
780 foreach ( grep /^radius_/, fields('svc_acct') ) {
784 #generate a password if it is blank
785 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
786 unless ( $recref->{_password} );
788 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
789 if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
790 $recref->{_password} = $1.$3;
791 #uncomment this to encrypt password immediately upon entry, or run
792 #bin/crypt_pw in cron to give new users a window during which their
793 #password is available to techs, for faxing, etc. (also be aware of
795 #$recref->{password} = $1.
796 # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
798 } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$\;\+]{13,60})$/ ) {
799 $recref->{_password} = $1.$3;
800 } elsif ( $recref->{_password} eq '*' ) {
801 $recref->{_password} = '*';
802 } elsif ( $recref->{_password} eq '!' ) {
803 $recref->{_password} = '!';
804 } elsif ( $recref->{_password} eq '!!' ) {
805 $recref->{_password} = '!!';
807 #return "Illegal password";
808 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
809 FS::Msgcat::_gettext('illegal_password_characters').
810 ": ". $recref->{_password};
822 scalar( grep { $self->username eq $_ || $self->email eq $_ }
823 $conf->config('system_usernames')
827 =item _check_duplicate
829 Internal function to check for duplicates usernames, username@domain pairs and
832 If the I<global_unique-username> configuration value is set to B<username> or
833 B<username@domain>, enforces global username or username@domain uniqueness.
835 In all cases, check for duplicate uids and usernames or username@domain pairs
836 per export and with identical I<svcpart> values.
840 sub _check_duplicate {
843 #this is Pg-specific. what to do for mysql etc?
844 # ( mysql LOCK TABLES certainly isn't equivalent or useful here :/ )
845 warn "$me locking svc_acct table for duplicate search" if $DEBUG;
846 dbh->do("LOCK TABLE svc_acct IN SHARE ROW EXCLUSIVE MODE")
848 warn "$me acquired svc_acct table lock for duplicate search" if $DEBUG;
850 my $svcpart = $self->svcpart;
851 my $part_svc = qsearchs('part_svc', { 'svcpart' => $svcpart } );
852 unless ( $part_svc ) {
853 return 'unknown svcpart '. $self->svcpart;
856 my $global_unique = $conf->config('global_unique-username');
858 my @dup_user = grep { $svcpart != $_->svcpart }
859 qsearch( 'svc_acct', { 'username' => $self->username } );
860 return gettext('username_in_use')
861 if $global_unique eq 'username' && @dup_user;
863 my @dup_userdomain = grep { $svcpart != $_->svcpart }
864 qsearch( 'svc_acct', { 'username' => $self->username,
865 'domsvc' => $self->domsvc } );
866 return gettext('username_in_use')
867 if $global_unique eq 'username@domain' && @dup_userdomain;
870 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
871 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
872 @dup_uid = grep { $svcpart != $_->svcpart }
873 qsearch( 'svc_acct', { 'uid' => $self->uid } );
878 if ( @dup_user || @dup_userdomain || @dup_uid ) {
879 my $exports = FS::part_export::export_info('svc_acct');
880 my %conflict_user_svcpart;
881 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
883 foreach my $part_export ( $part_svc->part_export ) {
885 #this will catch to the same exact export
886 my @svcparts = map { $_->svcpart } $part_export->export_svc;
888 #this will catch to exports w/same exporthost+type ???
889 #my @other_part_export = qsearch('part_export', {
890 # 'machine' => $part_export->machine,
891 # 'exporttype' => $part_export->exporttype,
893 #foreach my $other_part_export ( @other_part_export ) {
894 # push @svcparts, map { $_->svcpart }
895 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
898 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
899 #silly kludge to avoid uninitialized value errors
900 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
901 ? $exports->{$part_export->exporttype}{'nodomain'}
903 if ( $nodomain =~ /^Y/i ) {
904 $conflict_user_svcpart{$_} = $part_export->exportnum
907 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
912 foreach my $dup_user ( @dup_user ) {
913 my $dup_svcpart = $dup_user->cust_svc->svcpart;
914 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
915 return "duplicate username: conflicts with svcnum ". $dup_user->svcnum.
916 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
920 foreach my $dup_userdomain ( @dup_userdomain ) {
921 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
922 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
923 return "duplicate username\@domain: conflicts with svcnum ".
924 $dup_userdomain->svcnum. " via exportnum ".
925 $conflict_userdomain_svcpart{$dup_svcpart};
929 foreach my $dup_uid ( @dup_uid ) {
930 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
931 if ( exists($conflict_user_svcpart{$dup_svcpart})
932 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
933 return "duplicate uid: conflicts with svcnum". $dup_uid->svcnum.
934 "via exportnum ". $conflict_user_svcpart{$dup_svcpart}
935 || $conflict_userdomain_svcpart{$dup_svcpart};
947 Depriciated, use radius_reply instead.
952 carp "FS::svc_acct::radius depriciated, use radius_reply";
958 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
959 reply attributes of this record.
961 Note that this is now the preferred method for reading RADIUS attributes -
962 accessing the columns directly is discouraged, as the column names are
963 expected to change in the future.
965 Internal function to check the username against the list of system usernames
966 from the I<system_usernames> configuration value. Returns true if the username
967 is listed on the system username list.
976 my($column, $attrib) = ($1, $2);
977 #$attrib =~ s/_/\-/g;
978 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
979 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
980 if ( $self->slipip && $self->slipip ne '0e0' ) {
981 $reply{$radius_ip} = $self->slipip;
988 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
989 check attributes of this record.
991 Note that this is now the preferred method for reading RADIUS attributes -
992 accessing the columns directly is discouraged, as the column names are
993 expected to change in the future.
999 my $password = $self->_password;
1000 my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';
1001 ( $pw_attrib => $self->_password,
1004 my($column, $attrib) = ($1, $2);
1005 #$attrib =~ s/_/\-/g;
1006 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1007 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table )
1013 Returns the domain associated with this account.
1019 if ( $self->domsvc ) {
1020 #$self->svc_domain->domain;
1021 my $svc_domain = $self->svc_domain
1022 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
1023 $svc_domain->domain;
1025 $mydomain or die "svc_acct.domsvc is null and no legacy domain config file";
1031 Returns the FS::svc_domain record for this account's domain (see
1039 ? $self->{'_domsvc'}
1040 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
1045 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
1051 qsearchs( 'cust_svc', { 'svcnum' => $self->svcnum } );
1056 Returns an email address associated with the account.
1062 $self->username. '@'. $self->domain;
1067 Returns an array of FS::acct_snarf records associated with the account.
1068 If the acct_snarf table does not exist or there are no associated records,
1069 an empty list is returned
1075 return () unless dbdef->table('acct_snarf');
1076 eval "use FS::acct_snarf;";
1078 qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
1081 =item seconds_since TIMESTAMP
1083 Returns the number of seconds this account has been online since TIMESTAMP,
1084 according to the session monitor (see L<FS::Session>).
1086 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1087 L<Time::Local> and L<Date::Parse> for conversion functions.
1091 #note: POD here, implementation in FS::cust_svc
1094 $self->cust_svc->seconds_since(@_);
1097 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
1099 Returns the numbers of seconds this account has been online between
1100 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
1101 external SQL radacct table, specified via sqlradius export. Sessions which
1102 started in the specified range but are still open are counted from session
1103 start to the end of the range (unless they are over 1 day old, in which case
1104 they are presumed missing their stop record and not counted). Also, sessions
1105 which end in the range but started earlier are counted from the start of the
1106 range to session end. Finally, sessions which start before the range but end
1107 after are counted for the entire range.
1109 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1110 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1115 #note: POD here, implementation in FS::cust_svc
1116 sub seconds_since_sqlradacct {
1118 $self->cust_svc->seconds_since_sqlradacct(@_);
1121 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
1123 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
1124 in this package for sessions ending between TIMESTAMP_START (inclusive) and
1125 TIMESTAMP_END (exclusive).
1127 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1128 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1133 #note: POD here, implementation in FS::cust_svc
1134 sub attribute_since_sqlradacct {
1136 $self->cust_svc->attribute_since_sqlradacct(@_);
1139 =item get_session_history_sqlradacct TIMESTAMP_START TIMESTAMP_END
1141 Returns an array of hash references of this customers login history for the
1142 given time range. (document this better)
1146 sub get_session_history_sqlradacct {
1148 $self->cust_svc->get_session_history_sqlradacct(@_);
1153 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
1159 if ( $self->usergroup ) {
1160 #when provisioning records, export callback runs in svc_Common.pm before
1161 #radius_usergroup records can be inserted...
1162 @{$self->usergroup};
1164 map { $_->groupname }
1165 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
1169 =item clone_suspended
1171 Constructor used by FS::part_export::_export_suspend fallback. Document
1176 sub clone_suspended {
1178 my %hash = $self->hash;
1179 $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
1180 new FS::svc_acct \%hash;
1183 =item clone_kludge_unsuspend
1185 Constructor used by FS::part_export::_export_unsuspend fallback. Document
1190 sub clone_kludge_unsuspend {
1192 my %hash = $self->hash;
1193 $hash{_password} = '';
1194 new FS::svc_acct \%hash;
1197 =item check_password
1199 Checks the supplied password against the (possibly encrypted) password in the
1200 database. Returns true for a sucessful authentication, false for no match.
1202 Currently supported encryptions are: classic DES crypt() and MD5
1206 sub check_password {
1207 my($self, $check_password) = @_;
1209 #remove old-style SUSPENDED kludge, they should be allowed to login to
1210 #self-service and pay up
1211 ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
1213 #eventually should check a "password-encoding" field
1214 if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
1216 } elsif ( length($password) < 13 ) { #plaintext
1217 $check_password eq $password;
1218 } elsif ( length($password) == 13 ) { #traditional DES crypt
1219 crypt($check_password, $password) eq $password;
1220 } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
1221 unix_md5_crypt($check_password, $password) eq $password;
1222 } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
1223 warn "Can't check password: Blowfish encryption not yet supported, svcnum".
1224 $self->svcnum. "\n";
1227 warn "Can't check password: Unrecognized encryption for svcnum ".
1228 $self->svcnum. "\n";
1248 use Mail::Internet 1.44;
1251 $opt{mimetype} ||= 'text/plain';
1252 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
1254 $ENV{MAILADDRESS} = $opt{from};
1255 my $header = new Mail::Header ( [
1258 "Sender: $opt{from}",
1259 "Reply-To: $opt{from}",
1260 "Date: ". time2str("%a, %d %b %Y %X %z", time),
1261 "Subject: $opt{subject}",
1262 "Content-Type: $opt{mimetype}",
1264 my $message = new Mail::Internet (
1265 'Header' => $header,
1266 'Body' => [ map "$_\n", split("\n", $opt{body}) ],
1269 $message->smtpsend( Host => $smtpmachine )
1270 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
1271 or die "can't send email to $opt{to} via $smtpmachine with SMTP: $!";
1274 =item check_and_rebuild_fuzzyfiles
1278 sub check_and_rebuild_fuzzyfiles {
1279 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1280 -e "$dir/svc_acct.username"
1281 or &rebuild_fuzzyfiles;
1284 =item rebuild_fuzzyfiles
1288 sub rebuild_fuzzyfiles {
1290 use Fcntl qw(:flock);
1292 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1296 open(USERNAMELOCK,">>$dir/svc_acct.username")
1297 or die "can't open $dir/svc_acct.username: $!";
1298 flock(USERNAMELOCK,LOCK_EX)
1299 or die "can't lock $dir/svc_acct.username: $!";
1301 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
1303 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
1304 or die "can't open $dir/svc_acct.username.tmp: $!";
1305 print USERNAMECACHE join("\n", @all_username), "\n";
1306 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
1308 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
1318 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1319 open(USERNAMECACHE,"<$dir/svc_acct.username")
1320 or die "can't open $dir/svc_acct.username: $!";
1321 my @array = map { chomp; $_; } <USERNAMECACHE>;
1322 close USERNAMECACHE;
1326 =item append_fuzzyfiles USERNAME
1330 sub append_fuzzyfiles {
1331 my $username = shift;
1333 &check_and_rebuild_fuzzyfiles;
1335 use Fcntl qw(:flock);
1337 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1339 open(USERNAME,">>$dir/svc_acct.username")
1340 or die "can't open $dir/svc_acct.username: $!";
1341 flock(USERNAME,LOCK_EX)
1342 or die "can't lock $dir/svc_acct.username: $!";
1344 print USERNAME "$username\n";
1346 flock(USERNAME,LOCK_UN)
1347 or die "can't unlock $dir/svc_acct.username: $!";
1355 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
1359 sub radius_usergroup_selector {
1360 my $sel_groups = shift;
1361 my %sel_groups = map { $_=>1 } @$sel_groups;
1363 my $selectname = shift || 'radius_usergroup';
1366 my $sth = $dbh->prepare(
1367 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
1368 ) or die $dbh->errstr;
1369 $sth->execute() or die $sth->errstr;
1370 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
1374 function ${selectname}_doadd(object) {
1375 var myvalue = object.${selectname}_add.value;
1376 var optionName = new Option(myvalue,myvalue,false,true);
1377 var length = object.$selectname.length;
1378 object.$selectname.options[length] = optionName;
1379 object.${selectname}_add.value = "";
1382 <SELECT MULTIPLE NAME="$selectname">
1385 foreach my $group ( @all_groups ) {
1387 if ( $sel_groups{$group} ) {
1388 $html .= ' SELECTED';
1389 $sel_groups{$group} = 0;
1391 $html .= ">$group</OPTION>\n";
1393 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
1394 $html .= "<OPTION SELECTED>$group</OPTION>\n";
1396 $html .= '</SELECT>';
1398 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
1399 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
1408 The $recref stuff in sub check should be cleaned up.
1410 The suspend, unsuspend and cancel methods update the database, but not the
1411 current object. This is probably a bug as it's unexpected and
1414 radius_usergroup_selector? putting web ui components in here? they should
1415 probably live somewhere else...
1417 insertion of RADIUS group stuff in insert could be done with child_objects now
1418 (would probably clean up export of them too)
1422 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
1423 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
1424 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
1425 L<freeside-queued>), L<Net::SSH>, L<ssh>, L<FS::svc_acct_pop>,
1426 schema.html from the base documentation.