diff --git a/Base/res/icons/16x16/app-escalator.png b/Base/res/icons/16x16/app-escalator.png new file mode 100644 index 00000000000..d238ca0bcef Binary files /dev/null and b/Base/res/icons/16x16/app-escalator.png differ diff --git a/Base/res/icons/32x32/app-escalator.png b/Base/res/icons/32x32/app-escalator.png new file mode 100644 index 00000000000..e387ca93952 Binary files /dev/null and b/Base/res/icons/32x32/app-escalator.png differ diff --git a/Meta/build-root-filesystem.sh b/Meta/build-root-filesystem.sh index 71f2193d0f0..c4c0284818c 100755 --- a/Meta/build-root-filesystem.sh +++ b/Meta/build-root-filesystem.sh @@ -97,6 +97,10 @@ if [ -f mnt/bin/pls ]; then chown 0:$wheel_gid mnt/bin/pls chmod 4750 mnt/bin/pls fi +if [ -f mnt/bin/Escalator ]; then + chown 0:$wheel_gid mnt/bin/Escalator + chmod 4750 mnt/bin/Escalator +fi if [ -f mnt/bin/utmpupdate ]; then chown 0:$utmp_gid mnt/bin/utmpupdate chmod 2755 mnt/bin/utmpupdate diff --git a/Userland/Applications/CMakeLists.txt b/Userland/Applications/CMakeLists.txt index 936366cc3ff..4dc354409cb 100644 --- a/Userland/Applications/CMakeLists.txt +++ b/Userland/Applications/CMakeLists.txt @@ -12,6 +12,7 @@ add_subdirectory(ClockSettings) add_subdirectory(CrashReporter) add_subdirectory(Debugger) add_subdirectory(DisplaySettings) +add_subdirectory(Escalator) add_subdirectory(FileManager) add_subdirectory(FontEditor) add_subdirectory(GamesSettings) diff --git a/Userland/Applications/Escalator/CMakeLists.txt b/Userland/Applications/Escalator/CMakeLists.txt new file mode 100644 index 00000000000..e8a17493115 --- /dev/null +++ b/Userland/Applications/Escalator/CMakeLists.txt @@ -0,0 +1,19 @@ +serenity_component( + Escalator + REQUIRED + TARGETS Escalator +) + +compile_gml(Escalator.gml EscalatorGML.h escalator_gml) + +set(SOURCES + main.cpp + EscalatorWindow.cpp +) + +set(GENERATED_SOURCES + EscalatorGML.h +) + +serenity_app(Escalator ICON app-escalator) +target_link_libraries(Escalator LibCore LibDesktop LibGUI LibMain) diff --git a/Userland/Applications/Escalator/Escalator.gml b/Userland/Applications/Escalator/Escalator.gml new file mode 100644 index 00000000000..08ec1fb3ed2 --- /dev/null +++ b/Userland/Applications/Escalator/Escalator.gml @@ -0,0 +1,49 @@ +@GUI::Widget { + fill_with_background_color: true + layout: @GUI::VerticalBoxLayout { + margins: [4] + } + + @GUI::Widget { + max_height: 32 + layout: @GUI::HorizontalBoxLayout { + spacing: 16 + } + + @GUI::ImageWidget { + name: "icon" + } + + @GUI::Label { + name: "description" + text_alignment: "CenterLeft" + } + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout { + margins: [4] + } + + @GUI::PasswordBox { + name: "password" + } + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout {} + fixed_height: 22 + + @GUI::Layout::Spacer {} + + @GUI::DialogButton { + name: "ok_button" + text: "OK" + } + + @GUI::DialogButton { + name: "cancel_button" + text: "Cancel" + } + } +} diff --git a/Userland/Applications/Escalator/EscalatorWindow.cpp b/Userland/Applications/Escalator/EscalatorWindow.cpp new file mode 100644 index 00000000000..9b2927fe63c --- /dev/null +++ b/Userland/Applications/Escalator/EscalatorWindow.cpp @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2022, Ashley N. + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "EscalatorWindow.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +EscalatorWindow::EscalatorWindow(StringView executable, Vector arguments, EscalatorWindow::Options const& options) + : m_arguments(arguments) + , m_executable(executable) + , m_current_user(options.current_user) + , m_preserve_env(options.preserve_env) +{ + auto app_icon = GUI::FileIconProvider::icon_for_executable(m_executable); + + set_title("Run as Root"); + set_icon(app_icon.bitmap_for_size(16)); + resize(345, 100); + set_resizable(false); + set_minimizable(false); + + auto& main_widget = set_main_widget(); + main_widget.load_from_gml(escalator_gml); + + RefPtr app_label = *main_widget.find_descendant_of_type_named("description"); + + String prompt; + if (options.description.is_empty()) + prompt = String::formatted("{} requires root access. Please enter password for user \"{}\".", m_arguments[0], m_current_user.username()); + else + prompt = options.description; + + app_label->set_text(prompt); + + m_icon_image_widget = *main_widget.find_descendant_of_type_named("icon"); + m_icon_image_widget->set_bitmap(app_icon.bitmap_for_size(32)); + + m_ok_button = *main_widget.find_descendant_of_type_named("ok_button"); + m_ok_button->on_click = [this](auto) { + auto result = check_password(); + if (result.is_error()) { + GUI::MessageBox::show_error(this, String::formatted("Failed to execute command: {}", result.error())); + close(); + } + }; + m_ok_button->set_default(true); + + m_cancel_button = *main_widget.find_descendant_of_type_named("cancel_button"); + m_cancel_button->on_click = [this](auto) { + close(); + }; + + m_password_input = *main_widget.find_descendant_of_type_named("password"); +} + +ErrorOr EscalatorWindow::check_password() +{ + String password = m_password_input->text(); + if (password.is_empty()) { + GUI::MessageBox::show_error(this, "Please enter a password."sv); + return {}; + } + + // FIXME: PasswordBox really should store its input directly as a SecretString. + Core::SecretString password_secret = Core::SecretString::take_ownership(password.to_byte_buffer()); + if (!m_current_user.authenticate(password_secret)) { + GUI::MessageBox::show_error(this, "Incorrect or disabled password."sv); + m_password_input->select_all(); + return {}; + } + + // Caller will close Escalator if error is returned. + TRY(execute_command()); + VERIFY_NOT_REACHED(); +} + +ErrorOr EscalatorWindow::execute_command() +{ + // Translate environ to format for Core::System::exec. + Vector exec_environment; + for (size_t i = 0; environ[i]; ++i) { + StringView env_view { environ[i], strlen(environ[i]) }; + auto maybe_needle = env_view.find('='); + + if (!maybe_needle.has_value()) + continue; + + if (!m_preserve_env && env_view.substring_view(0, maybe_needle.value()) != "TERM"sv) + continue; + + exec_environment.append(env_view); + } + + // Escalate process privilege to root user. + TRY(Core::System::seteuid(0)); + auto root_user = TRY(Core::Account::from_uid(0)); + TRY(root_user.login()); + + TRY(Core::System::pledge("stdio sendfd rpath exec")); + TRY(Core::System::exec(m_executable, m_arguments, Core::System::SearchInPath::No, exec_environment)); + VERIFY_NOT_REACHED(); +} diff --git a/Userland/Applications/Escalator/EscalatorWindow.h b/Userland/Applications/Escalator/EscalatorWindow.h new file mode 100644 index 00000000000..21c51e7cc53 --- /dev/null +++ b/Userland/Applications/Escalator/EscalatorWindow.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022, Ashley N. + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class EscalatorWindow final : public GUI::Window { + C_OBJECT(EscalatorWindow) +public: + struct Options { + StringView description; + Core::Account current_user; + bool preserve_env { false }; + }; + + virtual ~EscalatorWindow() override = default; + + ErrorOr execute_command(); + +private: + EscalatorWindow(StringView executable, Vector arguments, Options const& options); + + ErrorOr check_password(); + + Vector m_arguments; + StringView m_executable; + Core::Account m_current_user; + bool m_preserve_env { false }; + + RefPtr m_icon_image_widget; + RefPtr m_ok_button; + RefPtr m_cancel_button; + RefPtr m_password_input; +}; diff --git a/Userland/Applications/Escalator/main.cpp b/Userland/Applications/Escalator/main.cpp new file mode 100644 index 00000000000..0570487e677 --- /dev/null +++ b/Userland/Applications/Escalator/main.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2022, Ashley N. + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "EscalatorWindow.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +ErrorOr serenity_main(Main::Arguments arguments) +{ + Vector command; + Core::ArgsParser args_parser; + StringView description; + bool preserve_env = false; + args_parser.set_general_help("Escalate privilege to root for a given command using a GUI prompt."); + args_parser.set_stop_on_first_non_option(true); + args_parser.add_option(description, "Custom prompt to use for dialog", "prompt", 'P', "prompt"); + args_parser.add_option(preserve_env, "Preserve user environment when running command", "preserve-env", 'E'); + args_parser.add_positional_argument(command, "Command to run at elevated privilege level", "command"); + args_parser.parse(arguments); + + TRY(Core::System::pledge("stdio recvfd sendfd thread cpath rpath wpath unix proc exec id")); + + auto app = TRY(GUI::Application::try_create(arguments)); + + auto executable_path = Core::File::resolve_executable_from_environment(command[0]); + if (!executable_path.has_value()) { + GUI::MessageBox::show_error(nullptr, String::formatted("Could not execute command {}: Command not found.", command[0])); + return 127; + } + + auto current_user = TRY(Core::Account::self()); + auto window = TRY(EscalatorWindow::try_create(executable_path.value(), command, EscalatorWindow::Options { description, current_user, preserve_env })); + + if (current_user.uid() != 0) { + window->show(); + return app->exec(); + } else { + // Run directly as root if already root uid. + TRY(window->execute_command()); + return 0; + } +}