/* * user.cc - Implementation of the Key classes for user account data * libhscript, the HorizonScript library for * Project Horizon * * Copyright (c) 2019-2020 Adélie Linux and contributors. All rights reserved. * This code is licensed under the AGPL 3.0 license, as noted in the * LICENSE-code file in the root directory of this repository. * * SPDX-License-Identifier: AGPL-3.0-only */ #include #include #include #include #include #include #include "user.hh" #include "util.hh" #include "util/filesystem.hh" #include "util/net.hh" #include "util/output.hh" using namespace Horizon::Keys; const static std::set system_names = { "root", "bin", "daemon", "adm", "lp", "sync", "shutdown", "halt", "mail", "news", "uucp", "operator", "man", "postmaster", "cron", "ftp", "sshd", "at", "squid", "xfs", "games", "postgres", "cyrus", "vpopmail", "utmp", "catchlog", "alias", "qmaild", "qmailp", "qmailq", "qmailr", "qmails", "qmaill", "ntp", "smmsp", "guest", "nobody" }; const static std::set system_groups = { "root", "bin", "daemon", "sys", "adm", "tty", "disk", "lp", "mem", "kmem", "wheel", "floppy", "mail", "news", "uucp", "man", "cron", "console", "audio", "cdrom", "dialout", "ftp", "sshd", "input", "at", "tape", "video", "netdev", "readproc", "squid", "xfs", "kvm", "games", "shadow", "postgres", "cdrw", "usb", "vpopmail", "users", "catchlog", "ntp", "nofiles", "qmail", "qmaill", "smmsp", "locate", "abuild", "utmp", "ping", "nogroup", "nobody" }; /* * is_valid_name is from shadow libmisc/chkname.c: * * Copyright (c) 1990 - 1994, Julianne Frances Haugh * Copyright (c) 1996 - 2000, Marek Michałkiewicz * Copyright (c) 2001 - 2005, Tomasz Kłoczko * Copyright (c) 2005 - 2008, Nicolas François * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. The name of the copyright holders or contributors may not be used to * endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ static bool is_valid_name (const char *name) { /* * User/group names must match [a-z_][a-z0-9_-]*[$] */ if (('\0' == *name) || !((('a' <= *name) && ('z' >= *name)) || ('_' == *name))) { return false; } while ('\0' != *++name) { if (!(( ('a' <= *name) && ('z' >= *name) ) || ( ('0' <= *name) && ('9' >= *name) ) || ('_' == *name) || ('-' == *name) || ('.' == *name) || ( ('$' == *name) && ('\0' == *(name + 1)) ) )) { return false; } } return true; } /* End above copyright ^ */ /*! Determine if a string is a valid crypt passphrase * @param pw The string to test for validity. * @param key The name of key being validated ('rootpw', 'userpw', ...) * @param pos The location where the key occurs. * @returns true if +pw+ is a valid crypt passphrase; false otherwise. */ static bool string_is_crypt(const std::string &pw, const std::string &key, const Horizon::ScriptLocation &pos) { if(pw.size() < 5 || pw[0] != '$' || (pw[1] != '2' && pw[1] != '6') || pw[2] != '$') { output_error(pos, key + ": value is not a crypt-style encrypted passphrase"); return false; } return true; } Key *RootPassphrase::parseFromData(const std::string &data, const ScriptLocation &pos, int *errors, int *, const Script *script) { if(!string_is_crypt(data, "rootpw", pos)) { if(errors) *errors += 1; return nullptr; } return new RootPassphrase(script, pos, data); } bool RootPassphrase::validate() const { return true; } bool RootPassphrase::execute() const { const std::string root_line = "root:" + this->_value + ":" + std::to_string(time(nullptr) / 86400) + ":0:::::"; output_info(pos, "rootpw: setting root passphrase"); if(script->options().test(Simulate)) { std::cout << "(printf '" << root_line << "\\" << "n'; " << "cat " << script->targetDirectory() << "/etc/shadow |" << "sed '1d') > /tmp/shadow" << std::endl << "mv /tmp/shadow " << script->targetDirectory() << "/etc/shadow" << std::endl << "chown root:shadow " << script->targetDirectory() << "/etc/shadow" << std::endl << "chmod 640 " << script->targetDirectory() << "/etc/shadow" << std::endl; return true; } #ifdef HAS_INSTALL_ENV /* This was tested on gwyn during development. */ std::ifstream old_shadow(script->targetDirectory() + "/etc/shadow"); if(!old_shadow) { output_error(pos, "rootpw: cannot open existing shadow file"); return false; } std::stringstream shadow_stream; char shadow_line[200]; /* Discard root. */ old_shadow.getline(shadow_line, sizeof(shadow_line)); assert(strncmp(shadow_line, "root", 4) == 0); /* Insert the new root line... */ shadow_stream << root_line << std::endl; /* ...and copy the rest of the old shadow file. */ while(old_shadow.getline(shadow_line, sizeof(shadow_line))) { shadow_stream << shadow_line << std::endl; } old_shadow.close(); std::ofstream new_shadow(script->targetDirectory() + "/etc/shadow", std::ios_base::trunc); if(!new_shadow) { output_error(pos, "rootpw: cannot replace target shadow file"); return false; } new_shadow << shadow_stream.str(); return true; #else return false; /* LCOV_EXCL_LINE */ #endif /* HAS_INSTALL_ENV */ } Key *Username::parseFromData(const std::string &data, const ScriptLocation &pos, int *errors, int *, const Script *script) { if(!is_valid_name(data.c_str())) { if(errors) *errors += 1; output_error(pos, "username: invalid username specified"); return nullptr; } /* REQ: Runner.Validate.username.System */ if(system_names.find(data) != system_names.end()) { if(errors) *errors += 1; output_error(pos, "username: reserved system username", data); return nullptr; } return new Username(script, pos, data); } bool Username::execute() const { output_info(pos, "username: creating account " + _value); if(script->options().test(Simulate)) { std::cout << "useradd -c \"Adélie User\" -m -R " << script->targetDirectory() << " -U " << _value << std::endl; return true; } #ifdef HAS_INSTALL_ENV if(run_command("chroot", {script->targetDirectory(), "useradd", "-c", "Adélie User", "-m", "-U", _value}) != 0) { output_error(pos, "username: failed to create user account", _value); return false; } #endif /* HAS_INSTALL_ENV */ return true; /* LCOV_EXCL_LINE */ } Key *UserAlias::parseFromData(const std::string &data, const ScriptLocation &pos, int *errors, int *, const Script *script) { /* REQ: Runner.Validate.useralias.Validity */ const std::string::size_type sep = data.find_first_of(' '); if(sep == std::string::npos || data.length() == sep + 1) { if(errors) *errors += 1; output_error(pos, "useralias: alias is required", "expected format is: useralias [username] [alias...]"); return nullptr; } return new UserAlias(script, pos, data.substr(0, sep), data.substr(sep + 1)); } bool UserAlias::validate() const { return true; } bool UserAlias::execute() const { output_info(pos, "useralias: setting GECOS name for " + _username); if(script->options().test(Simulate)) { std::cout << "usermod -c \"" << _alias << "\" " << "-R " << script->targetDirectory() << " " << _username << std::endl; return true; } #ifdef HAS_INSTALL_ENV if(run_command("chroot", {script->targetDirectory(), "usermod", "-c", _alias, _username}) != 0) { output_error(pos, "useralias: failed to change GECOS for " + _username); return false; } #endif /* HAS_INSTALL_ENV */ return true; /* LCOV_EXCL_LINE */ } Key *UserPassphrase::parseFromData(const std::string &data, const ScriptLocation &pos, int *errors, int *, const Script *script) { /* REQ: Runner.Validate.userpw.Validity */ const std::string::size_type sep = data.find_first_of(' '); if(sep == std::string::npos || data.length() == sep + 1) { if(errors) *errors += 1; output_error(pos, "userpw: passphrase is required", "expected format is: userpw [username] [crypt...]"); return nullptr; } std::string passphrase = data.substr(sep + 1); if(!string_is_crypt(passphrase, "userpw", pos)) { if(errors) *errors += 1; return nullptr; } return new UserPassphrase(script, pos, data.substr(0, sep), data.substr(sep + 1)); } bool UserPassphrase::validate() const { /* If it's parseable, it's valid. */ return true; } bool UserPassphrase::execute() const { output_info(pos, "userpw: setting passphrase for " + _username); if(script->options().test(Simulate)) { std::cout << "usermod -p '" << _passphrase << "' " << "-R " << script->targetDirectory() << " " << _username << std::endl; return true; } #ifdef HAS_INSTALL_ENV if(run_command("chroot", {script->targetDirectory(), "usermod", "-p", _passphrase, _username}) != 0) { output_error(pos, "userpw: failed to set passphrase for " + _username); return false; } #endif /* HAS_INSTALL_ENV */ return true; } Key *UserIcon::parseFromData(const std::string &data, const ScriptLocation &pos, int *errors, int *, const Script *script) { /* REQ: Runner.Validate.usericon.Validity */ const std::string::size_type sep = data.find_first_of(' '); if(sep == std::string::npos || data.length() == sep + 1) { if(errors) *errors += 1; output_error(pos, "usericon: icon is required", "expected format is: usericon [username] [path|url]"); return nullptr; } std::string icon_path = data.substr(sep + 1); if(icon_path[0] != '/' && !is_valid_url(icon_path)) { if(errors) *errors += 1; output_error(pos, "usericon: path must be absolute path or valid URL"); return nullptr; } return new UserIcon(script, pos, data.substr(0, sep), icon_path); } bool UserIcon::validate() const { /* TODO XXX: ensure URL is accessible */ return true; } bool UserIcon::execute() const { const std::string as_path(script->targetDirectory() + "/var/lib/AccountsService/icons/" + _username); const std::string face_path(script->targetDirectory() + "/home/" + _username + "/.face"); output_info(pos, "usericon: setting avatar for " + _username); if(script->options().test(Simulate)) { if(_icon_path[0] == '/') { std::cout << "cp " << _icon_path << " " << as_path << std::endl; } else { std::cout << "curl -LO " << as_path << " " << _icon_path << std::endl; } std::cout << "cp " << as_path << " " << face_path << ".icon" << std::endl; std::cout << "chown $(hscript-printowner " << script->targetDirectory() << "/home/" << _username << ") " << face_path << ".icon" << std::endl; std::cout << "ln -s .face.icon " << face_path << std::endl; return true; } #ifdef HAS_INSTALL_ENV error_code ec; if(_icon_path[0] == '/') { fs::copy_file(_icon_path, as_path, ec); if(ec) { output_error(pos, "usericon: failed to copy icon", ec.message()); return false; } } else { if(!download_file(_icon_path, as_path)) { output_error(pos, "usericon: failed to download icon"); return false; } } fs::copy_file(as_path, face_path + ".icon", ec); if(ec) { output_error(pos, "usericon: failed to copy icon home", ec.message()); return false; } fs::create_symlink(".face.icon", face_path, ec); if(ec) { output_warning(pos, "usericon: failed to create legacy symlink"); } #endif /* HAS_INSTALL_ENV */ return true; /* LCOV_EXCL_LINE */ } Key *UserGroups::parseFromData(const std::string &data, const ScriptLocation &pos, int *errors, int *, const Script *script) { /* REQ: Runner.Validate.usergroups.Validity */ const std::string::size_type sep = data.find_first_of(' '); if(sep == std::string::npos || data.length() == sep + 1) { if(errors) *errors += 1; output_error(pos, "usergroups: at least one group is required", "expected format is: usergroups [user] [group(,...)]"); return nullptr; } std::set group_set; char next_group[17]; std::istringstream stream(data.substr(sep + 1)); while(stream.getline(next_group, 17, ',')) { std::string group(next_group); /* REQ: Runner.Validate.usergroups.Group */ if(system_groups.find(group) == system_groups.end()) { if(errors) *errors += 1; output_error(pos, "usergroups: invalid group name '" + group + "'", "group is not a recognised system group"); return nullptr; } group_set.insert(group); } /* REQ: Runner.Validate.usergroups.Group */ if(stream.fail() && !stream.eof()) { if(errors) *errors += 1; output_error(pos, "usergroups: group name exceeds maximum length", "groups may only be 16 characters or less"); return nullptr; } return new UserGroups(script, pos, data.substr(0, sep), group_set); } bool UserGroups::validate() const { /* All validation is done in parsing stage */ return true; } bool UserGroups::execute() const { output_info(pos, "usergroups: setting group membership for " + _username); std::string groups; for(auto &grp : _groups) { groups += grp + ","; } /* remove the last comma. */ groups.pop_back(); if(script->options().test(Simulate)) { std::cout << "usermod -aG " << groups << "-R " << script->targetDirectory() << " " << _username << std::endl; return true; } #ifdef HAS_INSTALL_ENV if(run_command("chroot", {script->targetDirectory(), "usermod", "-a", "-G", groups, _username}) != 0) { output_error(pos, "usergroups: failed to add groups to " + _username); return false; } #endif /* HAS_INSTALL_ENV */ return true; /* LCOV_EXCL_LINE */ }