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