#!/usr/bin/perl # # Copyright (C) 2006 Mandriva # # Author: Florent Villard # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # run commands which needs root privilege # use strict; my $program_name = 'iurt_root_command'; use Mkcd::Commandline qw(parseCommandLine usage); use MDK::Common qw(any if_); use File::Copy qw(copy); use Iurt::Util qw(plog_init plog); use Cwd 'realpath'; use File::Path qw(make_path); use File::Slurp; use String::Escape; use File::Basename; my $arg = @ARGV; my (@params, %run); $run{program_name} = $program_name; my %authorized_modules; my %authorized_rw_bindmounts = (map { $_ => 1 } qw(/proc /dev/pts /var/cache/icecream)); $run{todo} = []; @params = ( # [ "one letter option", "long name option", "number of args (-X means ´at least X´)", "help text", "function to call", "log info"] # [ "", $program_name, 0, "[--verbose ] [--modprobe ] [--mkdir [--parents] [--mode ] ... ]", "$program_name is a perl script to execute commands which need root privilege, it helps probram which needs occasional root privileges for some commands.", sub { $arg or usage($program_name, \@params) }, String::Escape::elide(join(' ', "Running $program_name", @ARGV), 120) ], [ "", "cp", [ ["", "cp", -1, "[-r] ... ", "copy the files to dest", sub { my ($tmp, @arg) = @_; $tmp->[0] ||= {}; push @$tmp, @arg; 1; }, "Setting cp command arguments"], ["r", "recursive", 0, "", "Also copy directories and subdirectories", sub { my ($tmp) = @_; $tmp->[0]{recursive} = 1; 1 }, "Set the recursive flag"], ], "[-r] ... ", "Copy files", \&cp, "Copying files" ], [ "", "ln", [ ["", "ln", 2, " ", "link file1 to file2", sub { my ($tmp, @arg) = @_; $tmp->[0] ||= {}; push @$tmp, @arg; 1; }, "Setting ln command arguments"], ], " ", "Link files", \&ln, "Linking files" ], [ "", "mkdir", [ ["", "mkdir", -1, "[--parents] [--mode ] ... ", "mkdir create the given path", sub { my ($tmp, @arg) = @_; $tmp->[0] ||= {}; push @$tmp, @arg; 1; }, "Setting auto mode arguments"], ["p", "parents", 0, "", "Also create needed parents directories", sub { my ($tmp) = @_; $tmp->[0]{parents} = 1; 1 }, "Set the parents flag"], ["m", "mode", 1, "", "Set the given mode on created directories", sub { my ($tmp, $arg) = @_; $tmp->[0]{mode} = $arg; 1 }, "Set the mode flag"], ], "[--parents] [--mode ] ... ", "mkdir create the given path", \&mkdir, "Creating the path" ], [ "", "rm", [ ["", "rm", -1, "[-r] ... ", "remove the provided files", sub { my ($tmp, @arg) = @_; $tmp->[0] ||= {}; push @$tmp, @arg; 1; }, "Setting rm command arguments"], ["r", "recursive", 0, "", "Also create needed parents directories", sub { my ($tmp) = @_; $tmp->[0]{recursive} = 1; 1 }, "Set the recursive flag"], ], "[-r] ... ", "Remove files", \&rm, "Removing files" ], [ "v", "verbose", 1, "", "modprobe try to modprobe the given module if authorized.", sub { $run{verbose} = $_[0]; 1 }, "Setting verbose level" ], [ "", "modprobe", 1, "", "modprobe try to modprobe the given module if authorized.", \&modprobe, "Modprobing" ], [ "", "tar", [ ["", "tar", 2, " ", "tar directory into file", sub { my ($tmp, @arg) = @_; $tmp->[0] ||= {}; push @$tmp, @arg; 1; }, "Setting tar command arguments"], ], " ", "Create tarball", \&tar, "Create tarball" ], [ "", "untar", [ ["", "untar", -1, " [files]", "untar file into directory (optionally selecting files only)", sub { my ($tmp, @arg) = @_; $tmp->[0] ||= {}; push @$tmp, @arg; 1; }, "Setting untar command arguments"], ], " [files]", "Uncompress tarball", \&untar, "Uncompress tarball" ], [ "", "bindmount", 2, " ", "bind mount source on dest", \&bindmount, "Bind mounting" ], [ "", "tmpfs", 1, "", "mount a tmpfs on the given directory", \&tmpfs, "Mount tmpfs" ], [ "", "umount", 1, "", "umount the given directory", \&umount, "Unmounting" ], [ "", "btrfs_create", 1, "", "create a btrfs subvolume", \&btrfs_create, "Create btrfs" ], [ "", "btrfs_delete", 1, "", "delete a btrfs subvolume", \&btrfs_delete, "Delete btrfs" ], [ "", "btrfs_snapshot", 2, " ", "create a btrfs snapshot", \&btrfs_snapshot, "btrfs snapshot" ], [ "", "netns_create", 1, "", "create a network namespace", \&netns_create, "Create network namespace for given chroot" ], [ "", "netns_delete", 1, "", "delete a network namespace", \&netns_delete, "Delete network namespace for given chroot, killing all processes" ], [ "", "useradd", 3, " [uid]", "Add user in given chroot", \&useradd, "Useradd" ], [ "", "urpmi", [ ["", "urpmi", -1, "urpmi options", "run urpmi with urpmi options (should have chroot options)", sub { my ($tmp, @arg) = @_; push @$tmp, @arg; 1; }, "Setting urpmi options"], ], "urpmi options", "Run urpmi in chroot", \&urpmi, "Run urpmi in chroot" ], ); my %commands_with_more_args = ( "--urpmi" => [ \&urpmi, "urpmi" ], "--urpmi-addmedia" => [ \&urpmi_addmedia, "urpmi.addmedia" ], "--chroot" => [ \&run_chroot, "chroot" ], ); open(my $LOG, ">&STDERR"); plog_init($program_name, $LOG, $run{verbose}); #plog_init($program_name, $LOG, 7, 1); my $todo; # (blino) do not use mkcd to parse complicated command lines with unhandled options # I fail to make mkcd not parse arguments after -- on command line if (my $command = $commands_with_more_args{$ARGV[0]}) { my (undef, @options) = @ARGV; $todo = [ [ $command->[0], \@options, $command->[1] ] ]; } else { $todo = parseCommandLine($program_name, \@ARGV, \@params); @ARGV and usage($program_name, \@params, "@ARGV, too many arguments"); } my $ok = 1; foreach my $t (@$todo) { plog('DEBUG', $t->[2]); my $ok2 = &{$t->[0]}(\%run, @{$t->[1]}); $ok2 or plog("ERROR: $t->[2]"); $ok &&= $ok2; } plog('DEBUG', "Success!") if $ok; exit !$ok; sub modprobe { my ($_run, $module) = @_; if (!$authorized_modules{$module}) { plog("ERROR: unauthorized module $module"); return 0; } return 1 if any { /^$module\b/ } read_file("/proc/modules"); system("/sbin/depmod", "-a"); !system("/sbin/modprobe", "-f", $module); } sub mkdir { my ($_run, $opt, @dir) = @_; foreach my $path (@dir) { -d $path and next; if ($path =~ m,/dev|/proc|/root|/var, && $path !~ /chroot/) { plog('FAIL', "ERROR: $path creation forbidden"); } if ($opt->{parents}) { make_path($path); } else { mkdir $path; } if ($opt->{mode}) { chmod $opt->{mode}, $path; } } 1; } sub rm { my ($_run, $opt, @files) = @_; my $ok = 1; my $done; foreach my $f (@files) { if (-d $f) { if (!$opt->{recursive}) { plog('WARN', "can't remove directories without the -r option"); $ok = 0; } else { if (!check_path_authorized($f)) { plog('FAIL', "removal of $f forbidden"); $ok = 0; } else { system('rm', '-rf', $f); plog('DEBUG', "removing $f"); $done = 1; } } } else { if (!check_path_authorized($f)) { plog("removal of $f forbidden"); $ok = 0; } else { # CM: The original regexp was /\*?/, which doesn't seem to be # what we want. Check if we can always glob instead of # testing, or if glob expansion is needed at all if ($f =~ /[*?]/) { foreach my $file (glob $f) { if (!check_path_authorized($f)) { plog('FAIL', "removal of $f forbidden"); $ok = 0; } else { unlink $file; $done = 1; plog('DEBUG', "removing $file"); } } } else { unlink $f; $done = 1; plog('DEBUG', "removing $f"); } } } } if (!$done) { plog('DEBUG', "nothing deleted") } $ok; } sub cp { my ($_run, $opt, @files) = @_; my $ok = 1; my $done; my $dest = pop @files; check_path_authorized($dest) or return; foreach my $f (@files) { if (-d $f) { if (!$opt->{recursive}) { plog('WARN', "can't copy directories without the -r option"); $ok = 0; } else { system('cp', '-raf', $f); plog('DEBUG', "copying $f -> $dest"); $done = 1; } } else { if ($f =~ /\*?/) { foreach my $file (glob $f) { if (copy $file, $dest) { $done = 1; plog('DEBUG', "copying $file -> $dest"); } else { $ok = 0; plog('FAIL', "copying $file to $dest failed ($!)"); } } } else { if (copy $f, $dest) { $done = 1; plog('DEBUG', "copying $f -> $dest"); } else { $ok = 0; plog('FAIL', "copying $f to $dest failed ($!)"); } } } } if (!$done) { plog('DEBUG', "nothing copied") } $ok; } sub ln { my ($_run, $_opt, $file1, $file2) = @_; check_path_authorized($file1) && check_path_authorized($file2) or return; link $file1, $file2; } sub check_path_authorized { my ($path) = @_; if (!$ENV{SUDO_USER}) { plog('FAIL', "must be run from sudo"); return; } my $authorized = (getpwnam($ENV{SUDO_USER}))[7]; if (!$authorized) { plog('FAIL', "can't find home for $ENV{SUDO_USER}"); return; } #- check authorization for canonicalized path (with .. and symlinks resolved) my $realpath = realpath($path); if ($realpath !~ /^\Q$authorized\E/) { plog('FAIL', "$path forbidden"); return; } 1; } sub tar { my ($_run, $_opt, $file, $dir) = @_; if (!$file || !$dir) { plog('FAIL', "tar: missing arguments"); return; } check_path_authorized($file) && check_path_authorized($dir) or return; return !system('tar', 'caf', $file, '-C', $dir, '.'); } sub untar { my ($_run, $_opt, $file, $dir, @o_files) = @_; if (!$file || !$dir) { plog('FAIL', "untar: missing arguments"); return; } check_path_authorized($file) && check_path_authorized($dir) or return; if (any { /^-/ } @o_files) { plog('FAIL', "untar: options forbidden"); return; } make_path($dir); return !system('tar', 'xf', $file, '-C', $dir, @o_files); } sub btrfs_create { my ($_run, $dest) = @_; check_path_authorized($dest) or return; return !system("btrfs", "subvolume", "create", $dest); } sub btrfs_delete { my ($_run, $dest) = @_; check_path_authorized($dest) or return; return !system("btrfs", "subvolume", "delete", $dest); } sub btrfs_snapshot { my ($_run, $source, $dest) = @_; check_path_authorized($dest) or return; return !system("btrfs", "subvolume", "snapshot", $source, $dest); } sub netns_create { my ($_run, $dest) = @_; check_path_authorized($dest) or return; my $nsname = basename($dest); system("ip", "netns", "add", $nsname) and return; system("ip", "netns", "exec", $nsname, "ifconfig", "lo", "up"); # We don't configure a DNS server so make sure one is not excepted system('sed', '-i', 's|^hosts:.*|hosts: files|', "$dest/etc/nsswitch.conf"); # Some packages want a default route system("ip", "netns", "exec", $nsname, "ip", "route", "add", "0.0.0.0/0", "dev", "lo"); my $hostname = `hostname`; system("echo '127.0.0.1 localhost $hostname' > $dest/etc/hosts"); system("echo > $dest/etc/resolv.conf"); return 1; } sub netns_delete { my ($_run, $dest) = @_; check_path_authorized($dest) or return; my $nsname = basename($dest); system("ip netns pids $nsname | xargs -r kill -9"); return !system("ip", "netns", "del", $nsname); } sub bindmount { my ($_run, $source, $dest) = @_; check_path_authorized($dest) or return; system("mount", "--bind", $source, $dest) == 0 or return; if (!$authorized_rw_bindmounts{$source}) { system("mount", "-o", "remount,ro", $dest) == 0 or return; } return 1; } sub tmpfs { my ($_run, $dir) = @_; check_path_authorized($dir) or return; return !system("mount", "none", "-t", "tmpfs", $dir); } sub umount { my ($_run, $dir) = @_; check_path_authorized($dir) or return; return !system("umount", $dir); } sub useradd { my ($_run, $dir, $username, $o_uid) = @_; check_path_authorized($dir) or return; return system('chroot', $dir, 'useradd', if_($o_uid, '-o', '--uid', $o_uid), $username) == 0 || system('chroot', $dir, 'id', $username) == 0; } sub check_urpmi_chroot_options { my ($options) = @_; # get all --something options my %optvals; my $current_opt; foreach (@$options) { if (/^--/) { $current_opt = $_; } else { if ($current_opt) { $optvals{$current_opt} = $_; } undef $current_opt; } } # check that urpmi is rooted and using allowed chroot paths my $is_rooted = 0; foreach (qw(--root --urpmi-root)) { if ($optvals{$_}) { check_path_authorized($optvals{$_}) or return; $is_rooted = 1; } } if (!$is_rooted) { plog('FAIL', "urpmi commands must be rooted"); return; } return 1; } sub urpmi { my ($_run, @options) = @_; return check_urpmi_chroot_options(\@options) && !system('urpmi', @options); } sub urpmi_addmedia { my ($_run, @options) = @_; return check_urpmi_chroot_options(\@options) && !system('urpmi.addmedia', @options); } sub run_chroot { my ($_run, $dir, @options) = @_; if (!$dir) { plog('FAIL', "chroot: need directory argument"); return; } check_path_authorized($dir) or return; my $nsname = basename($dir); if (!system("ip netns list | grep -q '^$nsname\$'")) { return !system("ip", "netns", "exec", $nsname, "chroot", $dir, @options); } else { return !system("chroot", $dir, @options); } }