dtrt when deleting accouts wrt forwards, catchalls & other references to
[freeside.git] / FS / FS / svc_acct.pm
1 package FS::svc_acct;
2
3 use strict;
4 use vars qw( @ISA $nossh_hack $conf $dir_prefix @shells $usernamemin
5              $usernamemax $passwordmin $username_letter $username_letterfirst
6              $shellmachine $useradd $usermod $userdel 
7              @saltset @pw_set);
8 use Carp;
9 use FS::Conf;
10 use FS::Record qw( qsearch qsearchs fields );
11 use FS::svc_Common;
12 use Net::SSH qw(ssh);
13 use FS::part_svc;
14 use FS::svc_acct_pop;
15 use FS::svc_acct_sm;
16 use FS::cust_main_invoice;
17 use FS::svc_domain;
18
19 @ISA = qw( FS::svc_Common );
20
21 #ask FS::UID to run this stuff for us later
22 $FS::UID::callback{'FS::svc_acct'} = sub { 
23   $conf = new FS::Conf;
24   $dir_prefix = $conf->config('home');
25   @shells = $conf->config('shells');
26   $shellmachine = $conf->config('shellmachine');
27   $usernamemin = $conf->config('usernamemin') || 2;
28   $usernamemax = $conf->config('usernamemax');
29   $passwordmin = $conf->config('passwordmin') || 6;
30   if ( $shellmachine ) {
31     if ( $conf->exists('shellmachine-useradd') ) {
32       $useradd = join("\n", $conf->config('shellmachine-useradd') )
33                  || 'cp -pr /etc/skel $dir; chown -R $uid.$gid $dir';
34     } else {
35       $useradd = 'useradd -d $dir -m -s $shell -u $uid $username';
36     }
37     if ( $conf->exists('shellmachine-userdel') ) {
38       $userdel = join("\n", $conf->config('shellmachine-userdel') )
39                  || 'rm -rf $dir';
40     } else {
41       $userdel = 'userdel $username';
42     }
43     $usermod = join("\n", $conf->config('shellmachine-usermod') )
44                || '[ -d $old_dir ] && mv $old_dir $new_dir || ( '.
45                     'chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; '.
46                     'find . -depth -print | cpio -pdm $new_dir; '.
47                     'chmod u-t $new_dir; chown -R $uid.$gid $new_dir; '.
48                     'rm -rf $old_dir'.
49                   ')';
50   }
51   $username_letter = $conf->exists('username-letter');
52   $username_letterfirst = $conf->exists('username-letterfirst');
53 };
54
55 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
56 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
57
58 #not needed in 5.004 #srand($$|time);
59
60 =head1 NAME
61
62 FS::svc_acct - Object methods for svc_acct records
63
64 =head1 SYNOPSIS
65
66   use FS::svc_acct;
67
68   $record = new FS::svc_acct \%hash;
69   $record = new FS::svc_acct { 'column' => 'value' };
70
71   $error = $record->insert;
72
73   $error = $new_record->replace($old_record);
74
75   $error = $record->delete;
76
77   $error = $record->check;
78
79   $error = $record->suspend;
80
81   $error = $record->unsuspend;
82
83   $error = $record->cancel;
84
85   %hash = $record->radius;
86
87   %hash = $record->radius_reply;
88
89   %hash = $record->radius_check;
90
91 =head1 DESCRIPTION
92
93 An FS::svc_acct object represents an account.  FS::svc_acct inherits from
94 FS::svc_Common.  The following fields are currently supported:
95
96 =over 4
97
98 =item svcnum - primary key (assigned automatcially for new accounts)
99
100 =item username
101
102 =item _password - generated if blank
103
104 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
105
106 =item uid
107
108 =item gid
109
110 =item finger - GECOS
111
112 =item dir - set automatically if blank (and uid is not)
113
114 =item shell
115
116 =item quota - (unimplementd)
117
118 =item slipip - IP address
119
120 =item radius_I<Radius_Attribute> - I<Radius-Attribute>
121
122 =item domsvc - service number of svc_domain with which to associate
123
124 =back
125
126 =head1 METHODS
127
128 =over 4
129
130 =item new HASHREF
131
132 Creates a new account.  To add the account to the database, see L<"insert">.
133
134 =cut
135
136 sub table { 'svc_acct'; }
137
138 =item insert
139
140 Adds this account to the database.  If there is an error, returns the error,
141 otherwise returns false.
142
143 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be 
144 defined.  An FS::cust_svc record will be created and inserted.
145
146 If the configuration value (see L<FS::Conf>) shellmachine exists, and the 
147 username, uid, and dir fields are defined, the command(s) specified in
148 the shellmachine-useradd configuration are exectued on shellmachine via ssh.
149 This behaviour can be surpressed by setting $FS::svc_acct::nossh_hack true.
150 If the shellmachine-useradd configuration file does not exist,
151
152   useradd -d $dir -m -s $shell -u $uid $username
153
154 is the default.  If the shellmachine-useradd configuration file exists but
155 it empty,
156
157   cp -pr /etc/skel $dir; chown -R $uid.$gid $dir
158
159 is the default instead.  Otherwise the contents of the file are treated as
160 a double-quoted perl string, with the following variables available:
161 $username, $uid, $gid, $dir, and $shell.
162
163 =cut
164
165 sub insert {
166   my $self = shift;
167   my $error;
168
169   local $SIG{HUP} = 'IGNORE';
170   local $SIG{INT} = 'IGNORE';
171   local $SIG{QUIT} = 'IGNORE';
172   local $SIG{TERM} = 'IGNORE';
173   local $SIG{TSTP} = 'IGNORE';
174   local $SIG{PIPE} = 'IGNORE';
175
176   $error = $self->check;
177   return $error if $error;
178
179   return "Username ". $self->username. " in use"
180     if qsearchs( 'svc_acct', { 'username' => $self->username,
181                                'domsvc'   => $self->domsvc,
182                              } );
183
184   my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
185   return "Unknown svcpart" unless $part_svc;
186   return "uid in use"
187     if $part_svc->svc_acct__uid_flag ne 'F'
188       && qsearchs( 'svc_acct', { 'uid' => $self->uid } )
189       && $self->username !~ /^(hyla)?fax$/
190     ;
191
192   $error = $self->SUPER::insert;
193   return $error if $error;
194
195   my( $username, $uid, $gid, $dir, $shell ) = (
196     $self->username,
197     $self->uid,
198     $self->gid,
199     $self->dir,
200     $self->shell,
201   );
202   if ( $username && $uid && $dir && $shellmachine && ! $nossh_hack ) {
203     ssh("root\@$shellmachine", eval qq("$useradd") );
204   }
205
206   ''; #no error
207 }
208
209 =item delete
210
211 Deletes this account from the database.  If there is an error, returns the
212 error, otherwise returns false.
213
214 The corresponding FS::cust_svc record will be deleted as well.
215
216 If the configuration value (see L<FS::Conf>) shellmachine exists, the
217 command(s) specified in the shellmachine-userdel configuration file are
218 executed on shellmachine via ssh.  This behavior can be surpressed by setting
219 $FS::svc_acct::nossh_hack true.  If the shellmachine-userdel configuration
220 file does not exist,
221
222   userdel $username
223
224 is the default.  If the shellmachine-userdel configuration file exists but
225 is empty,
226
227   rm -rf $dir
228
229 is the default instead.  Otherwise the contents of the file are treated as a
230 double-quoted perl string, with the following variables available:
231 $username and $dir.
232
233 =cut
234
235 sub delete {
236   my $self = shift;
237
238   return "Can't delete an account which has (svc_acct_sm) mail aliases!"
239     if $self->uid && qsearch( 'svc_acct_sm', { 'domuid' => $self->uid } );
240
241   return "Can't delete an account which is a (svc_forward) source!"
242     if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
243
244   return "Can't delete an account which is a (svc_forward) destination!"
245     if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
246
247   return "Can't delete an account with (svc_www) web service!"
248     if qsearch( 'svc_www', { 'usersvc' => $self->usersvc } );
249
250   # what about records in session ?
251
252   local $SIG{HUP} = 'IGNORE';
253   local $SIG{INT} = 'IGNORE';
254   local $SIG{QUIT} = 'IGNORE';
255   local $SIG{TERM} = 'IGNORE';
256   local $SIG{TSTP} = 'IGNORE';
257   local $SIG{PIPE} = 'IGNORE';
258
259   my $oldAutoCommit = $FS::UID::AutoCommit;
260   local $FS::UID::AutoCommit = 0;
261   my $dbh = dbh;
262
263   foreach my $cust_main_invoice (
264     qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
265   ) {
266     my %hash = $cust_main_invoice->hash;
267     $hash{'dest'} = $self->email;
268     my $new = new FS::cust_main_invoice \%hash;
269     my $error = $new->replace($cust_main_invoice);
270     if ( $error ) {
271       $dbh->rollback if $oldAutoCommit;
272       return $error;
273     }
274   }
275
276   foreach my $svc_domain (
277     qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
278   ) {
279     my %hash = new FS::svc_domain->hash;
280     $hash{'catchall'} = '';
281     my $new = new FS::svc_domain \%hash;
282     my $error = $new->replace($svc_domain);
283     if ( $error ) {
284       $dbh->rollback if $oldAutoCommit;
285       return $error;
286     }
287   }
288
289   my $error = $self->SUPER::delete;
290   if ( $error ) {
291     $dbh->rollback if $oldAutoCommit;
292     return $error;
293   }
294
295   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
296
297   my( $username, $dir ) = (
298     $self->username,
299     $self->dir,
300   );
301   if ( $username && $shellmachine && ! $nossh_hack ) {
302     ssh("root\@$shellmachine", eval qq("$userdel") );
303   }
304
305   '';
306 }
307
308 =item replace OLD_RECORD
309
310 Replaces OLD_RECORD with this one in the database.  If there is an error,
311 returns the error, otherwise returns false.
312
313 If the configuration value (see L<FS::Conf>) shellmachine exists, and the 
314 dir field has changed, the command(s) specified in the shellmachine-usermod
315 configuraiton file are executed on shellmachine via ssh.  This behavior can
316 be surpressed by setting $FS::svc-acct::nossh_hack true.  If the
317 shellmachine-userdel configuration file does not exist or is empty, :
318
319   [ -d $old_dir ] && mv $old_dir $new_dir || (
320     chmod u+t $old_dir;
321     mkdir $new_dir;
322     cd $old_dir;
323     find . -depth -print | cpio -pdm $new_dir;
324     chmod u-t $new_dir;
325     chown -R $uid.$gid $new_dir;
326     rm -rf $old_dir
327   )
328
329 is executed on shellmachine via ssh.  This behaviour can be surpressed by
330 setting $FS::svc_acct::nossh_hack true.
331
332 =cut
333
334 sub replace {
335   my ( $new, $old ) = ( shift, shift );
336   my $error;
337
338   return "Username in use"
339     if $old->username ne $new->username &&
340       qsearchs( 'svc_acct', { 'username' => $new->username } );
341
342   return "Can't change uid!" if $old->uid != $new->uid;
343
344   #change homdir when we change username
345   $new->setfield('dir', '') if $old->username ne $new->username;
346
347   local $SIG{HUP} = 'IGNORE';
348   local $SIG{INT} = 'IGNORE';
349   local $SIG{QUIT} = 'IGNORE';
350   local $SIG{TERM} = 'IGNORE';
351   local $SIG{TSTP} = 'IGNORE';
352   local $SIG{PIPE} = 'IGNORE';
353
354   $error = $new->SUPER::replace($old);
355   return $error if $error;
356
357   my ( $old_dir, $new_dir, $uid, $gid ) = (
358     $old->getfield('dir'),
359     $new->getfield('dir'),
360     $new->getfield('uid'),
361     $new->getfield('gid'),
362   );
363   if ( $old_dir && $new_dir && $old_dir ne $new_dir && ! $nossh_hack ) {
364     ssh("root\@$shellmachine", eval qq("$usermod") );
365   }
366
367   ''; #no error
368 }
369
370 =item suspend
371
372 Suspends this account by prefixing *SUSPENDED* to the password.  If there is an
373 error, returns the error, otherwise returns false.
374
375 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
376
377 =cut
378
379 sub suspend {
380   my $self = shift;
381   my %hash = $self->hash;
382   unless ( $hash{_password} =~ /^\*SUSPENDED\* / ) {
383     $hash{_password} = '*SUSPENDED* '.$hash{_password};
384     my $new = new FS::svc_acct ( \%hash );
385     $new->replace($self);
386   } else {
387     ''; #no error (already suspended)
388   }
389 }
390
391 =item unsuspend
392
393 Unsuspends this account by removing *SUSPENDED* from the password.  If there is
394 an error, returns the error, otherwise returns false.
395
396 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
397
398 =cut
399
400 sub unsuspend {
401   my $self = shift;
402   my %hash = $self->hash;
403   if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
404     $hash{_password} = $1;
405     my $new = new FS::svc_acct ( \%hash );
406     $new->replace($self);
407   } else {
408     ''; #no error (already unsuspended)
409   }
410 }
411
412 =item cancel
413
414 Just returns false (no error) for now.
415
416 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
417
418 =item check
419
420 Checks all fields to make sure this is a valid service.  If there is an error,
421 returns the error, otherwise returns false.  Called by the insert and replace
422 methods.
423
424 Sets any fixed values; see L<FS::part_svc>.
425
426 =cut
427
428 sub check {
429   my $self = shift;
430
431   my($recref) = $self->hashref;
432
433   my $x = $self->setfixed;
434   return $x unless ref($x);
435   my $part_svc = $x;
436
437   my $error = $self->ut_number('domsvc');
438   return $error if $error;
439
440   my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
441   $recref->{username} =~ /^([a-z0-9_\-\.]{$usernamemin,$ulen})$/
442     or return "Illegal username";
443   $recref->{username} = $1;
444   if ( $username_letterfirst ) {
445     $recref->{username} =~ /^[a-z]/ or return "Illegal username";
446   } elsif ( $username_letter ) {
447     $recref->{username} =~ /[a-z]/ or return "Illegal username";
448   }
449
450   $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
451   $recref->{popnum} = $1;
452   return "Unknown popnum" unless
453     ! $recref->{popnum} ||
454     qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
455
456   unless ( $part_svc->getfield('svc_acct__uid_flag') eq 'F' ) {
457
458     $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
459     $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
460
461     $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
462     $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
463     #not all systems use gid=uid
464     #you can set a fixed gid in part_svc
465
466     return "Only root can have uid 0"
467       if $recref->{uid} == 0 && $recref->{username} ne 'root';
468
469     $error = $self->ut_textn('finger');
470     return $error if $error;
471
472     $recref->{dir} =~ /^([\/\w\-]*)$/
473       or return "Illegal directory";
474     $recref->{dir} = $1 || 
475       $dir_prefix . '/' . $recref->{username}
476       #$dir_prefix . '/' . substr($recref->{username},0,1). '/' . $recref->{username}
477     ;
478
479     unless ( $recref->{username} eq 'sync' ) {
480       if ( grep $_ eq $recref->{shell}, @shells ) {
481         $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
482       } else {
483         return "Illegal shell \`". $self->shell. "\'; ".
484                $conf->dir. "/shells contains: @shells";
485       }
486     } else {
487       $recref->{shell} = '/bin/sync';
488     }
489
490     $recref->{quota} =~ /^(\d*)$/ or return "Illegal quota (unimplemented)";
491     $recref->{quota} = $1;
492
493   } else {
494     $recref->{gid} ne '' ? 
495       return "Can't have gid without uid" : ( $recref->{gid}='' );
496     $recref->{finger} ne '' ? 
497       return "Can't have finger-name without uid" : ( $recref->{finger}='' );
498     $recref->{dir} ne '' ? 
499       return "Can't have directory without uid" : ( $recref->{dir}='' );
500     $recref->{shell} ne '' ? 
501       return "Can't have shell without uid" : ( $recref->{shell}='' );
502     $recref->{quota} ne '' ? 
503       return "Can't have quota without uid" : ( $recref->{quota}='' );
504   }
505
506   unless ( $part_svc->getfield('svc_acct__slipip_flag') eq 'F' ) {
507     unless ( $recref->{slipip} eq '0e0' ) {
508       $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
509         or return "Illegal slipip". $self->slipip;
510       $recref->{slipip} = $1;
511     } else {
512       $recref->{slipip} = '0e0';
513     }
514
515   }
516
517   #arbitrary RADIUS stuff; allow ut_textn for now
518   foreach ( grep /^radius_/, fields('svc_acct') ) {
519     $self->ut_textn($_);
520   }
521
522   #generate a password if it is blank
523   $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
524     unless ( $recref->{_password} );
525
526   #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
527   if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{$passwordmin,8})$/ ) {
528     $recref->{_password} = $1.$3;
529     #uncomment this to encrypt password immediately upon entry, or run
530     #bin/crypt_pw in cron to give new users a window during which their
531     #password is available to techs, for faxing, etc.  (also be aware of 
532     #radius issues!)
533     #$recref->{password} = $1.
534     #  crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
535     #;
536   } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$]{13,34})$/ ) {
537     $recref->{_password} = $1.$3;
538   } elsif ( $recref->{_password} eq '*' ) {
539     $recref->{_password} = '*';
540   } elsif ( $recref->{_password} eq '!!' ) {
541     $recref->{_password} = '!!';
542   } else {
543     return "Illegal password";
544   }
545
546   ''; #no error
547 }
548
549 =item radius
550
551 Depriciated, use radius_reply instead.
552
553 =cut
554
555 sub radius {
556   carp "FS::svc_acct::radius depriciated, use radius_reply";
557   $_[0]->radius_reply;
558 }
559
560 =item radius_reply
561
562 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
563 reply attributes of this record.
564
565 Note that this is now the preferred method for reading RADIUS attributes - 
566 accessing the columns directly is discouraged, as the column names are
567 expected to change in the future.
568
569 =cut
570
571 sub radius_reply { 
572   my $self = shift;
573   map {
574     /^(radius_(.*))$/;
575     my($column, $attrib) = ($1, $2);
576     $attrib =~ s/_/\-/g;
577     ( $attrib, $self->getfield($column) );
578   } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
579 }
580
581 =item radius_check
582
583 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
584 check attributes of this record.
585
586 Accessing RADIUS attributes directly is not supported and will break in the
587 future.
588
589 =cut
590
591 sub radius_check {
592   my $self = shift;
593   map {
594     /^(rc_(.*))$/;
595     my($column, $attrib) = ($1, $2);
596     $attrib =~ s/_/\-/g;
597     ( $attrib, $self->getfield($column) );
598   } grep { /^rc_/ && $self->getfield($_) } fields( $self->table );
599 }
600
601 =item email
602
603 Returns an email address associated with the account.
604
605 =cut
606
607 sub email {
608   my $self = shift;
609   my $domain;
610   my $svc_domain = qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
611   if ($svc_domain) {
612     $domain=$svc_domain->domain;
613   }else{
614     warn "couldn't find svc_acct.domsvc " . $self->domsvc . "!";
615     $domain="unknown";
616   }
617   return $self->username . "@" . $domain;
618 }
619
620 =back
621
622 =head1 VERSION
623
624 $Id: svc_acct.pm,v 1.25 2001-08-20 09:41:52 ivan Exp $
625
626 =head1 BUGS
627
628 The bits which ssh should fork before doing so (or maybe queue jobs for a
629 daemon).
630
631 The $recref stuff in sub check should be cleaned up.
632
633 The suspend, unsuspend and cancel methods update the database, but not the
634 current object.  This is probably a bug as it's unexpected and
635 counterintuitive.
636
637 =head1 SEE ALSO
638
639 L<FS::svc_Common>, L<FS::Record>, L<FS::Conf>, L<FS::cust_svc>,
640 L<FS::part_svc>, L<FS::cust_pkg>, L<Net::SSH>, L<ssh>, L<FS::svc_acct_pop>,
641 schema.html from the base documentation.
642
643 =cut
644
645 1;
646