Files
2026-07-13 13:12:17 +08:00

181 lines
5.2 KiB
Perl

#!/usr/bin/perl
use strict;
use warnings;
use Test::More;
use Cwd qw(abs_path);
use File::Basename qw(dirname);
use File::Path qw(make_path);
use File::Temp qw(tempdir);
my $bindir = dirname(__FILE__);
my $rootdir = abs_path("$bindir/../..") or die "rootdir: $!";
my $confdir = tempdir(CLEANUP => 1);
my $vardir = tempdir(CLEANUP => 1);
my $remote_user = getpwuid($<) || 'root';
sub write_text
{
my ($file, $text) = @_;
open(my $fh, ">", $file) or die "write $file: $!";
print $fh $text;
close($fh) or die "close $file: $!";
}
write_text("$confdir/config", "os_type=generic-linux\nos_version=0\n");
write_text("$confdir/var-path", "$vardir\n");
write_text("$confdir/miniserv.conf",
"root=$rootdir\nuserfile=$confdir/miniserv.users\n");
write_text("$confdir/miniserv.users", "");
make_path("$confdir/useradmin");
$ENV{'WEBMIN_CONFIG'} = $confdir;
$ENV{'WEBMIN_VAR'} = $vardir;
$ENV{'MINISERV_CONFIG'} = "$confdir/miniserv.conf";
$ENV{'FOREIGN_MODULE_NAME'} = 'useradmin';
$ENV{'FOREIGN_ROOT_DIRECTORY'} = $rootdir;
$ENV{'REMOTE_USER'} = $remote_user;
$ENV{'BASE_REMOTE_USER'} = $remote_user;
chdir("$rootdir/useradmin") or die "chdir useradmin: $!";
require "$rootdir/useradmin/user-lib.pl";
our %access;
sub clear_acl_cache
{
no warnings 'once';
undef(%main::read_file_cache);
undef(%main::read_file_missing);
undef(%main::read_file_cache_time);
}
sub write_global_acl
{
write_acl_for($remote_user, @_);
}
sub write_acl_for
{
my ($user, %opts) = @_;
my $text = "";
foreach my $k (sort keys %opts) {
$text .= "$k=$opts{$k}\n";
}
write_text("$confdir/$user.acl", $text);
clear_acl_cache();
}
my $tmp = tempdir(CLEANUP => 1);
my $allowed = "$tmp/allowed";
my $other = "$tmp/other";
my $outside = "$tmp/outside";
make_path($allowed, $other, $outside);
write_text("$allowed/batch.txt", "batch\n");
write_text("$other/also-batch.txt", "other\n");
write_text("$outside/secret.txt", "secret\n");
write_global_acl(
root => $allowed,
otherdirs => $other,
fileunix => $remote_user,
);
subtest 'local batch file path ACLs' => sub {
%access = ( 'batchdir' => $allowed );
ok(can_read_batch_local_file("$allowed/batch.txt"),
'file under batchdir and global root is allowed');
ok(!can_read_batch_local_file("$other/also-batch.txt"),
'global otherdirs alone cannot bypass narrower batchdir');
ok(!can_read_batch_local_file("$outside/secret.txt"),
'outside file is rejected');
ok(!can_read_batch_local_file("relative/batch.txt"),
'relative paths are rejected');
ok(!can_read_batch_local_file(undef),
'missing local file path is rejected');
%access = ( 'batchdir' => "" );
ok(!can_read_batch_local_file("$allowed/batch.txt"),
'blank batchdir denies local server files');
%access = ( );
ok(can_read_batch_local_file("$allowed/batch.txt"),
'missing batchdir falls back to / for old ACL compatibility');
%access = ( 'batchdir' => $tmp );
ok(can_read_batch_local_file("$other/also-batch.txt"),
'batchdir plus global otherdirs allows second configured tree');
};
SKIP: {
skip('symlinks are unavailable', 1)
if !symlink("$outside/secret.txt", "$allowed/link.txt");
%access = ( 'batchdir' => $allowed );
ok(!can_read_batch_local_file("$allowed/link.txt"),
'symlink escaping batchdir/global root is rejected');
}
%access = ( 'batchdir' => $allowed );
is(read_batch_local_file("$allowed/batch.txt"), "batch\n",
'allowed batch file is read');
subtest 'global ACL file read and copy helpers' => sub {
my $copy = "$tmp/copied-batch.txt";
ok(copy_file_under_global_acl("$allowed/batch.txt", $copy),
'allowed file can be copied through global ACL helper');
is(read_file_contents($copy), "batch\n",
'copied file has expected contents');
my $modecopy = "$tmp/mode-preserving-copy.txt";
write_text($modecopy, "old\n");
chmod(0644, "$allowed/batch.txt");
chmod(0600, $modecopy);
ok(copy_source_dest("$allowed/batch.txt", $modecopy, 1, 1),
'contents-only copy succeeds');
is((stat($modecopy))[2] & 0777, 0600,
'contents-only copy retains destination permissions');
my $blocked = "$tmp/blocked-copy.txt";
ok(!copy_file_under_global_acl("$outside/secret.txt", $blocked),
'outside file cannot be copied through global ACL helper');
ok(!-e $blocked, 'blocked copy does not create destination');
};
subtest 'global ACL home fallback for Webmin-only users' => sub {
my $webmin_only = "webmin-batch-no-such-user-$$";
no warnings 'once';
local $main::remote_user = $webmin_only;
local $main::base_remote_user = $webmin_only;
local $WebminCore::remote_user = $webmin_only;
local $WebminCore::base_remote_user = $webmin_only;
write_acl_for(
$webmin_only,
root => "",
otherdirs => "",
fileunix => "nobody",
);
%access = ( 'batchdir' => "/" );
ok(!can_read_batch_local_file("$outside/secret.txt"),
'missing Unix home does not fall back to filesystem root');
write_acl_for(
$webmin_only,
root => "~/allowed",
otherdirs => "",
fileunix => "nobody",
);
ok(!can_read_batch_local_file("/allowed/batch.txt"),
'home-relative root does not become an absolute path without a home');
write_acl_for(
$webmin_only,
root => "",
otherdirs => $allowed,
fileunix => "nobody",
);
ok(can_read_batch_local_file("$allowed/batch.txt"),
'explicit otherdirs still permits configured paths');
};
done_testing();