do shell_quote-ing after STDIN strings are evaluated
[freeside.git] / FS / FS / part_export / shellcommands.pm
1 package FS::part_export::shellcommands;
2
3 use vars qw(@ISA %info);
4 use Tie::IxHash;
5 use String::ShellQuote;
6 use FS::part_export;
7 use FS::Record qw( qsearch qsearchs );
8
9 @ISA = qw(FS::part_export);
10
11 tie my %options, 'Tie::IxHash',
12   'user' => { label=>'Remote username', default=>'root' },
13   'useradd' => { label=>'Insert command',
14                  default=>'useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username'
15                 #default=>'cp -pr /etc/skel $dir; chown -R $uid.$gid $dir'
16                },
17   'useradd_stdin' => { label=>'Insert command STDIN',
18                        type =>'textarea',
19                        default=>'',
20                      },
21   'userdel' => { label=>'Delete command',
22                  default=>'userdel -r $username',
23                  #default=>'rm -rf $dir',
24                },
25   'userdel_stdin' => { label=>'Delete command STDIN',
26                        type =>'textarea',
27                        default=>'',
28                      },
29   'usermod' => { label=>'Modify command',
30                  default=>'usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -g $new_gid -p $new_crypt_password $old_username',
31                 #default=>'[ -d $old_dir ] && mv $old_dir $new_dir || ( '.
32                  #  'chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; '.
33                  #  'find . -depth -print | cpio -pdm $new_dir; '.
34                  #  'chmod u-t $new_dir; chown -R $uid.$gid $new_dir; '.
35                  #  'rm -rf $old_dir'.
36                  #')'
37                },
38   'usermod_stdin' => { label=>'Modify command STDIN',
39                        type =>'textarea',
40                        default=>'',
41                      },
42   'usermod_pwonly' => { label=>'Disallow username, domain, uid, gid, and dir changes', #and RADIUS group changes',
43                         type =>'checkbox',
44                       },
45   'usermod_nousername' => { label=>'Disallow just username changes',
46                             type =>'checkbox',
47                           },
48   'suspend' => { label=>'Suspension command',
49                  default=>'usermod -L $username',
50                },
51   'suspend_stdin' => { label=>'Suspension command STDIN',
52                        default=>'',
53                      },
54   'unsuspend' => { label=>'Unsuspension command',
55                    default=>'usermod -U $username',
56                  },
57   'unsuspend_stdin' => { label=>'Unsuspension command STDIN',
58                          default=>'',
59                        },
60   'crypt' => { label   => 'Default password encryption',
61                type=>'select', options=>[qw(crypt md5)],
62                default => 'crypt',
63              },
64   'groups_susp_reason' => { label =>
65                              'Radius group mapping to reason (via template user)',
66                             type  => 'textarea',
67                           },
68 ;
69
70 %info = (
71   'svc'      => 'svc_acct',
72   'desc'     =>
73     'Real-time export via remote SSH (i.e. useradd, userdel, etc.)',
74   'options'  => \%options,
75   'nodomain' => 'Y',
76   'notes' => <<'END'
77 Run remote commands via SSH.  Usernames are considered unique (also see
78 shellcommands_withdomain).  You probably want this if the commands you are
79 running will not accept a domain as a parameter.  You will need to
80 <a href="../docs/ssh.html">setup SSH for unattended operation</a>.
81
82 <BR><BR>Use these buttons for some useful presets:
83 <UL>
84   <LI>
85     <INPUT TYPE="button" VALUE="Linux" onClick='
86       this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
87       this.form.useradd_stdin.value = "";
88       this.form.userdel.value = "userdel -r $username";
89       this.form.userdel_stdin.value="";
90       this.form.usermod.value = "usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -g $new_gid -p $new_crypt_password $old_username";
91       this.form.usermod_stdin.value = "";
92       this.form.suspend.value = "usermod -L $username";
93       this.form.suspend_stdin.value="";
94       this.form.unsuspend.value = "usermod -U $username";
95       this.form.unsuspend_stdin.value="";
96     '>
97   <LI>
98     <INPUT TYPE="button" VALUE="FreeBSD before 4.10 / 5.3" onClick='
99       this.form.useradd.value = "lockf /etc/passwd.lock pw useradd $username -d $dir -m -s $shell -u $uid -c $finger -h 0";
100       this.form.useradd_stdin.value = "$_password\n";
101       this.form.userdel.value = "lockf /etc/passwd.lock pw userdel $username -r"; this.form.userdel_stdin.value="";
102       this.form.usermod.value = "lockf /etc/passwd.lock pw usermod $old_username -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -g $new_gid -c $new_finger -h 0";
103       this.form.usermod_stdin.value = "$new__password\n"; this.form.suspend.value = "lockf /etc/passwd.lock pw lock $username";
104       this.form.suspend_stdin.value="";
105       this.form.unsuspend.value = "lockf /etc/passwd.lock pw unlock $username"; this.form.unsuspend_stdin.value="";
106     '>
107     Note: On FreeBSD versions before 5.3 and 4.10 (4.10 is after 4.9, not
108     4.1!), due to deficient locking in pw(1), you must disable the chpass(1),
109     chsh(1), chfn(1), passwd(1), and vipw(1) commands, or replace them with
110     wrappers that prepend "lockf /etc/passwd.lock".  Alternatively, apply the
111     patch in
112     <A HREF="http://www.freebsd.org/cgi/query-pr.cgi?pr=23501">FreeBSD PR#23501</A>
113     and use the "FreeBSD 4.10 / 5.3 or later" button below.
114   <LI>
115     <INPUT TYPE="button" VALUE="FreeBSD 4.10 / 5.3 or later" onClick='
116       this.form.useradd.value = "pw useradd $username -d $dir -m -s $shell -u $uid -g $gid -c $finger -h 0";
117       this.form.useradd_stdin.value = "$_password\n";
118       this.form.userdel.value = "pw userdel $username -r";
119       this.form.userdel_stdin.value="";
120       this.form.usermod.value = "pw usermod $old_username -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -g $new_gid -c $new_finger -h 0";
121       this.form.usermod_stdin.value = "$new__password\n";
122       this.form.suspend.value = "pw lock $username";
123       this.form.suspend_stdin.value="";
124       this.form.unsuspend.value = "pw unlock $username";
125       this.form.unsuspend_stdin.value="";
126     '>
127   <LI>
128     <INPUT TYPE="button" VALUE="NetBSD/OpenBSD" onClick='
129       this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
130       this.form.useradd_stdin.value = "";
131       this.form.userdel.value = "userdel -r $username";
132       this.form.userdel_stdin.value="";
133       this.form.usermod.value = "usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -g $new_gid -p $new_crypt_password $old_username";
134       this.form.usermod_stdin.value = "";
135       this.form.suspend.value = "";
136       this.form.suspend_stdin.value="";
137       this.form.unsuspend.value = "";
138       this.form.unsuspend_stdin.value="";
139     '>
140   <LI>
141     <INPUT TYPE="button" VALUE="Just maintain directories (use with sysvshell or bsdshell)" onClick='
142       this.form.useradd.value = "cp -pr /etc/skel $dir; chown -R $uid.$gid $dir"; this.form.useradd_stdin.value = "";
143       this.form.usermod.value = "[ -d $old_dir ] && mv $old_dir $new_dir || ( chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; find . -depth -print | cpio -pdm $new_dir; chmod u-t $new_dir; chown -R $new_uid.$new_gid $new_dir; rm -rf $old_dir )";
144       this.form.usermod_stdin.value = "";
145       this.form.userdel.value = "rm -rf $dir";
146       this.form.userdel_stdin.value="";
147       this.form.suspend.value = "";
148       this.form.suspend_stdin.value="";
149       this.form.unsuspend.value = "";
150       this.form.unsuspend_stdin.value="";
151     '>
152 </UL>
153
154 The following variables are available for interpolation (prefixed with new_ or
155 old_ for replace operations):
156 <UL>
157   <LI><code>$username</code>
158   <LI><code>$_password</code>
159   <LI><code>$quoted_password</code> - unencrypted password, already quoted for the shell (do not add additional quotes).
160   <LI><code>$crypt_password</code> - encrypted password.  When used on the command line (rather than STDIN), it will be already quoted for the shell (do not add additional quotes).
161   <LI><code>$uid</code>
162   <LI><code>$gid</code>
163   <LI><code>$finger</code> - GECOS.  When used on the command line (rather than STDIN), it will be already quoted for the shell (do not add additional quotes).
164   <LI><code>$first</code> - First name of GECOS.  When used on the command line (rather than STDIN), it will be already quoted for the shell (do not add additional quotes).
165   <LI><code>$last</code> - Last name of GECOS.  When used on the command line (rather than STDIN), it will be already quoted for the shell (do not add additional quotes).
166   <LI><code>$dir</code> - home directory
167   <LI><code>$shell</code>
168   <LI><code>$quota</code>
169   <LI><code>@radius_groups</code>
170   <LI><code>$reasonnum (when suspending)</code>
171   <LI><code>$reasontext (when suspending)</code>
172   <LI><code>$reasontypenum (when suspending)</code>
173   <LI><code>$reasontypetext (when suspending)</code>
174   <LI>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.
175 </UL>
176 END
177 );
178
179 sub _groups_susp_reason_map { shift->_map('groups_susp_reason'); }
180
181 sub _map {
182   my $self = shift;
183   map { reverse(/^\s*(\S+)\s*(.*)\s*$/) } split("\n", $self->option(shift) );
184 }
185
186 sub rebless { shift; }
187
188 sub _export_insert {
189   my($self) = shift;
190   $self->_export_command('useradd', @_);
191 }
192
193 sub _export_delete {
194   my($self) = shift;
195   $self->_export_command('userdel', @_);
196 }
197
198 sub _export_suspend {
199   my($self) = shift;
200   $self->_export_command_or_super('suspend', @_);
201 }
202
203 sub _export_unsuspend {
204   my($self) = shift;
205   $self->_export_command_or_super('unsuspend', @_);
206 }
207
208 sub _export_command_or_super {
209   my($self, $action) = (shift, shift);
210   if ( $self->option($action) =~ /^\s*$/ ) {
211     my $method = "SUPER::_export_$action";
212     $self->$method(@_);
213   } else {
214     $self->_export_command($action, @_);
215   }
216 };
217
218 sub _export_command {
219   my ( $self, $action, $svc_acct) = (shift, shift, shift);
220   my $command = $self->option($action);
221   return '' if $command =~ /^\s*$/;
222   my $stdin = $self->option($action."_stdin");
223
224   no strict 'vars';
225   {
226     no strict 'refs';
227     ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
228
229     # snarfs are unused at this point?
230     my $count = 1;
231     foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
232       ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
233         foreach qw( machine username _password );
234       $count++;
235     }
236   }
237
238   my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
239   if ( $cust_pkg ) {
240     $email = ( grep { $_ !~ /^(POST|FAX)$/ } $cust_pkg->cust_main->invoicing_list )[0];
241   } else {
242     $email = '';
243   }
244
245   $finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
246   ($first, $last ) = ( $1, $2 );
247   $domain = $svc_acct->domain;
248
249   $quoted_password = shell_quote $_password;
250
251   $crypt_password = $svc_acct->crypt_password( $self->option('crypt') );
252
253   @radius_groups = $svc_acct->radius_groups;
254
255   my ($reasonnum, $reasontext, $reasontypenum, $reasontypetext);
256   if ( $cust_pkg && $action eq 'suspend' && (my $r = $cust_pkg->last_reason) ) {
257     $reasonnum = $r->reasonnum;
258     $reasontext = $r->reason;
259     $reasontypenum = $r->reason_type;
260     $reasontypetext = $r->reasontype->type;
261
262     my %reasonmap = $self->_groups_susp_reason_map;
263     my $userspec = '';
264     $userspec = $reasonmap{$reasonnum}
265       if exists($reasonmap{$reasonnum});
266     $userspec = $reasonmap{$reasontext}
267       if (!$userspec && exists($reasonmap{$reasontext}));
268
269     my $suspend_user;
270     if ( $userspec =~ /^\d+$/ ) {
271       $suspend_user = qsearchs( 'svc_acct', { 'svcnum' => $userspec } );
272     } elsif ( $userspec =~ /^\S+\@\S+$/ ) {
273       my ($username,$domain) = split(/\@/, $userspec);
274       for my $user (qsearch( 'svc_acct', { 'username' => $username } )){
275         $suspend_user = $user if $userspec eq $user->email;
276       }
277     } elsif ($userspec) {
278       $suspend_user = qsearchs( 'svc_acct', { 'username' => $userspec } );
279     }
280
281     @radius_groups = $suspend_user->radius_groups
282       if $suspend_user;  
283
284   } else {
285     $reasonnum = $reasontext = $reasontypenum = $reasontypetext = '';
286   }
287
288   my $stdin_string = eval(qq("$stdin"));
289
290   $first = shell_quote $first;
291   $last = shell_quote $last;
292   $finger = shell_quote $finger;
293   $crypt_password = shell_quote $crypt_password;
294
295   my $command_string = eval(qq("$command"));
296
297   $self->shellcommands_queue( $svc_acct->svcnum,
298     user         => $self->option('user')||'root',
299     host         => $self->machine,
300     command      => $command_string,
301     stdin_string => $stdin_string,
302   );
303 }
304
305 sub _export_replace {
306   my($self, $new, $old ) = (shift, shift, shift);
307   my $command = $self->option('usermod');
308   my $stdin = $self->option('usermod_stdin');
309   no strict 'vars';
310   {
311     no strict 'refs';
312     ${"old_$_"} = $old->getfield($_) foreach $old->fields;
313     ${"new_$_"} = $new->getfield($_) foreach $new->fields;
314   }
315   $new_finger =~ /^(.*)\s+(\S+)$/ or $new_finger =~ /^((.*))$/;
316   ($new_first, $new_last ) = ( $1, $2 );
317   $quoted_new__password = shell_quote $new__password; #old, wrong?
318   $new_quoted_password = shell_quote $new__password; #new, better?
319   $old_domain = $old->domain;
320   $new_domain = $new->domain;
321
322   $new_crypt_password = $new->crypt_password( $self->option('crypt') );
323
324   @old_radius_groups = $old->radius_groups;
325   @new_radius_groups = $new->radius_groups;
326
327   my $error = '';
328   if ( $self->option('usermod_pwonly') || $self->option('usermod_nousername') ){
329     if ( $old_username ne $new_username ) {
330       $error ||= "can't change username";
331     }
332   }
333   if ( $self->option('usermod_pwonly') ) {
334     if ( $old_domain ne $new_domain ) {
335       $error ||= "can't change domain";
336     }
337     if ( $old_uid != $new_uid ) {
338       $error ||= "can't change uid";
339     }
340     if ( $old_gid != $new_gid ) {
341       $error ||= "can't change gid";
342     }
343     if ( $old_dir ne $new_dir ) {
344       $error ||= "can't change dir";
345     }
346     #if ( join("\n", sort @old_radius_groups) ne
347     #     join("\n", sort @new_radius_groups)    ) {
348     #  $error ||= "can't change RADIUS groups";
349     #}
350   }
351   return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
352     if $error;
353
354   my $stdin_string = eval(qq("$stdin"));
355
356   $new_first = shell_quote $new_first;
357   $new_last = shell_quote $new_last;
358   $new_finger = shell_quote $new_finger;
359   $new_crypt_password = shell_quote $crypt_password;
360
361   my $command_string = eval(qq("$command"));
362
363   $self->shellcommands_queue( $new->svcnum,
364     user         => $self->option('user')||'root',
365     host         => $self->machine,
366     command      => $command_string,
367     stdin_string => $stdin_string,
368   );
369 }
370
371 #a good idea to queue anything that could fail or take any time
372 sub shellcommands_queue {
373   my( $self, $svcnum ) = (shift, shift);
374   my $queue = new FS::queue {
375     'svcnum' => $svcnum,
376     'job'    => "FS::part_export::shellcommands::ssh_cmd",
377   };
378   $queue->insert( @_ );
379 }
380
381 sub ssh_cmd { #subroutine, not method
382   use Net::SSH '0.08';
383   &Net::SSH::ssh_cmd( { @_ } );
384 }
385
386 #sub shellcommands_insert { #subroutine, not method
387 #}
388 #sub shellcommands_replace { #subroutine, not method
389 #}
390 #sub shellcommands_delete { #subroutine, not method
391 #}
392
393 1;
394