ports/infrastructure/lib/DPB/User.pm

315 lines
6.2 KiB
Perl

# ex:ts=8 sw=4:
# $OpenBSD: User.pm,v 1.27 2023/05/06 05:20:31 espie Exp $
#
# Copyright (c) 2010-2019 Marc Espie <espie@openbsd.org>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
use v5.36;
# handling user personalities
# note that all the "running around" starts with dpb
# having a saved uid of 0, so we can switch back to root
# in order to change personality
# the main class is a user that can be used for various operations
package DPB::User;
use Fcntl;
sub from_uid($class, $u, $g = undef)
{
if (my ($l, undef, $uid, $gid) = getpwuid $u) {
my $groups = `/usr/bin/id -G $u`;
chomp $groups;
if (defined $g) {
$gid = $g;
}
my $group = getgrgid($gid);
return bless { user => $l, uid => $uid,
group => $group, gid => $gid,
grouplist => "$gid $groups" }, $class;
} else {
return undef;
}
}
sub new($class, $u)
{
# local users are used to do operations
# otherwise, distant users are "just" a name for the distant
# exec stuff
if (my ($l, undef, $uid, $gid) = getpwnam $u) {
# XXX getgrouplist(3) is bsd specific. This happens
# seldom enough that we can delegate
my $groups = `/usr/bin/id -G $u`;
chomp $groups;
my $group = getgrgid($gid);
return bless { user => $l, uid => $uid,
group => $group, gid => $gid,
grouplist => "$gid $groups" }, $class;
} else {
return bless { user => $u}, $class;
}
}
sub user($self)
{
return $self->{user};
}
sub run_as($self, $code)
{
local $> = 0;
local $) = $self->{grouplist};
{
local $> = $self->{uid};
return &$code();
}
}
sub enforce_local($self)
{
if (!defined $self->{uid}) {
print STDERR "User $self->{user} does not exist locally\n";
exit 1;
} else {
return $self;
}
}
sub _make_path($self, @directories)
{
my $p = pop @directories;
if ($p->{mode}) {
my $m = umask(0);
File::Path::make_path(@directories, $p);
umask($m);
} else {
File::Path::make_path(@directories, $p);
}
}
sub make_path($self, @directories)
{
require File::Path;
my $p = {};
if ($self->{dirmode}) {
$p->{mode} = $self->{dirmode};
}
if ($self->{droppriv}) {
$self->run_as(
sub() {
$self->_make_path(@directories, $p);
});
} else {
if ($self->{uid}) {
$p->{uid} = $self->{uid};
} else {
$p->{owner} = $self->{user};
}
if ($self->{gid}) {
$p->{group} = $self->{gid};
}
local ($>, $)) = (0, 0);
$self->_make_path(@directories, $p);
}
}
sub open($self, $mode, @parms)
{
return $self->run_as(
sub() {
# XXX don't try to read directories,
# there's opendir for that.
if (-d $parms[0]) {
require Errno;
$! = Errno::EISDIR();
return undef;
}
if (open(my $fh, $mode, @parms)) {
my $flags = fcntl($fh, F_GETFL, 0);
fcntl($fh, F_SETFL, $flags | FD_CLOEXEC);
return $fh;
} else {
return undef;
}
});
}
sub opendir($self, $dirname)
{
return $self->run_as(
sub() {
if (opendir(my $fh, $dirname)) {
return $fh;
} else {
return undef;
}
});
}
sub unlink($self, @links)
{
return $self->run_as(
sub() {
unlink(@links);
});
}
sub link($self, $a, $b)
{
return $self->run_as(
sub() {
link($a, $b);
});
}
sub rename($self, $o, $n)
{
return $self->run_as(
sub() {
rename($o, $n);
});
}
sub stat($self, $name)
{
return $self->run_as(
sub() {
return stat $name;
});
}
sub rewrite_file($self, $state, $filename, $sub)
{
$self->make_path(File::Basename::dirname($filename));
$self->run_as(
sub() {
my $f;
if (!CORE::open $f, '>', "$filename.part") {
$state->fatal("#1 can't write #2: #3",
$self->user, "$filename.part", $!);
}
if (!&$sub($f) || !close $f) {
$state->fatal("#1 can't write data to #2: #3",
$self->user, "$filename.part", $!);
}
CORE::rename "$filename.part", $filename or
$state->fatal("#1 can't rename #2 to #3: #4",
$self->user, "$filename.part", $filename, $!);
});
}
# this is the class we can inherit from
# the derived class is responsible for implementing ->user
# (if the default ->{user} isn't enough)
# to get the actual user object (we encapsulate)
# then we delegate most of the actual operations to user
package DPB::UserProxy;
sub run_as($self, $code)
{
$self->user->run_as($code);
}
sub make_path($self, @dirs)
{
$self->user->make_path(@dirs);
}
sub open($self, @parms)
{
return $self->user->open(@parms);
}
sub file($self, $filename)
{
return DPB::UserFile->new($self, $filename);
}
sub opendir($self, $dirname)
{
return $self->user->opendir($dirname);
}
sub unlink($self, @links)
{
return $self->user->unlink(@links);
}
sub link($self, $a, $b)
{
return $self->user->link($a, $b);
}
sub rename($self, @parms)
{
return $self->user->rename(@parms);
}
sub stat($self, $name)
{
return $self->user->stat($name);
}
sub user($self)
{
return $self->{user};
}
sub write_error($self, $name)
{
DPB::Util->die_bang($self->user->user." can't write to $name");
}
sub redirect($self, $log)
{
$self->user->run_as(
sub() {
close STDOUT;
CORE::open STDOUT, '>>', $log or DPB::Util->die_bang(
$self->user->user." can't write to $log");
close STDERR;
CORE::open STDERR, '>&STDOUT' or
DPB::Util->die_bang("bad redirect");
});
}
# since we don't want to keep too many open files, encapsulate
# filename + file
package DPB::UserFile;
# can't inherit from UserProxy, open/stat have different calling mechanisms
sub new($class, $user, $filename)
{
bless {filename => $filename, user => $user}, $class;
}
sub name($self)
{
return $self->{filename};
}
sub open($self, $mode)
{
return $self->{user}->open($mode, $self->name);
}
sub stat($self)
{
return $self->{user}->stat($self->name);
}
1;