355675b43f
ResolvedUser now carries home, looked up from the image's own /etc/passwd entry for the final resolved uid (falling back to /root for uid 0 or / otherwise when there's no matching row). bwrap's HOME now uses this whenever a user override applies (--user/--group or an image-declared default user); the plain /root default is kept only when no override applies at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
134 lines
4.6 KiB
C++
134 lines
4.6 KiB
C++
// Copyright (C) 2026 Viorel Munteanu
|
|
//
|
|
// 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 of the License, 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.,
|
|
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
|
|
#include "user_spec.h"
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
|
|
#include <spdlog/spdlog.h>
|
|
|
|
namespace {
|
|
|
|
std::vector<std::string> split(const std::string& line, char delim) {
|
|
std::vector<std::string> fields;
|
|
std::istringstream stream(line);
|
|
std::string field;
|
|
while (std::getline(stream, field, delim)) {
|
|
fields.push_back(field);
|
|
}
|
|
return fields;
|
|
}
|
|
|
|
bool is_all_digits(const std::string& s) {
|
|
return !s.empty() && std::all_of(s.begin(), s.end(), [](unsigned char c) { return std::isdigit(c); });
|
|
}
|
|
|
|
std::optional<int> parse_int(const std::string& s) {
|
|
try {
|
|
return std::stoi(s);
|
|
} catch (const std::exception&) {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
// Looks up `key` by name (field 0) or numeric id (field `id_field`) in a
|
|
// colon-separated database file (/etc/passwd or /etc/group). Malformed lines are
|
|
// skipped rather than treated as errors.
|
|
std::optional<std::vector<std::string>> lookup_entry(const std::filesystem::path& db_file,
|
|
const std::string& key, size_t id_field) {
|
|
std::ifstream in(db_file, std::ios::binary);
|
|
if (!in) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::string line;
|
|
while (std::getline(in, line)) {
|
|
auto fields = split(line, ':');
|
|
if (fields.size() <= id_field) {
|
|
continue;
|
|
}
|
|
if (fields[0] == key || fields[id_field] == key) {
|
|
return fields;
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::optional<ResolvedUser> resolve_user_and_group(const std::string& user,
|
|
const std::optional<std::string>& group,
|
|
const std::filesystem::path& image_root) {
|
|
std::filesystem::path passwd_file = image_root / "etc" / "passwd";
|
|
|
|
int uid = 0;
|
|
int gid = 0;
|
|
|
|
if (is_all_digits(user)) {
|
|
uid = *parse_int(user);
|
|
auto entry = lookup_entry(passwd_file, user, 2);
|
|
if (entry && entry->size() > 3) {
|
|
gid = parse_int((*entry)[3]).value_or(uid);
|
|
} else {
|
|
gid = uid;
|
|
}
|
|
} else {
|
|
auto entry = lookup_entry(passwd_file, user, 2);
|
|
if (!entry || entry->size() <= 3) {
|
|
spdlog::error("could not resolve user '{}' in the image's /etc/passwd", user);
|
|
return std::nullopt;
|
|
}
|
|
auto entry_uid = parse_int((*entry)[2]);
|
|
auto entry_gid = parse_int((*entry)[3]);
|
|
if (!entry_uid || !entry_gid) {
|
|
spdlog::error("malformed /etc/passwd entry for user '{}'", user);
|
|
return std::nullopt;
|
|
}
|
|
uid = *entry_uid;
|
|
gid = *entry_gid;
|
|
}
|
|
|
|
if (group) {
|
|
if (is_all_digits(*group)) {
|
|
gid = *parse_int(*group);
|
|
} else {
|
|
std::filesystem::path group_file = image_root / "etc" / "group";
|
|
auto entry = lookup_entry(group_file, *group, 2);
|
|
auto entry_gid = entry ? parse_int((*entry)[2]) : std::nullopt;
|
|
if (!entry_gid) {
|
|
spdlog::error("could not resolve group '{}' in the image's /etc/group", *group);
|
|
return std::nullopt;
|
|
}
|
|
gid = *entry_gid;
|
|
}
|
|
}
|
|
|
|
// Looked up by the final resolved uid (field 2), independent of whether `user`
|
|
// was given as a name or a number, so it matches whichever /etc/passwd row
|
|
// actually owns that uid. No matching row -> fall back to "/root" for uid 0
|
|
// (matches useradd-less images' own convention for root) or "/" otherwise.
|
|
std::string home = uid == 0 ? "/root" : "/";
|
|
auto home_entry = lookup_entry(passwd_file, std::to_string(uid), 2);
|
|
if (home_entry && home_entry->size() > 5 && !(*home_entry)[5].empty()) {
|
|
home = (*home_entry)[5];
|
|
}
|
|
|
|
return ResolvedUser{uid, gid, home};
|
|
}
|