| 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 | |