alas, a 5.6-ism
[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 $passwordmax
6              $username_ampersand $username_letter $username_letterfirst
7              $username_noperiod $username_uppercase
8              $shellmachine $useradd $usermod $userdel $mydomain
9              $cyrus_server $cyrus_admin_user $cyrus_admin_pass
10              $dirhash
11              $icradius_dbh
12              @saltset @pw_set);
13 use Carp;
14 use FS::Conf;
15 use FS::Record qw( qsearch qsearchs fields dbh );
16 use FS::svc_Common;
17 use Net::SSH;
18 use FS::part_svc;
19 use FS::svc_acct_pop;
20 use FS::svc_acct_sm;
21 use FS::cust_main_invoice;
22 use FS::svc_domain;
23 use FS::raddb;
24 use FS::queue;
25
26 @ISA = qw( FS::svc_Common );
27
28 #ask FS::UID to run this stuff for us later
29 $FS::UID::callback{'FS::svc_acct'} = sub { 
30   $conf = new FS::Conf;
31   $dir_prefix = $conf->config('home');
32   @shells = $conf->config('shells');
33   $shellmachine = $conf->config('shellmachine');
34   $usernamemin = $conf->config('usernamemin') || 2;
35   $usernamemax = $conf->config('usernamemax');
36   $passwordmin = $conf->config('passwordmin') || 6;
37   $passwordmax = $conf->config('passwordmax') || 8;
38   if ( $shellmachine ) {
39     if ( $conf->exists('shellmachine-useradd') ) {
40       $useradd = join("\n", $conf->config('shellmachine-useradd') )
41                  || 'cp -pr /etc/skel $dir; chown -R $uid.$gid $dir';
42     } else {
43       $useradd = 'useradd -d $dir -m -s $shell -u $uid $username';
44     }
45     if ( $conf->exists('shellmachine-userdel') ) {
46       $userdel = join("\n", $conf->config('shellmachine-userdel') )
47                  || 'rm -rf $dir';
48     } else {
49       $userdel = 'userdel $username';
50     }
51     $usermod = join("\n", $conf->config('shellmachine-usermod') )
52                || '[ -d $old_dir ] && mv $old_dir $new_dir || ( '.
53                     'chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; '.
54                     'find . -depth -print | cpio -pdm $new_dir; '.
55                     'chmod u-t $new_dir; chown -R $uid.$gid $new_dir; '.
56                     'rm -rf $old_dir'.
57                   ')';
58   }
59   $username_letter = $conf->exists('username-letter');
60   $username_letterfirst = $conf->exists('username-letterfirst');
61   $username_noperiod = $conf->exists('username-noperiod');
62   $username_uppercase = $conf->exists('username-uppercase');
63   $username_ampersand = $conf->exists('username-ampersand');
64   $mydomain = $conf->config('domain');
65   if ( $conf->exists('cyrus') ) {
66     ($cyrus_server, $cyrus_admin_user, $cyrus_admin_pass) =
67       $conf->config('cyrus');
68     eval "use Cyrus::IMAP::Admin;"
69   } else {
70     $cyrus_server = '';
71     $cyrus_admin_user = '';
72     $cyrus_admin_pass = '';
73   }
74   if ( $conf->exists('icradius_secrets') ) {
75     $icradius_dbh = DBI->connect($conf->config('icradius_secrets'))
76       or die $DBI::errstr;
77   } else {
78     $icradius_dbh = '';
79   }
80   $dirhash = $conf->config('dirhash') || 0;
81 };
82
83 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
84 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
85
86 #not needed in 5.004 #srand($$|time);
87
88 sub _cache {
89   my $self = shift;
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'},
96     } );
97   }
98 }
99
100 =head1 NAME
101
102 FS::svc_acct - Object methods for svc_acct records
103
104 =head1 SYNOPSIS
105
106   use FS::svc_acct;
107
108   $record = new FS::svc_acct \%hash;
109   $record = new FS::svc_acct { 'column' => 'value' };
110
111   $error = $record->insert;
112
113   $error = $new_record->replace($old_record);
114
115   $error = $record->delete;
116
117   $error = $record->check;
118
119   $error = $record->suspend;
120
121   $error = $record->unsuspend;
122
123   $error = $record->cancel;
124
125   %hash = $record->radius;
126
127   %hash = $record->radius_reply;
128
129   %hash = $record->radius_check;
130
131 =head1 DESCRIPTION
132
133 An FS::svc_acct object represents an account.  FS::svc_acct inherits from
134 FS::svc_Common.  The following fields are currently supported:
135
136 =over 4
137
138 =item svcnum - primary key (assigned automatcially for new accounts)
139
140 =item username
141
142 =item _password - generated if blank
143
144 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
145
146 =item uid
147
148 =item gid
149
150 =item finger - GECOS
151
152 =item dir - set automatically if blank (and uid is not)
153
154 =item shell
155
156 =item quota - (unimplementd)
157
158 =item slipip - IP address
159
160 =item seconds - 
161
162 =item domsvc - svcnum from svc_domain
163
164 =item radius_I<Radius_Attribute> - I<Radius-Attribute>
165
166 =item domsvc - service number of svc_domain with which to associate
167
168 =back
169
170 =head1 METHODS
171
172 =over 4
173
174 =item new HASHREF
175
176 Creates a new account.  To add the account to the database, see L<"insert">.
177
178 =cut
179
180 sub table { 'svc_acct'; }
181
182 =item insert
183
184 Adds this account to the database.  If there is an error, returns the error,
185 otherwise returns false.
186
187 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be 
188 defined.  An FS::cust_svc record will be created and inserted.
189
190 If the configuration value (see L<FS::Conf>) shellmachine exists, and the 
191 username, uid, and dir fields are defined, the command(s) specified in
192 the shellmachine-useradd configuration are added to the job queue (see
193 L<FS::queue> and L<freeside-queued>) to be exectued on shellmachine via ssh.
194 This behaviour can be surpressed by setting $FS::svc_acct::nossh_hack true.
195 If the shellmachine-useradd configuration file does not exist,
196
197   useradd -d $dir -m -s $shell -u $uid $username
198
199 is the default.  If the shellmachine-useradd configuration file exists but
200 it empty,
201
202   cp -pr /etc/skel $dir; chown -R $uid.$gid $dir
203
204 is the default instead.  Otherwise the contents of the file are treated as
205 a double-quoted perl string, with the following variables available:
206 $username, $uid, $gid, $dir, and $shell.
207
208 (TODOC: cyrus config file, L<FS::queue> and L<freeside-queued>)
209
210 =cut
211
212 sub insert {
213   my $self = shift;
214   my $error;
215
216   local $SIG{HUP} = 'IGNORE';
217   local $SIG{INT} = 'IGNORE';
218   local $SIG{QUIT} = 'IGNORE';
219   local $SIG{TERM} = 'IGNORE';
220   local $SIG{TSTP} = 'IGNORE';
221   local $SIG{PIPE} = 'IGNORE';
222
223   my $oldAutoCommit = $FS::UID::AutoCommit;
224   local $FS::UID::AutoCommit = 0;
225   my $dbh = dbh;
226
227   my $amount = 0;
228
229   $error = $self->check;
230   return $error if $error;
231
232   return "Username ". $self->username. " in use"
233     if qsearchs( 'svc_acct', { 'username' => $self->username,
234                                'domsvc'   => $self->domsvc,
235                              } );
236
237   my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
238   return "Unknown svcpart" unless $part_svc;
239   return "uid in use"
240     if $part_svc->part_svc_column('uid')->columnflag ne 'F'
241       && qsearchs( 'svc_acct', { 'uid' => $self->uid } )
242       && $self->username !~ /^(hyla)?fax$/
243     ;
244
245   $error = $self->SUPER::insert;
246   if ( $error ) {
247     $dbh->rollback if $oldAutoCommit;
248     return $error;
249   }
250
251   my( $username, $uid, $gid, $dir, $shell ) = (
252     $self->username,
253     $self->uid,
254     $self->gid,
255     $self->dir,
256     $self->shell,
257   );
258   if ( $username && $uid && $dir && $shellmachine && ! $nossh_hack ) {
259     my $queue = new FS::queue { 'job' => 'FS::svc_acct::ssh' };
260     $error = $queue->insert("root\@$shellmachine", eval qq("$useradd") );
261     if ( $error ) {
262       $dbh->rollback if $oldAutoCommit;
263       return "queueing job (transaction rolled back): $error";
264     }
265   }
266
267   if ( $cyrus_server ) {
268     my $queue = new FS::queue { 'job' => 'FS::svc_acct::cyrus_insert' };
269     $error = $queue->insert($self->username, $self->quota);
270     if ( $error ) {
271       $dbh->rollback if $oldAutoCommit;
272       return "queueing job (transaction rolled back): $error";
273     }
274   }
275   if ( $icradius_dbh ) {
276     my $queue = new FS::queue { 'job' => 'FS::svc_acct::icradius_rc_insert' };
277     $error = $queue->insert( $self->username,
278                              $self->_password,
279                              $self->radius_check
280                            );
281     if ( $error ) {
282       $dbh->rollback if $oldAutoCommit;
283       return "queueing job (transaction rolled back): $error";
284     }
285   }
286
287   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
288   ''; #no error
289 }
290
291 sub cyrus_insert {
292   my( $username, $quota ) = @_;
293
294   warn "cyrus_insert: starting for user $username, quota $quota\n";
295
296   warn "cyrus_insert: connecting to $cyrus_server\n";
297   my $client = Cyrus::IMAP::Admin->new($cyrus_server);
298
299   warn "cyrus_insert: authentication as $cyrus_admin_user\n";
300   $client->authenticate(
301     -user      => $cyrus_admin_user,
302     -mechanism => "login",       
303     -password  => $cyrus_admin_pass
304   );
305
306   warn "cyrus_insert: creating user.$username\n";
307   my $rc = $client->create("user.$username");
308   my $error = $client->error;
309   die "cyrus_insert: error creating user.$username: $error" if $error;
310
311   warn "cyrus_insert: setacl user.$username, $username => all\n";
312   $rc = $client->setacl("user.$username", $username => 'all' );
313   $error = $client->error;
314   die "cyrus_insert: error setacl user.$username: $error" if $error;
315
316   if ( $quota ) {
317     warn "cyrus_insert: setquota user.$username, STORAGE => $quota\n";
318     $rc = $client->setquota("user.$username", 'STORAGE' => $quota );
319     $error = $client->error;
320     die "cyrus_insert: error setquota user.$username: $error" if $error;
321   }
322
323   1;
324 }
325
326 sub icradius_rc_insert {
327   my( $username, $password, %radcheck ) = @_;
328   
329   my $sth = $icradius_dbh->prepare(
330     "INSERT INTO radcheck ( id, UserName, Attribute, Value ) VALUES ( ".
331     join(", ", map { $icradius_dbh->quote($_) } (
332       '',
333       $username,
334       "Password",
335       $password,
336     ) ). " )"
337   );
338   $sth->execute or die "can't insert into radcheck table: ". $sth->errstr;
339
340   foreach my $attribute ( keys %radcheck ) {
341     my $sth = $icradius_dbh->prepare(
342       "INSERT INTO radcheck ( id, UserName, Attribute, Value ) VALUES ( ".
343       join(", ", map { $icradius_dbh->quote($_) } (
344         '',
345         $username,
346         $attribute,
347         $radcheck{$attribute},
348       ) ). " )"
349     );
350     $sth->execute or die "can't insert into radcheck table: ". $sth->errstr;
351   }
352
353   1;
354 }
355
356 =item delete
357
358 Deletes this account from the database.  If there is an error, returns the
359 error, otherwise returns false.
360
361 The corresponding FS::cust_svc record will be deleted as well.
362
363 If the configuration value (see L<FS::Conf>) shellmachine exists, the
364 command(s) specified in the shellmachine-userdel configuration file are
365 added to the job queue (see L<FS::queue> and L<freeside-queued>) to be executed
366 on shellmachine via ssh.  This behavior can be surpressed by setting
367 $FS::svc_acct::nossh_hack true.  If the shellmachine-userdel configuration
368 file does not exist,
369
370   userdel $username
371
372 is the default.  If the shellmachine-userdel configuration file exists but
373 is empty,
374
375   rm -rf $dir
376
377 is the default instead.  Otherwise the contents of the file are treated as a
378 double-quoted perl string, with the following variables available:
379 $username and $dir.
380
381 (TODOC: cyrus config file)
382
383 =cut
384
385 sub delete {
386   my $self = shift;
387
388   if ( defined( $FS::Record::dbdef->table('svc_acct_sm') ) ) {
389     return "Can't delete an account which has (svc_acct_sm) mail aliases!"
390       if $self->uid && qsearch( 'svc_acct_sm', { 'domuid' => $self->uid } );
391   }
392
393   return "Can't delete an account which is a (svc_forward) source!"
394     if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
395
396   return "Can't delete an account which is a (svc_forward) destination!"
397     if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
398
399   return "Can't delete an account with (svc_www) web service!"
400     if qsearch( 'svc_www', { 'usersvc' => $self->usersvc } );
401
402   # what about records in session ?
403
404   local $SIG{HUP} = 'IGNORE';
405   local $SIG{INT} = 'IGNORE';
406   local $SIG{QUIT} = 'IGNORE';
407   local $SIG{TERM} = 'IGNORE';
408   local $SIG{TSTP} = 'IGNORE';
409   local $SIG{PIPE} = 'IGNORE';
410
411   my $oldAutoCommit = $FS::UID::AutoCommit;
412   local $FS::UID::AutoCommit = 0;
413   my $dbh = dbh;
414
415   foreach my $cust_main_invoice (
416     qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
417   ) {
418     unless ( defined($cust_main_invoice) ) {
419       warn "WARNING: something's wrong with qsearch";
420       next;
421     }
422     my %hash = $cust_main_invoice->hash;
423     $hash{'dest'} = $self->email;
424     my $new = new FS::cust_main_invoice \%hash;
425     my $error = $new->replace($cust_main_invoice);
426     if ( $error ) {
427       $dbh->rollback if $oldAutoCommit;
428       return $error;
429     }
430   }
431
432   foreach my $svc_domain (
433     qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
434   ) {
435     my %hash = new FS::svc_domain->hash;
436     $hash{'catchall'} = '';
437     my $new = new FS::svc_domain \%hash;
438     my $error = $new->replace($svc_domain);
439     if ( $error ) {
440       $dbh->rollback if $oldAutoCommit;
441       return $error;
442     }
443   }
444
445   my $error = $self->SUPER::delete;
446   if ( $error ) {
447     $dbh->rollback if $oldAutoCommit;
448     return $error;
449   }
450
451   my( $username, $dir ) = (
452     $self->username,
453     $self->dir,
454   );
455   if ( $username && $shellmachine && ! $nossh_hack ) {
456     my $queue = new FS::queue { 'job' => 'FS::svc_acct::ssh' };
457     $error = $queue->insert("root\@$shellmachine", eval qq("$userdel") );
458     if ( $error ) {
459       $dbh->rollback if $oldAutoCommit;
460       return "queueing job (transaction rolled back): $error";
461     }
462
463   }
464
465   if ( $cyrus_server ) {
466     my $queue = new FS::queue { 'job' => 'FS::svc_acct::cyrus_delete' };
467     $error = $queue->insert($self->username);
468     if ( $error ) {
469       $dbh->rollback if $oldAutoCommit;
470       return "queueing job (transaction rolled back): $error";
471     }
472   }
473   if ( $icradius_dbh ) {
474     my $queue = new FS::queue { 'job' => 'FS::svc_acct::icradius_rc_delete' };
475     $error = $queue->insert( $self->username );
476     if ( $error ) {
477       $dbh->rollback if $oldAutoCommit;
478       return "queueing job (transaction rolled back): $error";
479     }
480   }
481
482   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
483   '';
484 }
485
486 sub cyrus_delete {
487   my $username = shift; 
488
489   my $client = Cyrus::IMAP::Admin->new($cyrus_server);
490   $client->authenticate(
491     -user      => $cyrus_admin_user,
492     -mechanism => "login",       
493     -password  => $cyrus_admin_pass
494   );
495
496   my $rc = $client->setacl("user.$username", $cyrus_admin_user => 'all' );
497   my $error = $client->error;
498   die $error if $error;
499
500   $rc = $client->delete("user.$username");
501   $error = $client->error;
502   die $error if $error;
503
504   1;
505 }
506
507 sub icradius_rc_delete {
508   my $username = shift;
509   
510   my $sth = $icradius_dbh->prepare(
511     'DELETE FROM radcheck WHERE UserName = ?'
512   );
513   $sth->execute($username)
514     or die "can't delete from radcheck table: ". $sth->errstr;
515
516   1;
517 }
518
519 =item replace OLD_RECORD
520
521 Replaces OLD_RECORD with this one in the database.  If there is an error,
522 returns the error, otherwise returns false.
523
524 If the configuration value (see L<FS::Conf>) shellmachine exists, and the 
525 dir field has changed, the command(s) specified in the shellmachine-usermod
526 configuraiton file are added to the job queue (see L<FS::queue> and
527 L<freeside-queued>) to be executed on shellmachine via ssh.  This behavior can
528 be surpressed by setting $FS::svc-acct::nossh_hack true.  If the
529 shellmachine-userdel configuration file does not exist or is empty,
530
531   [ -d $old_dir ] && mv $old_dir $new_dir || (
532     chmod u+t $old_dir;
533     mkdir $new_dir;
534     cd $old_dir;
535     find . -depth -print | cpio -pdm $new_dir;
536     chmod u-t $new_dir;
537     chown -R $uid.$gid $new_dir;
538     rm -rf $old_dir
539   )
540
541 is the default.  This behaviour can be surpressed by setting
542 $FS::svc_acct::nossh_hack true.
543
544 =cut
545
546 sub replace {
547   my ( $new, $old ) = ( shift, shift );
548   my $error;
549
550   return "Username in use"
551     if $old->username ne $new->username &&
552       qsearchs( 'svc_acct', { 'username' => $new->username,
553                                'domsvc'   => $new->domsvc,
554                              } );
555 #  {
556 #    no warnings 'numeric';  #alas, a 5.006-ism
557     return "Can't change uid!" if $old->uid != $new->uid;
558 #  }
559
560   return "can't change username using Cyrus"
561     if $cyrus_server && $old->username ne $new->username;
562
563   #change homdir when we change username
564   $new->setfield('dir', '') if $old->username ne $new->username;
565
566   local $SIG{HUP} = 'IGNORE';
567   local $SIG{INT} = 'IGNORE';
568   local $SIG{QUIT} = 'IGNORE';
569   local $SIG{TERM} = 'IGNORE';
570   local $SIG{TSTP} = 'IGNORE';
571   local $SIG{PIPE} = 'IGNORE';
572
573   my $oldAutoCommit = $FS::UID::AutoCommit;
574   local $FS::UID::AutoCommit = 0;
575   my $dbh = dbh;
576
577   $error = $new->SUPER::replace($old);
578   if ( $error ) {
579     $dbh->rollback if $oldAutoCommit;
580     return $error if $error;
581   }
582
583   my ( $old_dir, $new_dir, $uid, $gid ) = (
584     $old->getfield('dir'),
585     $new->getfield('dir'),
586     $new->getfield('uid'),
587     $new->getfield('gid'),
588   );
589   if ( $old_dir && $new_dir && $old_dir ne $new_dir && ! $nossh_hack ) {
590     my $queue = new FS::queue { 'job' => 'FS::svc_acct::ssh' };
591     $error = $queue->insert("root\@$shellmachine", eval qq("$usermod") );
592     if ( $error ) {
593       $dbh->rollback if $oldAutoCommit;
594       return "queueing job (transaction rolled back): $error";
595     }
596   }
597
598   if ( $icradius_dbh ) {
599     my $queue = new FS::queue { 'job' => 'FS::svc_acct::icradius_rc_replace' };
600     $error = $queue->insert( $new->username,
601                              $new->_password,
602                            );
603     if ( $error ) {
604       $dbh->rollback if $oldAutoCommit;
605       return "queueing job (transaction rolled back): $error";
606     }
607   }
608
609   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
610   ''; #no error
611 }
612
613 sub icradius_rc_replace {
614   my( $username, $new_password ) = @_;
615  
616    my $sth = $icradius_dbh->prepare(
617      "UPDATE radcheck SET Value = ? WHERE UserName = ? and Attribute = ?"
618    );
619    $sth->execute($new_password, $username, 'Password' )
620      or die "can't update radcheck table: ". $sth->errstr;
621
622   1;
623 }
624
625 =item suspend
626
627 Suspends this account by prefixing *SUSPENDED* to the password.  If there is an
628 error, returns the error, otherwise returns false.
629
630 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
631
632 =cut
633
634 sub suspend {
635   my $self = shift;
636   my %hash = $self->hash;
637   unless ( $hash{_password} =~ /^\*SUSPENDED\* / ) {
638     $hash{_password} = '*SUSPENDED* '.$hash{_password};
639     my $new = new FS::svc_acct ( \%hash );
640     $new->replace($self);
641   } else {
642     ''; #no error (already suspended)
643   }
644 }
645
646 =item unsuspend
647
648 Unsuspends this account by removing *SUSPENDED* from the password.  If there is
649 an error, returns the error, otherwise returns false.
650
651 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
652
653 =cut
654
655 sub unsuspend {
656   my $self = shift;
657   my %hash = $self->hash;
658   if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
659     $hash{_password} = $1;
660     my $new = new FS::svc_acct ( \%hash );
661     $new->replace($self);
662   } else {
663     ''; #no error (already unsuspended)
664   }
665 }
666
667 =item cancel
668
669 Just returns false (no error) for now.
670
671 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
672
673 =item check
674
675 Checks all fields to make sure this is a valid service.  If there is an error,
676 returns the error, otherwise returns false.  Called by the insert and replace
677 methods.
678
679 Sets any fixed values; see L<FS::part_svc>.
680
681 =cut
682
683 sub check {
684   my $self = shift;
685
686   my($recref) = $self->hashref;
687
688   my $x = $self->setfixed;
689   return $x unless ref($x);
690   my $part_svc = $x;
691
692   my $error = $self->ut_numbern('svcnum')
693               || $self->ut_number('domsvc')
694   ;
695   return $error if $error;
696
697   my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
698   if ( $username_uppercase ) {
699     $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/i
700       or return "Illegal username: ". $recref->{username};
701     $recref->{username} = $1;
702   } else {
703     $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/
704       or return "Illegal username: ". $recref->{username};
705     $recref->{username} = $1;
706   }
707
708   if ( $username_letterfirst ) {
709     $recref->{username} =~ /^[a-z]/ or return "Illegal username";
710   } elsif ( $username_letter ) {
711     $recref->{username} =~ /[a-z]/ or return "Illegal username";
712   }
713   if ( $username_noperiod ) {
714     $recref->{username} =~ /\./ and return "Illegal username";
715   }
716   unless ( $username_ampersand ) {
717     $recref->{username} =~ /\&/ and return "Illegal username";
718   }
719
720   $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
721   $recref->{popnum} = $1;
722   return "Unknown popnum" unless
723     ! $recref->{popnum} ||
724     qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
725
726   unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
727
728     $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
729     $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
730
731     $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
732     $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
733     #not all systems use gid=uid
734     #you can set a fixed gid in part_svc
735
736     return "Only root can have uid 0"
737       if $recref->{uid} == 0 && $recref->{username} ne 'root';
738
739 #    $error = $self->ut_textn('finger');
740 #    return $error if $error;
741     $self->getfield('finger') =~
742       /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\*\<\>]*)$/
743         or return "Illegal finger: ". $self->getfield('finger');
744     $self->setfield('finger', $1);
745
746     $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
747       or return "Illegal directory";
748     $recref->{dir} = $1;
749     return "Illegal directory"
750       if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
751     return "Illegal directory"
752       if $recref->{dir} =~ /\&/ && ! $username_ampersand;
753     unless ( $recref->{dir} ) {
754       $recref->{dir} = $dir_prefix . '/';
755       if ( $dirhash > 0 ) {
756         for my $h ( 1 .. $dirhash ) {
757           $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
758         }
759       } elsif ( $dirhash < 0 ) {
760         for my $h ( reverse $dirhash .. -1 ) {
761           $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
762         }
763       }
764       $recref->{dir} .= $recref->{username};
765     ;
766     }
767
768     unless ( $recref->{username} eq 'sync' ) {
769       if ( grep $_ eq $recref->{shell}, @shells ) {
770         $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
771       } else {
772         return "Illegal shell \`". $self->shell. "\'; ".
773                $conf->dir. "/shells contains: @shells";
774       }
775     } else {
776       $recref->{shell} = '/bin/sync';
777     }
778
779     $recref->{quota} =~ /^(\d*)$/ or return "Illegal quota (unimplemented)";
780     $recref->{quota} = $1;
781
782   } else {
783     $recref->{gid} ne '' ? 
784       return "Can't have gid without uid" : ( $recref->{gid}='' );
785     $recref->{finger} ne '' ? 
786       return "Can't have finger-name without uid" : ( $recref->{finger}='' );
787     $recref->{dir} ne '' ? 
788       return "Can't have directory without uid" : ( $recref->{dir}='' );
789     $recref->{shell} ne '' ? 
790       return "Can't have shell without uid" : ( $recref->{shell}='' );
791     $recref->{quota} ne '' ? 
792       return "Can't have quota without uid" : ( $recref->{quota}='' );
793   }
794
795   unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
796     unless ( $recref->{slipip} eq '0e0' ) {
797       $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
798         or return "Illegal slipip". $self->slipip;
799       $recref->{slipip} = $1;
800     } else {
801       $recref->{slipip} = '0e0';
802     }
803
804   }
805
806   #arbitrary RADIUS stuff; allow ut_textn for now
807   foreach ( grep /^radius_/, fields('svc_acct') ) {
808     $self->ut_textn($_);
809   }
810
811   #generate a password if it is blank
812   $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
813     unless ( $recref->{_password} );
814
815   #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
816   if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
817     $recref->{_password} = $1.$3;
818     #uncomment this to encrypt password immediately upon entry, or run
819     #bin/crypt_pw in cron to give new users a window during which their
820     #password is available to techs, for faxing, etc.  (also be aware of 
821     #radius issues!)
822     #$recref->{password} = $1.
823     #  crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
824     #;
825   } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$]{13,34})$/ ) {
826     $recref->{_password} = $1.$3;
827   } elsif ( $recref->{_password} eq '*' ) {
828     $recref->{_password} = '*';
829   } elsif ( $recref->{_password} eq '!!' ) {
830     $recref->{_password} = '!!';
831   } else {
832     #return "Illegal password";
833     return "Illegal password: ". $recref->{_password};
834   }
835
836   ''; #no error
837 }
838
839 =item radius
840
841 Depriciated, use radius_reply instead.
842
843 =cut
844
845 sub radius {
846   carp "FS::svc_acct::radius depriciated, use radius_reply";
847   $_[0]->radius_reply;
848 }
849
850 =item radius_reply
851
852 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
853 reply attributes of this record.
854
855 Note that this is now the preferred method for reading RADIUS attributes - 
856 accessing the columns directly is discouraged, as the column names are
857 expected to change in the future.
858
859 =cut
860
861 sub radius_reply { 
862   my $self = shift;
863   map {
864     /^(radius_(.*))$/;
865     my($column, $attrib) = ($1, $2);
866     #$attrib =~ s/_/\-/g;
867     ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
868   } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
869 }
870
871 =item radius_check
872
873 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
874 check attributes of this record.
875
876 Accessing RADIUS attributes directly is not supported and will break in the
877 future.
878
879 =cut
880
881 sub radius_check {
882   my $self = shift;
883   map {
884     /^(rc_(.*))$/;
885     my($column, $attrib) = ($1, $2);
886     #$attrib =~ s/_/\-/g;
887     ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
888   } grep { /^rc_/ && $self->getfield($_) } fields( $self->table );
889 }
890
891 =item domain
892
893 Returns the domain associated with this account.
894
895 =cut
896
897 sub domain {
898   my $self = shift;
899   if ( $self->domsvc ) {
900     #$self->svc_domain->domain;
901     my $svc_domain = $self->svc_domain
902       or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
903     $svc_domain->domain;
904   } else {
905     $mydomain or die "svc_acct.domsvc is null and no legacy domain config file";
906   }
907 }
908
909 =item svc_domain
910
911 Returns the FS::svc_domain record for this account's domain (see
912 L<FS::svc_domain>.
913
914 =cut
915
916 sub svc_domain {
917   my $self = shift;
918   $self->{'_domsvc'}
919     ? $self->{'_domsvc'}
920     : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
921 }
922
923 =item email
924
925 Returns an email address associated with the account.
926
927 =cut
928
929 sub email {
930   my $self = shift;
931   $self->username. '@'. $self->domain;
932 }
933
934 =item ssh
935
936 =cut
937
938 sub ssh {
939   my ( $host, @cmd_and_args ) = @_;
940
941   use IO::File;
942   my $reader = IO::File->new();
943   my $writer = IO::File->new();
944   my $error = IO::File->new();
945
946   &Net::SSH::sshopen3( $host, $reader, $writer, $error, @cmd_and_args) or die $!;
947
948   local $/ = undef;
949   my $output_stream = <$writer>;
950   my $error_stream = <$error>;
951   if ( length $error_stream ) {
952     #warn "[FS::svc_acct::ssh] STDERR $error_stream";
953     die "[FS::svc_acct::ssh] STDERR $error_stream";
954   }
955   if ( length $output_stream ) {
956     warn "[FS::svc_acct::ssh] STDOUT $output_stream";
957   }
958
959 #  &Net::SSH::ssh(@args,">>/usr/local/etc/freeside/sshoutput 2>&1");
960 }
961
962 =back
963
964 =head1 VERSION
965
966 $Id: svc_acct.pm,v 1.58 2001-12-19 14:33:48 ivan Exp $
967
968 =head1 BUGS
969
970 The bits which ssh should fork before doing so (or maybe queue jobs for a
971 daemon).
972
973 The $recref stuff in sub check should be cleaned up.
974
975 The suspend, unsuspend and cancel methods update the database, but not the
976 current object.  This is probably a bug as it's unexpected and
977 counterintuitive.
978
979 =head1 SEE ALSO
980
981 L<FS::svc_Common>, L<FS::Record>, L<FS::Conf>, L<FS::cust_svc>,
982 L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>, L<freeside-queued>),
983 L<Net::SSH>, L<ssh>, L<FS::svc_acct_pop>,
984 schema.html from the base documentation.
985
986 =cut
987
988 1;
989