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