New stuff.
[clinton/scripts.git] / freeze
1 #!/usr/bin/perl
2
3 #
4 # Purpose: freeze user (cancel user services except email), or unfreeze user.
5 #
6 # Usage (RUN AS _ADMIN USER ON DELEUZE WITHOUT SUDO):
7 #
8 # Display frozen users or details for one user (one user implies -verbose):
9 # freeze [user], OR
10 # freeze [ --action list | -a ] [--verbose | -v] [user]
11 #
12 # Freeze user:
13 # freeze -a f user
14 #
15 # Unfreeze:
16 # freeze -a u user
17 #
18 #
19 # How it works:
20 #
21 # Script invokes a list of modules, where each module knows how to
22 # implement and unimplement a specific restriction. Implemented modules
23 # should be listed in @modules array or they won't get called. Admin
24 # can override list of modules with --modules=one,two,three.
25 #
26 # Modules execute in the order as specified for freeze, and in reverse
27 # order for unfreeze.
28 #
29 # Module gets called as &module($action, $user) . The proper way to
30 # test for which action is requested is as shown below. (Note that
31 # module adds or removes itself to the list of modules that ran on the user).
32 #
33 # if ($a =~ /^f/i) {
34 # ... freeze ...
35 # push @{ $$store{$u}{modules} }, 'MODULE';
36 # } elsif ($a =~ /^u/i) {
37 # ... unfreeze ...
38 # @{ $$store{$u}{modules} } = grep {!/^MODULE$/} @{ $$store{$u}{modules} };
39 # }
40 #
41 # Also each system-modifying action should be wrapped in if (!DRY) as shown:
42 #
43 # if (!DRY) {
44 # system(qq{SOME COMMAND})
45 # } else {
46 # warn qq|SOME COMMAND|
47 # }
48 #
49 # User is valid system username, and user's getent entry is prepared and
50 # retrievable through @user array, should you need some of its info.
51 #
52 # Module can save all persistent data to $$store{$user}{$modulename}. For
53 # example, after cron module removes user from all cron.allows, it
54 # registers the machines where user was removed to
55 # @{ $$store{$u}{cron} }, so that it can revert it back if user is
56 # unfreezed.
57 #
58 # Module 'record' creates or deletes initial user entry in $$store.
59 # If you create a new module that will use the store, announce its
60 # hash key by creating it empty in record().
61 #
62 # For additional detail, here's how the stored hash might look like:
63 #
64 #
65 # $store = {
66 # user1 => {
67 # date => 'Sun Jun 29 18:45:43 CEST 2008',
68 # getent => [qw/docelic 1000 1000 DavorOcelic /home/docelic /bin/bash]
69 #
70 # modules => [qw/login domtool cron slay/], # (modules that ran)
71 # domains => [qw/spinlock.hr test.hr/], # (domains that were removed)
72 # cron => [qw/mire/], # (hosts where cron.allow entry was removed)
73 # },
74 # user2 => {
75 # ...
76 # },
77 # ...
78 # ...
79 # ...
80 # }
81 #
82 #
83 # Wiki page relating to this script is http://wiki.hcoop.net/MemberFreezing
84 #
85 # Davor Ocelic, docelic@hcoop.net, Sun Jun 29 18:41:02 CEST 2008
86 #
87 #
88
89 use warnings;
90 use strict;
91
92 use Storable qw/lock_nstore lock_retrieve/;
93 use Getopt::Long qw/GetOptions/;
94
95 use constant DEBUG => 1;
96 use constant DRY => 0;
97 use constant STORE => "/var/tmp/frozen/cache";
98 use constant DEFAULT_SHELL => '/bin/bash';
99 use constant FROZEN_SHELL => '/afs/hcoop.net/common/etc/scripts/frozen_shell';
100 use constant PUBLIC_ACCESS => (qw/mire/);
101 use constant RUN_SERVER => 'deleuze';
102
103 my $store = {}; # cached info
104 my $action = 'list'; # list, freeze, unfreeze
105 my $verbose = 0; # 0/1 (print which modules were applied to user in freezing)
106 my $force = 0; # 0/1 (force action even if user already freezed/unfreezed)
107 my $user; # who to freeze/unfreeze
108 my @user; # getent passwd entry for user
109 my $modules; # list of freeze/unfreeze actions to take
110 my @modules; # modules, possibly overriden by split /,\s+/, $modules
111
112 # Keep modules listed in order of application, honoring possible dependencies.
113 @modules = (qw/record login domtool slay/);
114
115 unless ( GetOptions (
116 'do|d|a=s' => \$action,
117 'verbose|v!' => \$verbose,
118 'force!' => \$force,
119 'modules|m=s' => \$modules,
120 )) { die "Error parsing options: $!\n" }
121
122 $user = shift ;
123
124 if ( $> == 0 or $< == 0 ) {
125 die "Run script under admin account without sudo.\n";
126 }
127
128 if ( -e STORE ) {
129 $store = lock_retrieve(STORE);
130 } else {
131 warn "No '" . STORE . "', skipping load.\n";
132 }
133
134 if ( `hostname` ne RUN_SERVER . "\n" ) {
135 die "Please run script on " . RUN_SERVER . "\n";
136 }
137
138 if ( $action =~ /^l/i ) {
139 while (my ($k,$v) = each %$store ) {
140 if (! $user or $user eq $k ) {
141 print "$k $$v{date}\n";
142 print " @{$$v{modules}}\n" if $verbose or $user;
143 }
144 }
145 exit 0;
146 }
147
148 $user or die "Must specify user to freeze/unfreeze. Exiting.\n";
149 $user =~ /^[a-z0-9]+$/ or die "Invalid username (not [a-z0-9]+).\n";
150 @user = split(/:/, `getent passwd $user`);
151 @user or die "No such user? (getent passwd USER empty.)\n";
152 chomp $user[$#user];
153
154 if ( $action !~ /^[fu]/i ) {
155 warn "Unknown action: use -a [list (l)|freeze (f)|unfreeze (u)]\n";
156 }
157
158 if ( $action =~ /^f/i ) {
159 if ( exists $$store{$user} ) {
160 warn "User already frozen since $$store{$user}{date}.\n";
161 if (! $force) {
162 die "Exiting.\n";
163 }
164 }
165 }
166
167 elsif ( $action =~ /^u/i ) {
168 if (! exists $$store{$user} ) {
169 warn "User not frozen in the first place.\n";
170 if (! $force) {
171 die "Exiting.\n";
172 }
173 }
174 }
175
176 else {
177 warn "How did you get through?\n";
178 die;
179 }
180
181
182 if ($modules) { @modules = split /[,\s+]/, $modules; }
183 for ( $action =~ /^u/i ? reverse @modules : @modules ) {
184 no strict 'refs';
185 print "Module: $_\n";
186 &{ $_ }($action, $user);
187 }
188
189 lock_nstore $store, STORE;
190
191
192 ###########################################################################
193 # Helpers below
194
195 # GETENT (available to modules automatically in @user):
196 # 0 1 2 3 4 5 6
197 # docelic:x:10235:65534:docelic:/afs/hcoop.net/user/d/do/docelic:/bin/bash
198
199 sub record {
200 my ($a, $u) = @_;
201 $a =~ /^f/i and $$store{$u} = {
202 date => scalar localtime,
203 getent => [ @user ],
204 modules => [],
205 domains => [],
206 cron => [],
207 };
208 $a =~ /^u/i and delete $$store{$u};
209 }
210
211 sub login {
212 my ($a, $u) = @_;
213
214 if ($a =~ /^f/i) {
215 if ( $user[6] ne DEFAULT_SHELL ) {
216 $$store{$u}{shell} = $user[6] unless $user[6] eq FROZEN_SHELL;
217 }
218
219
220 if ( -e "$user[5]/.loginshell" ) {
221 if (!DRY) {
222 unlink "$user[5]/.loginshell" or warn "unlink: $!"
223 } else {
224 warn qq{unlink $user[5]/.loginshell\n};
225 }
226 }
227
228 if (!DRY) {
229 symlink FROZEN_SHELL, "$user[5]/.loginshell"
230 or warn "symlink: $!";
231 } else {
232 warn qq{symlink FROZEN_SHELL, "$user[5]/.loginshell"\n}
233 }
234
235 push @{ $$store{$u}{modules} }, 'login';
236
237 if ( -x "/usr/sbin/nscd" ) { system("sudo /usr/sbin/nscd -i passwd") };
238 }
239
240 elsif ($a =~ /^u/i) {
241 if ( $$store{$u}{shell}) {
242 if ( -l "$user[5]/.loginshell" or -e "$user[5]/.loginshell" ) {
243 if (!DRY) {
244 system("rm '$user[5]/.loginshell'");
245 } else {
246 warn qq{system("rm '$user[5]/.loginshell'")\n};
247 }
248 }
249 if (!DRY) {
250 symlink($$store{$u}{shell}, "$user[5]/.loginshell")
251 or warn "symlink: $!";
252 } else {
253 warn qq|symlink($$store{$u}{shell}, "$user[5]/.loginshell")\n|;
254 }
255 }
256
257 @{ $$store{$u}{modules} } = grep {!/^login$/} @{ $$store{$u}{modules} };
258
259 if ( -x "/usr/sbin/nscd" ) { system("sudo /usr/sbin/nscd -i passwd") };
260 }
261 }
262
263
264 sub domtool {
265 my ($a, $u) = @_;
266
267 # XXX handle all types of domtool privs, not just domains
268 # XXX how to restart services after that?
269
270 if ($a =~ /^f/i) {
271 my $domains = `domtool-admin perms $u | grep '^domain: '`;
272 chomp $domains;
273 my @domains = split / +/, $domains;
274
275 for (@domains) {
276 push @{ $$store{$u}{domains} }, $_;
277
278 # As per adamc's suggestion, I should not be
279 # running rmdom explicitly.
280 # https://bugzilla.hcoop.net/show_bug.cgi?id=555
281 #if (!DRY) {
282 # system("domtool-admin rmdom $_")
283 #} else {
284 # warn qq|system("domtool-admin rmdom $_")\n|
285 #}
286 }
287
288 if (!DRY) {
289 system("domtool-rmuser $u")
290 } else {
291 warn qq|system("domtool-rmuser $u")\n|
292 }
293
294 push @{ $$store{$u}{modules} }, 'domtool';
295 }
296
297 elsif ($a =~ /^u/i) {
298 if (!DRY) {
299 system("domtool-adduser $u")
300 } else {
301 warn qq|system("domtool-adduser $u")\n|
302 }
303
304 for ( @{ $$store{$u}{domains} } ) {
305 if (!DRY) {
306 system("domtool-admin grant $u domain $_")
307 } else {
308 warn qq|system("domtool-admin grant $u domain $_")\n|
309 }
310 }
311
312 @{ $$store{$u}{modules} } = grep {!/^domtool$/} @{ $$store{$u}{modules} };
313 }
314 }
315
316
317
318 sub cron {
319 my ($a, $u) = @_;
320
321 if ($a =~ /^f/i) {
322 for ( PUBLIC_ACCESS ) {
323 if ( qx{ssh -K $_ grep -E '^$u\$' /etc/cron.allow }) {
324 push @{ $$store{$u}{cron} }, $_;
325
326 if (!DRY) {
327 qx{ssh -K $_ perl -ni -e 'print unless /^\$/' /etc/cron.allow }
328 } else {
329 warn qq{ssh -K $_ perl -ni -e 'print unless /^\$/' /etc/cron.allow\n}
330 }
331 }
332 }
333
334 push @{ $$store{$u}{modules} }, 'cron';
335 }
336
337 elsif ($a =~ /^u/i) {
338 for ( @{ $$store{$u}{cron} } ) {
339 if (!DRY) {
340 qx{ssh -K $_ sh -c 'echo $u >> /etc/cron.allow'};
341 } else {
342 warn qq{ssh -K $_ sh -c 'echo $u >> /etc/cron.allow'\n};
343 }
344 }
345
346 @{ $$store{$u}{modules} } = grep {!/^cron$/} @{ $$store{$u}{modules} };
347 }
348 }
349
350
351 sub slay {
352 my ($a, $u) = @_;
353
354 if ($a =~ /^f/i) {
355 for ( PUBLIC_ACCESS ) {
356 if (!DRY) {
357 qx{ssh -K $_ sudo slay $u}; sleep 5; qx{ssh -K $_ sudo slay -9 $u};
358 } else {
359 warn qq|ssh -K $_ sudo slay $u; sleep 5; ssh -K $_ sudo slay -9 $u\n|
360 }
361 }
362
363 push @{ $$store{$u}{modules} }, 'slay';
364 }
365
366 elsif ($a =~ /^f/i) {
367 @{ $$store{$u}{modules} } = grep {!/^slay$/} @{ $$store{$u}{modules} };
368 }
369 }
370
371