quiet warnings
[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     local($^W) = 0;
558     return "Can't change uid!" if $old->uid != $new->uid;
559   }
560
561   return "can't change username using Cyrus"
562     if $cyrus_server && $old->username ne $new->username;
563
564   #change homdir when we change username
565   $new->setfield('dir', '') if $old->username ne $new->username;
566
567   local $SIG{HUP} = 'IGNORE';
568   local $SIG{INT} = 'IGNORE';
569   local $SIG{QUIT} = 'IGNORE';
570   local $SIG{TERM} = 'IGNORE';
571   local $SIG{TSTP} = 'IGNORE';
572   local $SIG{PIPE} = 'IGNORE';
573
574   my $oldAutoCommit = $FS::UID::AutoCommit;
575   local $FS::UID::AutoCommit = 0;
576   my $dbh = dbh;
577
578   $error = $new->SUPER::replace($old);
579   if ( $error ) {
580     $dbh->rollback if $oldAutoCommit;
581     return $error if $error;
582   }
583
584   my ( $old_dir, $new_dir, $uid, $gid ) = (
585     $old->getfield('dir'),
586     $new->getfield('dir'),
587     $new->getfield('uid'),
588     $new->getfield('gid'),
589   );
590   if ( $old_dir && $new_dir && $old_dir ne $new_dir && ! $nossh_hack ) {
591     my $queue = new FS::queue { 'job' => 'FS::svc_acct::ssh' };
592     $error = $queue->insert("root\@$shellmachine", eval qq("$usermod") );
593     if ( $error ) {
594       $dbh->rollback if $oldAutoCommit;
595       return "queueing job (transaction rolled back): $error";
596     }
597   }
598
599   if ( $icradius_dbh ) {
600     my $queue = new FS::queue { 'job' => 'FS::svc_acct::icradius_rc_replace' };
601     $error = $queue->insert( $new->username,
602                              $new->_password,
603                            );
604     if ( $error ) {
605       $dbh->rollback if $oldAutoCommit;
606       return "queueing job (transaction rolled back): $error";
607     }
608   }
609
610   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
611   ''; #no error
612 }
613
614 sub icradius_rc_replace {
615   my( $username, $new_password ) = @_;
616  
617    my $sth = $icradius_dbh->prepare(
618      "UPDATE radcheck SET Value = ? WHERE UserName = ? and Attribute = ?"
619    );
620    $sth->execute($new_password, $username, 'Password' )
621      or die "can't update radcheck table: ". $sth->errstr;
622
623   1;
624 }
625
626 =item suspend
627
628 Suspends this account by prefixing *SUSPENDED* to the password.  If there is an
629 error, returns the error, otherwise returns false.
630
631 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
632
633 =cut
634
635 sub suspend {
636   my $self = shift;
637   my %hash = $self->hash;
638   unless ( $hash{_password} =~ /^\*SUSPENDED\* / ) {
639     $hash{_password} = '*SUSPENDED* '.$hash{_password};
640     my $new = new FS::svc_acct ( \%hash );
641     $new->replace($self);
642   } else {
643     ''; #no error (already suspended)
644   }
645 }
646
647 =item unsuspend
648
649 Unsuspends this account by removing *SUSPENDED* from the password.  If there is
650 an error, returns the error, otherwise returns false.
651
652 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
653
654 =cut
655
656 sub unsuspend {
657   my $self = shift;
658   my %hash = $self->hash;
659   if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
660     $hash{_password} = $1;
661     my $new = new FS::svc_acct ( \%hash );
662     $new->replace($self);
663   } else {
664     ''; #no error (already unsuspended)
665   }
666 }
667
668 =item cancel
669
670 Just returns false (no error) for now.
671
672 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
673
674 =item check
675
676 Checks all fields to make sure this is a valid service.  If there is an error,
677 returns the error, otherwise returns false.  Called by the insert and replace
678 methods.
679
680 Sets any fixed values; see L<FS::part_svc>.
681
682 =cut
683
684 sub check {
685   my $self = shift;
686
687   my($recref) = $self->hashref;
688
689   my $x = $self->setfixed;
690   return $x unless ref($x);
691   my $part_svc = $x;
692
693   my $error = $self->ut_numbern('svcnum')
694               || $self->ut_number('domsvc')
695   ;
696   return $error if $error;
697
698   my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
699   if ( $username_uppercase ) {
700     $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/i
701       or return "Illegal username: ". $recref->{username};
702     $recref->{username} = $1;
703   } else {
704     $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/
705       or return "Illegal username: ". $recref->{username};
706     $recref->{username} = $1;
707   }
708
709   if ( $username_letterfirst ) {
710     $recref->{username} =~ /^[a-z]/ or return "Illegal username";
711   } elsif ( $username_letter ) {
712     $recref->{username} =~ /[a-z]/ or return "Illegal username";
713   }
714   if ( $username_noperiod ) {
715     $recref->{username} =~ /\./ and return "Illegal username";
716   }
717   unless ( $username_ampersand ) {
718     $recref->{username} =~ /\&/ and return "Illegal username";
719   }
720
721   $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
722   $recref->{popnum} = $1;
723   return "Unknown popnum" unless
724     ! $recref->{popnum} ||
725     qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
726
727   unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
728
729     $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
730     $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
731
732     $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
733     $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
734     #not all systems use gid=uid
735     #you can set a fixed gid in part_svc
736
737     return "Only root can have uid 0"
738       if $recref->{uid} == 0 && $recref->{username} ne 'root';
739
740 #    $error = $self->ut_textn('finger');
741 #    return $error if $error;
742     $self->getfield('finger') =~
743       /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\*\<\>]*)$/
744         or return "Illegal finger: ". $self->getfield('finger');
745     $self->setfield('finger', $1);
746
747     $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
748       or return "Illegal directory";
749     $recref->{dir} = $1;
750     return "Illegal directory"
751       if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
752     return "Illegal directory"
753       if $recref->{dir} =~ /\&/ && ! $username_ampersand;
754     unless ( $recref->{dir} ) {
755       $recref->{dir} = $dir_prefix . '/';
756       if ( $dirhash > 0 ) {
757         for my $h ( 1 .. $dirhash ) {
758           $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
759         }
760       } elsif ( $dirhash < 0 ) {
761         for my $h ( reverse $dirhash .. -1 ) {
762           $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
763         }
764       }
765       $recref->{dir} .= $recref->{username};
766     ;
767     }
768
769     unless ( $recref->{username} eq 'sync' ) {
770       if ( grep $_ eq $recref->{shell}, @shells ) {
771         $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
772       } else {
773         return "Illegal shell \`". $self->shell. "\'; ".
774                $conf->dir. "/shells contains: @shells";
775       }
776     } else {
777       $recref->{shell} = '/bin/sync';
778     }
779
780     $recref->{quota} =~ /^(\d*)$/ or return "Illegal quota (unimplemented)";
781     $recref->{quota} = $1;
782
783   } else {
784     $recref->{gid} ne '' ? 
785       return "Can't have gid without uid" : ( $recref->{gid}='' );
786     $recref->{finger} ne '' ? 
787       return "Can't have finger-name without uid" : ( $recref->{finger}='' );
788     $recref->{dir} ne '' ? 
789       return "Can't have directory without uid" : ( $recref->{dir}='' );
790     $recref->{shell} ne '' ? 
791       return "Can't have shell without uid" : ( $recref->{shell}='' );
792     $recref->{quota} ne '' ? 
793       return "Can't have quota without uid" : ( $recref->{quota}='' );
794   }
795
796   unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
797     unless ( $recref->{slipip} eq '0e0' ) {
798       $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
799         or return "Illegal slipip". $self->slipip;
800       $recref->{slipip} = $1;
801     } else {
802       $recref->{slipip} = '0e0';
803     }
804
805   }
806
807   #arbitrary RADIUS stuff; allow ut_textn for now
808   foreach ( grep /^radius_/, fields('svc_acct') ) {
809     $self->ut_textn($_);
810   }
811
812   #generate a password if it is blank
813   $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
814     unless ( $recref->{_password} );
815
816   #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
817   if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
818     $recref->{_password} = $1.$3;
819     #uncomment this to encrypt password immediately upon entry, or run
820     #bin/crypt_pw in cron to give new users a window during which their
821     #password is available to techs, for faxing, etc.  (also be aware of 
822     #radius issues!)
823     #$recref->{password} = $1.
824     #  crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
825     #;
826   } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$]{13,34})$/ ) {
827     $recref->{_password} = $1.$3;
828   } elsif ( $recref->{_password} eq '*' ) {
829     $recref->{_password} = '*';
830   } elsif ( $recref->{_password} eq '!!' ) {
831     $recref->{_password} = '!!';
832   } else {
833     #return "Illegal password";
834     return "Illegal password: ". $recref->{_password};
835   }
836
837   ''; #no error
838 }
839
840 =item radius
841
842 Depriciated, use radius_reply instead.
843
844 =cut
845
846 sub radius {
847   carp "FS::svc_acct::radius depriciated, use radius_reply";
848   $_[0]->radius_reply;
849 }
850
851 =item radius_reply
852
853 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
854 reply attributes of this record.
855
856 Note that this is now the preferred method for reading RADIUS attributes - 
857 accessing the columns directly is discouraged, as the column names are
858 expected to change in the future.
859
860 =cut
861
862 sub radius_reply { 
863   my $self = shift;
864   map {
865     /^(radius_(.*))$/;
866     my($column, $attrib) = ($1, $2);
867     #$attrib =~ s/_/\-/g;
868     ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
869   } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
870 }
871
872 =item radius_check
873
874 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
875 check attributes of this record.
876
877 Accessing RADIUS attributes directly is not supported and will break in the
878 future.
879
880 =cut
881
882 sub radius_check {
883   my $self = shift;
884   map {
885     /^(rc_(.*))$/;
886     my($column, $attrib) = ($1, $2);
887     #$attrib =~ s/_/\-/g;
888     ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
889   } grep { /^rc_/ && $self->getfield($_) } fields( $self->table );
890 }
891
892 =item domain
893
894 Returns the domain associated with this account.
895
896 =cut
897
898 sub domain {
899   my $self = shift;
900   if ( $self->domsvc ) {
901     #$self->svc_domain->domain;
902     my $svc_domain = $self->svc_domain
903       or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
904     $svc_domain->domain;
905   } else {
906     $mydomain or die "svc_acct.domsvc is null and no legacy domain config file";
907   }
908 }
909
910 =item svc_domain
911
912 Returns the FS::svc_domain record for this account's domain (see
913 L<FS::svc_domain>.
914
915 =cut
916
917 sub svc_domain {
918   my $self = shift;
919   $self->{'_domsvc'}
920     ? $self->{'_domsvc'}
921     : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
922 }
923
924 =item email
925
926 Returns an email address associated with the account.
927
928 =cut
929
930 sub email {
931   my $self = shift;
932   $self->username. '@'. $self->domain;
933 }
934
935 =item ssh
936
937 =cut
938
939 sub ssh {
940   my ( $host, @cmd_and_args ) = @_;
941
942   use IO::File;
943   my $reader = IO::File->new();
944   my $writer = IO::File->new();
945   my $error = IO::File->new();
946
947   &Net::SSH::sshopen3( $host, $reader, $writer, $error, @cmd_and_args) or die $!;
948
949   local $/ = undef;
950   my $output_stream = <$writer>;
951   my $error_stream = <$error>;
952   if ( length $error_stream ) {
953     #warn "[FS::svc_acct::ssh] STDERR $error_stream";
954     die "[FS::svc_acct::ssh] STDERR $error_stream";
955   }
956   if ( length $output_stream ) {
957     warn "[FS::svc_acct::ssh] STDOUT $output_stream";
958   }
959
960 #  &Net::SSH::ssh(@args,">>/usr/local/etc/freeside/sshoutput 2>&1");
961 }
962
963 =back
964
965 =head1 VERSION
966
967 $Id: svc_acct.pm,v 1.59 2001-12-20 02:07:04 ivan Exp $
968
969 =head1 BUGS
970
971 The bits which ssh should fork before doing so (or maybe queue jobs for a
972 daemon).
973
974 The $recref stuff in sub check should be cleaned up.
975
976 The suspend, unsuspend and cancel methods update the database, but not the
977 current object.  This is probably a bug as it's unexpected and
978 counterintuitive.
979
980 =head1 SEE ALSO
981
982 L<FS::svc_Common>, L<FS::Record>, L<FS::Conf>, L<FS::cust_svc>,
983 L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>, L<freeside-queued>),
984 L<Net::SSH>, L<ssh>, L<FS::svc_acct_pop>,
985 schema.html from the base documentation.
986
987 =cut
988
989 1;
990