add a _password_encoding field
[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 quoted for the shell already (do not add additional quotes).
161   <LI><code>$ldap_password</code> - Password in LDAP/RFC2307 format (for example, "{PLAIN}himom", "{CRYPT}94pAVyK/4oIBk" or "{MD5}5426824942db4253f87a1009fd5d2d4").  When used on the command line (rather than STDIN), it will be quoted for the shell already (do not add additional quotes).
162   <LI><code>$uid</code>
163   <LI><code>$gid</code>
164   <LI><code>$finger</code> - GECOS.  When used on the command line (rather than STDIN), it will be quoted for the shell already (do not add additional quotes).
165   <LI><code>$first</code> - First name of GECOS.  When used on the command line (rather than STDIN), it will be quoted for the shell already (do not add additional quotes).
166   <LI><code>$last</code> - Last name of GECOS.  When used on the command line (rather than STDIN), it will be quoted for the shell already (do not add additional quotes).
167   <LI><code>$dir</code> - home directory
168   <LI><code>$shell</code>
169   <LI><code>$quota</code>
170   <LI><code>@radius_groups</code>
171   <LI><code>$reasonnum (when suspending)</code>
172   <LI><code>$reasontext (when suspending)</code>
173   <LI><code>$reasontypenum (when suspending)</code>
174   <LI><code>$reasontypetext (when suspending)</code>
175   <LI>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.
176 </UL>
177 END
178 );
179
180 sub _groups_susp_reason_map { shift->_map('groups_susp_reason'); }
181
182 sub _map {
183   my $self = shift;
184   map { reverse(/^\s*(\S+)\s*(.*)\s*$/) } split("\n", $self->option(shift) );
185 }
186
187 sub rebless { shift; }
188
189 sub _export_insert {
190   my($self) = shift;
191   $self->_export_command('useradd', @_);
192 }
193
194 sub _export_delete {
195   my($self) = shift;
196   $self->_export_command('userdel', @_);
197 }
198
199 sub _export_suspend {
200   my($self) = shift;
201   $self->_export_command_or_super('suspend', @_);
202 }
203
204 sub _export_unsuspend {
205   my($self) = shift;
206   $self->_export_command_or_super('unsuspend', @_);
207 }
208
209 sub _export_command_or_super {
210   my($self, $action) = (shift, shift);
211   if ( $self->option($action) =~ /^\s*$/ ) {
212     my $method = "SUPER::_export_$action";
213     $self->$method(@_);
214   } else {
215     $self->_export_command($action, @_);
216   }
217 };
218
219 sub _export_command {
220   my ( $self, $action, $svc_acct) = (shift, shift, shift);
221   my $command = $self->option($action);
222   return '' if $command =~ /^\s*$/;
223   my $stdin = $self->option($action."_stdin");
224
225   no strict 'vars';
226   {
227     no strict 'refs';
228     ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
229
230     # snarfs are unused at this point?
231     my $count = 1;
232     foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
233       ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
234         foreach qw( machine username _password );
235       $count++;
236     }
237   }
238
239   my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
240   if ( $cust_pkg ) {
241     $email = ( grep { $_ !~ /^(POST|FAX)$/ } $cust_pkg->cust_main->invoicing_list )[0];
242   } else {
243     $email = '';
244   }
245
246   $finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
247   ($first, $last ) = ( $1, $2 );
248   $domain = $svc_acct->domain;
249
250   $quoted_password = shell_quote $_password;
251
252   $crypt_password = $svc_acct->crypt_password( $self->option('crypt') );
253   $ldap_password  = $svc_acct->ldap_password(  $self->option('crypt') );
254
255   @radius_groups = $svc_acct->radius_groups;
256
257   my ($reasonnum, $reasontext, $reasontypenum, $reasontypetext);
258   if ( $cust_pkg && $action eq 'suspend' && (my $r = $cust_pkg->last_reason) ) {
259     $reasonnum = $r->reasonnum;
260     $reasontext = $r->reason;
261     $reasontypenum = $r->reason_type;
262     $reasontypetext = $r->reasontype->type;
263
264     my %reasonmap = $self->_groups_susp_reason_map;
265     my $userspec = '';
266     $userspec = $reasonmap{$reasonnum}
267       if exists($reasonmap{$reasonnum});
268     $userspec = $reasonmap{$reasontext}
269       if (!$userspec && exists($reasonmap{$reasontext}));
270
271     my $suspend_user;
272     if ( $userspec =~ /^\d+$/ ) {
273       $suspend_user = qsearchs( 'svc_acct', { 'svcnum' => $userspec } );
274     } elsif ( $userspec =~ /^\S+\@\S+$/ ) {
275       my ($username,$domain) = split(/\@/, $userspec);
276       for my $user (qsearch( 'svc_acct', { 'username' => $username } )){
277         $suspend_user = $user if $userspec eq $user->email;
278       }
279     } elsif ($userspec) {
280       $suspend_user = qsearchs( 'svc_acct', { 'username' => $userspec } );
281     }
282
283     @radius_groups = $suspend_user->radius_groups
284       if $suspend_user;  
285
286   } else {
287     $reasonnum = $reasontext = $reasontypenum = $reasontypetext = '';
288   }
289
290   my $stdin_string = eval(qq("$stdin"));
291
292   $first = shell_quote $first;
293   $last = shell_quote $last;
294   $finger = shell_quote $finger;
295   $crypt_password = shell_quote $crypt_password;
296   $ldap_password  = shell_quote $ldap_password;
297
298   my $command_string = eval(qq("$command"));
299
300   $self->shellcommands_queue( $svc_acct->svcnum,
301     user         => $self->option('user')||'root',
302     host         => $self->machine,
303     command      => $command_string,
304     stdin_string => $stdin_string,
305   );
306 }
307
308 sub _export_replace {
309   my($self, $new, $old ) = (shift, shift, shift);
310   my $command = $self->option('usermod');
311   my $stdin = $self->option('usermod_stdin');
312   no strict 'vars';
313   {
314     no strict 'refs';
315     ${"old_$_"} = $old->getfield($_) foreach $old->fields;
316     ${"new_$_"} = $new->getfield($_) foreach $new->fields;
317   }
318   $new_finger =~ /^(.*)\s+(\S+)$/ or $new_finger =~ /^((.*))$/;
319   ($new_first, $new_last ) = ( $1, $2 );
320   $quoted_new__password = shell_quote $new__password; #old, wrong?
321   $new_quoted_password = shell_quote $new__password; #new, better?
322   $old_domain = $old->domain;
323   $new_domain = $new->domain;
324
325   $new_crypt_password = $new->crypt_password( $self->option('crypt') );
326   $new_ldap_password  = $new->ldap_password(  $self->option('crypt') );
327
328   @old_radius_groups = $old->radius_groups;
329   @new_radius_groups = $new->radius_groups;
330
331   my $error = '';
332   if ( $self->option('usermod_pwonly') || $self->option('usermod_nousername') ){
333     if ( $old_username ne $new_username ) {
334       $error ||= "can't change username";
335     }
336   }
337   if ( $self->option('usermod_pwonly') ) {
338     if ( $old_domain ne $new_domain ) {
339       $error ||= "can't change domain";
340     }
341     if ( $old_uid != $new_uid ) {
342       $error ||= "can't change uid";
343     }
344     if ( $old_gid != $new_gid ) {
345       $error ||= "can't change gid";
346     }
347     if ( $old_dir ne $new_dir ) {
348       $error ||= "can't change dir";
349     }
350     #if ( join("\n", sort @old_radius_groups) ne
351     #     join("\n", sort @new_radius_groups)    ) {
352     #  $error ||= "can't change RADIUS groups";
353     #}
354   }
355   return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
356     if $error;
357
358   my $stdin_string = eval(qq("$stdin"));
359
360   $new_first = shell_quote $new_first;
361   $new_last = shell_quote $new_last;
362   $new_finger = shell_quote $new_finger;
363   $new_crypt_password = shell_quote $new_crypt_password;
364   $new_ldap_password  = shell_quote $new_ldap_password;
365
366   my $command_string = eval(qq("$command"));
367
368   $self->shellcommands_queue( $new->svcnum,
369     user         => $self->option('user')||'root',
370     host         => $self->machine,
371     command      => $command_string,
372     stdin_string => $stdin_string,
373   );
374 }
375
376 #a good idea to queue anything that could fail or take any time
377 sub shellcommands_queue {
378   my( $self, $svcnum ) = (shift, shift);
379   my $queue = new FS::queue {
380     'svcnum' => $svcnum,
381     'job'    => "FS::part_export::shellcommands::ssh_cmd",
382   };
383   $queue->insert( @_ );
384 }
385
386 sub ssh_cmd { #subroutine, not method
387   use Net::SSH '0.08';
388   &Net::SSH::ssh_cmd( { @_ } );
389 }
390
391 #sub shellcommands_insert { #subroutine, not method
392 #}
393 #sub shellcommands_replace { #subroutine, not method
394 #}
395 #sub shellcommands_delete { #subroutine, not method
396 #}
397
398 1;
399