#!/usr/bin/perl # # Purpose: freeze user (cancel user services except email), or unfreeze user. # # Usage (RUN AS _ADMIN USER ON FRITZ WITHOUT SUDO ... but it shouldn't matter where you run it): # # Display frozen users or details for one user (one user implies -verbose): # freeze [user], OR # freeze [ --action list | -a ] [--verbose | -v] [user] # # Freeze user: # freeze -a f user # # Unfreeze: # freeze -a u user # # # How it works: # # Script invokes a list of modules, where each module knows how to # implement and unimplement a specific restriction. Implemented modules # should be listed in @modules array or they won't get called. Admin # can override list of modules with --modules=one,two,three. # # Modules execute in the order as specified for freeze, and in reverse # order for unfreeze. # # Module gets called as &module($action, $user) . The proper way to # test for which action is requested is as shown below. (Note that # module adds or removes itself to the list of modules that ran on the user). # # if ($a =~ /^f/i) { # ... freeze ... # push @{ $$store{$u}{modules} }, 'MODULE'; # } elsif ($a =~ /^u/i) { # ... unfreeze ... # @{ $$store{$u}{modules} } = grep {!/^MODULE$/} @{ $$store{$u}{modules} }; # } # # Also each system-modifying action should be wrapped in if (!DRY) as shown: # # if (!DRY) { # system(qq{SOME COMMAND}) # } else { # warn qq|SOME COMMAND| # } # # User is valid system username, and user's getent entry is prepared and # retrievable through @user array, should you need some of its info. # # Module can save all persistent data to $$store{$user}{$modulename}. For # example, after cron module removes user from all cron.allows, it # registers the machines where user was removed to # @{ $$store{$u}{cron} }, so that it can revert it back if user is # unfreezed. # # Module 'record' creates or deletes initial user entry in $$store. # If you create a new module that will use the store, announce its # hash key by creating it empty in record(). # # For additional detail, here's how the stored hash might look like: # # # $store = { # user1 => { # date => 'Sun Jun 29 18:45:43 CEST 2008', # getent => [qw/docelic 1000 1000 DavorOcelic /home/docelic /bin/bash] # # modules => [qw/login domtool cron slay/], # (modules that ran) # domains => [qw/spinlock.hr test.hr/], # (domains that were removed) # cron => [qw/mire/], # (hosts where cron.allow entry was removed) # }, # user2 => { # ... # }, # ... # ... # ... # } # # # Wiki page relating to this script is http://wiki.hcoop.net/MemberFreezing # # Davor Ocelic, docelic@hcoop.net, Sun Jun 29 18:41:02 CEST 2008 # # use warnings; use strict; use Storable qw/lock_nstore lock_retrieve/; use Getopt::Long qw/GetOptions/; use constant DEBUG => 1; use constant DRY => 0; use constant STORE => "/afs/hcoop.net/common/etc/frozen/cache"; use constant DEFAULT_SHELL => '/bin/bash'; use constant FROZEN_SHELL => '/afs/hcoop.net/common/etc/scripts/frozen_shell'; use constant PUBLIC_ACCESS => (qw/mire/); use constant RUN_SERVER => 'fritz'; my $store = {}; # cached info my $action = 'list'; # list, freeze, unfreeze my $verbose = 0; # 0/1 (print which modules were applied to user in freezing) my $force = 0; # 0/1 (force action even if user already freezed/unfreezed) my $user; # who to freeze/unfreeze my @user; # getent passwd entry for user my $modules; # list of freeze/unfreeze actions to take my @modules; # modules, possibly overriden by split /,\s+/, $modules # Keep modules listed in order of application, honoring possible dependencies. @modules = (qw/record login domtool slay/); unless ( GetOptions ( 'do|d|a=s' => \$action, 'verbose|v!' => \$verbose, 'force!' => \$force, 'modules|m=s' => \$modules, )) { die "Error parsing options: $!\n" } $user = shift ; if ( $> == 0 or $< == 0 ) { die "Run script under admin account without sudo.\n"; } if ( -e STORE ) { $store = lock_retrieve(STORE); } else { warn "No '" . STORE . "', skipping load.\n"; } if ( `hostname` ne RUN_SERVER . "\n" ) { die "Please run script on " . RUN_SERVER . "\n"; } if ( $action =~ /^l/i ) { while (my ($k,$v) = each %$store ) { if (! $user or $user eq $k ) { print "$k $$v{date}\n"; print " @{$$v{modules}}\n" if $verbose or $user; } } exit 0; } $user or die "Must specify user to freeze/unfreeze. Exiting.\n"; $user =~ /^[a-z0-9]+$/ or die "Invalid username (not [a-z0-9]+).\n"; @user = split(/:/, `getent passwd $user`); @user or die "No such user? (getent passwd USER empty.)\n"; chomp $user[$#user]; if ( $action !~ /^[fu]/i ) { warn "Unknown action: use -a [list (l)|freeze (f)|unfreeze (u)]\n"; } if ( $action =~ /^f/i ) { if ( exists $$store{$user} ) { warn "User already frozen since $$store{$user}{date}.\n"; if (! $force) { die "Exiting.\n"; } } } elsif ( $action =~ /^u/i ) { if (! exists $$store{$user} ) { warn "User not frozen in the first place.\n"; if (! $force) { die "Exiting.\n"; } } } else { warn "How did you get through?\n"; die; } if ($modules) { @modules = split /[,\s+]/, $modules; } for ( $action =~ /^u/i ? reverse @modules : @modules ) { no strict 'refs'; print "Module: $_\n"; &{ $_ }($action, $user); } lock_nstore $store, STORE; ########################################################################### # Helpers below # GETENT (available to modules automatically in @user): # 0 1 2 3 4 5 6 # docelic:x:10235:65534:docelic:/afs/hcoop.net/user/d/do/docelic:/bin/bash sub record { my ($a, $u) = @_; $a =~ /^f/i and $$store{$u} = { date => scalar localtime, getent => [ @user ], modules => [], domains => [], cron => [], }; $a =~ /^u/i and delete $$store{$u}; } sub login { my ($a, $u) = @_; if ($a =~ /^f/i) { if ( $user[6] ne DEFAULT_SHELL ) { $$store{$u}{shell} = $user[6] unless $user[6] eq FROZEN_SHELL; } if ( -e "$user[5]/.loginshell" ) { if (!DRY) { unlink "$user[5]/.loginshell" or warn "unlink: $!" } else { warn qq{unlink $user[5]/.loginshell\n}; } } if (!DRY) { symlink FROZEN_SHELL, "$user[5]/.loginshell" or warn "symlink: $!"; } else { warn qq{symlink FROZEN_SHELL, "$user[5]/.loginshell"\n} } push @{ $$store{$u}{modules} }, 'login'; if ( -x "/usr/sbin/nscd" ) { system("sudo /usr/sbin/nscd -i passwd") }; } elsif ($a =~ /^u/i) { if ( -l "$user[5]/.loginshell" or -e "$user[5]/.loginshell" ) { if (!DRY) { system("rm '$user[5]/.loginshell'"); } else { warn qq{system("rm '$user[5]/.loginshell'")\n}; } } if ( $$store{$u}{shell}) { if (!DRY) { symlink($$store{$u}{shell}, "$user[5]/.loginshell") or warn "symlink: $!"; } else { warn qq|symlink($$store{$u}{shell}, "$user[5]/.loginshell")\n|; } } @{ $$store{$u}{modules} } = grep {!/^login$/} @{ $$store{$u}{modules} }; if ( -x "/usr/sbin/nscd" ) { system("sudo /usr/sbin/nscd -i passwd") }; } } sub domtool { my ($a, $u) = @_; # XXX handle all types of domtool privs, not just domains # XXX how to restart services after that? if ($a =~ /^f/i) { my $domains = `domtool-admin perms $u | grep '^domain: '`; chomp $domains; my @domains = split / +/, $domains; for (@domains) { push @{ $$store{$u}{domains} }, $_; # As per adamc's suggestion, I should not be # running rmdom explicitly. # https://bugzilla.hcoop.net/show_bug.cgi?id=555 #if (!DRY) { # system("domtool-admin rmdom $_") #} else { # warn qq|system("domtool-admin rmdom $_")\n| #} } if (!DRY) { system("domtool-rmuser $u") } else { warn qq|system("domtool-rmuser $u")\n| } push @{ $$store{$u}{modules} }, 'domtool'; } elsif ($a =~ /^u/i) { if (!DRY) { system("ssh -K deleuze domtool-adduser $u") } else { warn qq|system("ssh -K deleuze domtool-adduser $u")\n| } for ( @{ $$store{$u}{domains} } ) { if (!DRY) { system("domtool-admin grant $u domain $_") } else { warn qq|system("domtool-admin grant $u domain $_")\n| } } @{ $$store{$u}{modules} } = grep {!/^domtool$/} @{ $$store{$u}{modules} }; } } sub cron { my ($a, $u) = @_; if ($a =~ /^f/i) { for ( PUBLIC_ACCESS ) { if ( qx{ssh -K $_ grep -E '^$u\$' /etc/cron.allow }) { push @{ $$store{$u}{cron} }, $_; if (!DRY) { qx{ssh -K $_ perl -ni -e 'print unless /^\$/' /etc/cron.allow } } else { warn qq{ssh -K $_ perl -ni -e 'print unless /^\$/' /etc/cron.allow\n} } } } push @{ $$store{$u}{modules} }, 'cron'; } elsif ($a =~ /^u/i) { for ( @{ $$store{$u}{cron} } ) { if (!DRY) { qx{ssh -K $_ sh -c 'echo $u >> /etc/cron.allow'}; } else { warn qq{ssh -K $_ sh -c 'echo $u >> /etc/cron.allow'\n}; } } @{ $$store{$u}{modules} } = grep {!/^cron$/} @{ $$store{$u}{modules} }; } } sub slay { my ($a, $u) = @_; if ($a =~ /^f/i) { for ( PUBLIC_ACCESS ) { if (!DRY) { qx{ssh -K $_ sudo slay $u}; sleep 5; qx{ssh -K $_ sudo slay -9 $u}; } else { warn qq|ssh -K $_ sudo slay $u; sleep 5; ssh -K $_ sudo slay -9 $u\n| } } push @{ $$store{$u}{modules} }, 'slay'; } elsif ($a =~ /^f/i) { @{ $$store{$u}{modules} } = grep {!/^slay$/} @{ $$store{$u}{modules} }; } }