diff --git a/gui/__main__.py b/gui/__main__.py index d52b89c..4257a6b 100755 --- a/gui/__main__.py +++ b/gui/__main__.py @@ -14,7 +14,7 @@ import subprocess import qrcode from io import BytesIO from typing import Union -from core.Errors import UnknownConnectionTypeError, CommandNotFoundError, MissingSubscriptionError, InvalidSubscriptionError, ProfileActivationError, UnsupportedApplicationVersionError, FileIntegrityError, ProfileModificationError, ProfileStateConflictError, EndpointVerificationError +from core.Errors import UnknownConnectionTypeError, CommandNotFoundError, MissingSubscriptionError, InvalidSubscriptionError, ProfileActivationError, UnsupportedApplicationVersionError, FileIntegrityError, ProfileModificationError, ProfileStateConflictError, EndpointVerificationError, PolicyAssignmentError, PolicyInstatementError, PolicyRevocationError from core.controllers.ApplicationVersionController import ApplicationVersionController from core.controllers.ApplicationController import ApplicationController from core.controllers.ClientController import ClientController @@ -24,6 +24,7 @@ from core.controllers.LocationController import LocationController from core.controllers.ProfileController import ProfileController from core.controllers.SubscriptionController import SubscriptionController from core.controllers.SubscriptionPlanController import SubscriptionPlanController +from core.controllers.PrivilegePolicyController import PrivilegePolicyController from core.models.session.SessionConnection import SessionConnection from core.models.session.SessionProfile import SessionProfile from core.models.system.SystemConnection import SystemConnection @@ -608,6 +609,21 @@ class CustomWindow(QMainWindow): if config["logging"]["gui_logging_enabled"]: self._setup_gui_logging() + def has_shown_systemwide_prompt(self): + config = self._load_gui_config() + if not config: + return False + return config.get("systemwide", {}).get("prompt_shown", False) + + def mark_systemwide_prompt_shown(self): + config = self._load_gui_config() + if config is None: + config = {"logging": {"gui_logging_enabled": False, "log_level": "INFO"}} + if "systemwide" not in config: + config["systemwide"] = {} + config["systemwide"]["prompt_shown"] = True + self._save_gui_config(config) + def stop_gui_logging(self): @@ -895,6 +911,7 @@ class CustomWindow(QMainWindow): ResidentialPage(self.page_stack, self), InstallSystemPackage(self.page_stack, self), WelcomePage(self.page_stack, self), + SystemwidePromptPage(self.page_stack, self), PaymentDetailsPage(self.page_stack, self), DurationSelectionPage(self.page_stack, self), CurrencySelectionPage(self.page_stack, self), @@ -2841,7 +2858,14 @@ class InstallSystemPackage(Page): self.status_label.setStyleSheet("color: red;") def go_next(self): - self.page_stack.setCurrentIndex(self.page_stack.indexOf(self.page_stack.findChild(MenuPage))) + if not self.update_status.has_shown_systemwide_prompt(): + system_page = self.page_stack.findChild(SystemwidePromptPage) + if system_page is None: + self.page_stack.setCurrentIndex(self.page_stack.indexOf(self.page_stack.findChild(MenuPage))) + return + self.page_stack.setCurrentIndex(self.page_stack.indexOf(system_page)) + else: + self.page_stack.setCurrentIndex(self.page_stack.indexOf(self.page_stack.findChild(MenuPage))) class ProtocolPage(Page): def __init__(self, page_stack, main_window=None, parent=None): @@ -5308,6 +5332,7 @@ class Settings(Page): ("Subscriptions", self.show_subscription_page), ("Create/Edit", self.show_registrations_page), ("Verification", self.show_verification_page), + ("Systemwide", self.show_systemwide_page), ("Delete Profile", self.show_delete_page), ("Error Logs", self.show_logs_page), ("Debug Help", self.show_debug_page) @@ -6101,39 +6126,6 @@ class Settings(Page): - def show_account_page(self): - self.content_layout.setCurrentWidget(self.account_page) - self.update_button_states(0) - - - def show_subscription_page(self): - self.content_layout.setCurrentWidget(self.subscription_page) - self.update_button_states(1) - - def show_registrations_page(self): - self.content_layout.setCurrentWidget(self.registrations_page) - self.update_button_states(2) - - def show_verification_page(self): - self.content_layout.setCurrentWidget(self.verification_page) - self.update_button_states(3) - - def show_delete_page(self): - self.content_layout.setCurrentWidget(self.delete_page) - self.update_button_states(4) - - def show_logs_page(self): - self.content_layout.setCurrentWidget(self.logs_page) - self.update_button_states(5) - - def show_debug_page(self): - self.content_layout.setCurrentWidget(self.debug_page) - self.update_button_states(6) - - def update_button_states(self, active_index): - for i, btn in enumerate(self.menu_buttons): - btn.setChecked(i == active_index) - def reverse(self): self.page_stack.setCurrentIndex(self.page_stack.indexOf(self.page_stack.findChild(MenuPage))) @@ -6547,6 +6539,10 @@ class Settings(Page): self.verification_page = self.create_verification_page() self.content_layout.addWidget(self.verification_page) + self.content_layout.removeWidget(self.systemwide_page) + self.systemwide_page = self.create_systemwide_page() + self.content_layout.addWidget(self.systemwide_page) + self.content_layout.removeWidget(self.logs_page) self.logs_page = self.create_logs_page() self.content_layout.addWidget(self.logs_page) @@ -6751,6 +6747,7 @@ class Settings(Page): self.subscription_page = self.create_subscription_page() self.registrations_page = self.create_registrations_page() self.verification_page = self.create_verification_page() + self.systemwide_page = self.create_systemwide_page() self.logs_page = self.create_logs_page() self.delete_page = self.create_delete_page() self.debug_page = self.create_debug_page() @@ -6759,6 +6756,7 @@ class Settings(Page): self.content_layout.addWidget(self.subscription_page) self.content_layout.addWidget(self.registrations_page) self.content_layout.addWidget(self.verification_page) + self.content_layout.addWidget(self.systemwide_page) self.content_layout.addWidget(self.logs_page) self.content_layout.addWidget(self.delete_page) self.content_layout.addWidget(self.debug_page) @@ -6766,6 +6764,45 @@ class Settings(Page): self.content_layout.setCurrentIndex(0) self.show_account_page() + def show_account_page(self): + self.content_layout.setCurrentWidget(self.account_page) + self._select_menu_button("Overview") + + def show_subscription_page(self): + self.content_layout.setCurrentWidget(self.subscription_page) + self._select_menu_button("Subscriptions") + + def show_registrations_page(self): + self.content_layout.setCurrentWidget(self.registrations_page) + self._select_menu_button("Create/Edit") + + def show_verification_page(self): + self.content_layout.setCurrentWidget(self.verification_page) + self._select_menu_button("Verification") + + def show_systemwide_page(self): + self.content_layout.setCurrentWidget(self.systemwide_page) + self._select_menu_button("Systemwide") + + def show_logs_page(self): + self.content_layout.setCurrentWidget(self.logs_page) + self._select_menu_button("Error Logs") + + def show_delete_page(self): + self.content_layout.setCurrentWidget(self.delete_page) + self._select_menu_button("Delete Profile") + + def show_debug_page(self): + self.content_layout.setCurrentWidget(self.debug_page) + self._select_menu_button("Debug Help") + + def _select_menu_button(self, label): + for btn in self.menu_buttons: + if btn.text() == label: + btn.setChecked(True) + else: + btn.setChecked(False) + def create_logs_page(self): page = QWidget() layout = QVBoxLayout(page) @@ -6879,6 +6916,87 @@ class Settings(Page): self.load_registrations_settings() return page + def create_systemwide_page(self): + page = QWidget() + layout = QVBoxLayout(page) + layout.setSpacing(20) + layout.setContentsMargins(20, 20, 20, 20) + + title = QLabel("SYSTEM-WIDE PROFILES") + title.setStyleSheet(f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}") + layout.addWidget(title) + + description = QLabel("Control whether HydraVeil configures a sudo policy so system-wide WireGuard profiles can be started without entering your sudo password.") + description.setWordWrap(True) + description.setStyleSheet(f"color: white; font-size: 14px; {self.font_style}") + layout.addWidget(description) + + status_layout = QHBoxLayout() + status_label = QLabel("Current status:") + status_label.setStyleSheet(f"color: white; font-size: 14px; {self.font_style}") + self.systemwide_status_value = QLabel("") + self.systemwide_status_value.setStyleSheet(f"color: #e67e22; font-size: 14px; {self.font_style}") + status_layout.addWidget(status_label) + status_layout.addWidget(self.systemwide_status_value) + status_layout.addStretch() + layout.addLayout(status_layout) + + toggle_layout = QHBoxLayout() + self.systemwide_toggle = QCheckBox("Enable system-wide policy") + self.systemwide_toggle.setStyleSheet(self.get_checkbox_style()) + toggle_layout.addWidget(self.systemwide_toggle) + toggle_layout.addStretch() + layout.addLayout(toggle_layout) + + save_button = QPushButton() + save_button.setFixedSize(75, 46) + save_button.setIcon(QIcon(os.path.join(self.btn_path, "save.png"))) + save_button.setIconSize(QSize(75, 46)) + save_button.clicked.connect(self.save_systemwide_settings) + + button_layout = QHBoxLayout() + button_layout.addWidget(save_button) + button_layout.addStretch() + layout.addLayout(button_layout) + + layout.addStretch() + self.load_systemwide_settings() + return page + + def load_systemwide_settings(self): + enabled = False + try: + enabled = PrivilegePolicyController.is_instated() + except Exception: + enabled = False + self.systemwide_toggle.setChecked(enabled) + if enabled: + self.systemwide_status_value.setText("Enabled") + self.systemwide_status_value.setStyleSheet(f"color: #2ecc71; font-size: 14px; {self.font_style}") + else: + self.systemwide_status_value.setText("Disabled") + self.systemwide_status_value.setStyleSheet(f"color: #e67e22; font-size: 14px; {self.font_style}") + + def save_systemwide_settings(self): + enable = self.systemwide_toggle.isChecked() + try: + if enable: + PrivilegePolicyController.instate() + else: + if PrivilegePolicyController.is_instated(): + PrivilegePolicyController.revoke() + self.load_systemwide_settings() + self.update_status.update_status("System-wide policy settings updated") + except CommandNotFoundError as e: + self.systemwide_status_value.setText(str(e)) + self.systemwide_status_value.setStyleSheet(f"color: red; font-size: 14px; {self.font_style}") + except (PolicyAssignmentError, PolicyInstatementError, PolicyRevocationError) as e: + self.systemwide_status_value.setText(str(e)) + self.systemwide_status_value.setStyleSheet(f"color: red; font-size: 14px; {self.font_style}") + except Exception: + self.systemwide_status_value.setText("Failed to update policy") + self.systemwide_status_value.setStyleSheet(f"color: red; font-size: 14px; {self.font_style}") + def load_registrations_settings(self) -> None: try: config = self.update_status._load_gui_config() @@ -7855,6 +7973,99 @@ class WelcomePage(Page): self.page_stack.setCurrentIndex(self.page_stack.indexOf(install_page)) +class SystemwidePromptPage(Page): + def __init__(self, page_stack, main_window=None, parent=None): + super().__init__("Systemwide", page_stack, main_window, parent) + self.btn_path = main_window.btn_path + self.update_status = main_window + self.button_back.setVisible(True) + self.button_back.clicked.connect(self.back_to_install) + self.button_next.setVisible(False) + self.button_go.setVisible(False) + self.status_label = QLabel(self) + self.status_label.setGeometry(80, 430, 640, 40) + self.status_label.setStyleSheet("font-size: 14px; color: cyan;") + self.setup_ui() + + def setup_ui(self): + self.title.setGeometry(20, 50, 760, 40) + self.title.setText("Enable system-wide WireGuard profiles") + description = QLabel(self) + description.setGeometry(80, 100, 640, 80) + description.setWordWrap(True) + description.setStyleSheet("font-size: 14px; color: cyan;") + description.setText("You can enable system-wide WireGuard profiles so they can be started without entering your sudo password each time. This uses a secure sudo policy tailored to your user.") + icon_label = QLabel(self) + icon_label.setGeometry(80, 200, 64, 64) + icon_pix = QPixmap(os.path.join(self.btn_path, "wireguard_system_wide.png")) + icon_label.setPixmap(icon_pix.scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)) + yes_button = QPushButton("Yes, enable system-wide profiles", self) + yes_button.setGeometry(170, 200, 300, 50) + yes_button.setStyleSheet(""" + QPushButton { + background: #007AFF; + color: white; + border: none; + border-radius: 4px; + font-size: 13px; + font-weight: bold; + } + QPushButton:hover { + background: #0056CC; + } + """) + yes_button.clicked.connect(self.enable_systemwide) + no_button = QPushButton("Not now", self) + no_button.setGeometry(170, 270, 120, 40) + no_button.setStyleSheet(""" + QPushButton { + background: #007AFF; + color: white; + border: none; + border-radius: 4px; + font-size: 13px; + font-weight: bold; + } + QPushButton:hover { + background: #0056CC; + } + """) + no_button.clicked.connect(self.skip_systemwide) + self.refresh_status() + + def refresh_status(self): + if PrivilegePolicyController.is_instated(): + self.status_label.setText("Current status: system-wide policy is enabled.") + self.status_label.setStyleSheet("font-size: 14px; color: #2ecc71;") + else: + self.status_label.setText("Current status: system-wide policy is disabled.") + self.status_label.setStyleSheet("font-size: 14px; color: #e67e22;") + + def enable_systemwide(self): + try: + PrivilegePolicyController.instate() + self.update_status.mark_systemwide_prompt_shown() + self.refresh_status() + self.update_status.update_status("System-wide policy enabled") + self.page_stack.setCurrentIndex(self.page_stack.indexOf(self.page_stack.findChild(MenuPage))) + except CommandNotFoundError as e: + self.status_label.setText(str(e)) + self.status_label.setStyleSheet("font-size: 14px; color: red;") + except (PolicyAssignmentError, PolicyInstatementError) as e: + self.status_label.setText(str(e)) + self.status_label.setStyleSheet("font-size: 14px; color: red;") + except Exception: + self.status_label.setText("Failed to enable system-wide policy") + self.status_label.setStyleSheet("font-size: 14px; color: red;") + + def skip_systemwide(self): + self.update_status.mark_systemwide_prompt_shown() + self.page_stack.setCurrentIndex(self.page_stack.indexOf(self.page_stack.findChild(MenuPage))) + + def back_to_install(self): + install_page = self.page_stack.findChild(InstallSystemPackage) + if install_page: + self.page_stack.setCurrentIndex(self.page_stack.indexOf(install_page)) class DurationSelectionPage(Page): def __init__(self, page_stack, main_window=None, parent=None): super().__init__("Select Duration", page_stack, main_window, parent) diff --git a/pyproject.toml b/pyproject.toml index c33be34..816dbb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sp-hydra-veil-gui" -version = "1.1.7" +version = "1.1.8" authors = [ { name = "Simplified Privacy" }, ] @@ -12,7 +12,7 @@ classifiers = [ "Operating System :: POSIX :: Linux", ] dependencies = [ - "sp-hydra-veil-core == 1.1.7", + "sp-hydra-veil-core == 1.1.8-a2", "pyperclip ~= 1.9.0", "pyqt6 ~= 6.7.1", "qrcode[pil] ~= 8.2"