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