import os import traceback from typing import Tuple, Optional from PyQt5.QtCore import Qt, QThread, pyqtSignal from PyQt5.QtGui import QCursor from PyQt5.QtWidgets import QLineEdit, QApplication, QDialog, QPushButton, QVBoxLayout, \ QSizePolicy, QToolBar, QLabel from bauh.api.abstract.context import ApplicationContext from bauh.commons.system import new_subprocess from bauh.view.core.config import CoreConfigManager from bauh.view.qt.components import QtComponentsManager, new_spacer from bauh.view.util import util from bauh.view.util.translation import I18n ACTION_ASK_ROOT = 99 class ValidatePassword(QThread): signal_valid = pyqtSignal(bool) def __init__(self, password: Optional[str] = None): super(ValidatePassword, self).__init__() self.password = password def run(self): if isinstance(self.password, str): try: valid = validate_password(self.password) except: traceback.print_exc() valid = False self.signal_valid.emit(valid) class RootDialog(QDialog): def __init__(self, i18n: I18n, max_tries: int = 3): super(RootDialog, self).__init__(flags=Qt.CustomizeWindowHint | Qt.WindowTitleHint) self.i18n = i18n self.max_tries = max_tries self.tries = 0 self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) self.setWindowIcon(util.get_default_icon()[1]) self.setWindowTitle(i18n['popup.root.title']) self.setLayout(QVBoxLayout()) self.setMinimumWidth(300) self.label_msg = QLabel(i18n['popup.root.msg']) self.label_msg.setObjectName('message') self.layout().addWidget(self.label_msg) self.input_password = QLineEdit() self.input_password.setObjectName('password') self.layout().addWidget(self.input_password) self.label_error = QLabel() self.label_error.setProperty('error', 'true') self.layout().addWidget(self.label_error) self.label_error.hide() self.lower_bar = QToolBar() self.layout().addWidget(self.lower_bar) self.lower_bar.addWidget(new_spacer()) self.bt_ok = QPushButton(i18n['popup.root.continue']) self.bt_ok.setDefault(True) self.bt_ok.setAutoDefault(True) self.bt_ok.setCursor(QCursor(Qt.PointingHandCursor)) self.bt_ok.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.bt_ok.setObjectName('ok') self.bt_ok.clicked.connect(self._validate_password) self.lower_bar.addWidget(self.bt_ok) self.bt_cancel = QPushButton() self.bt_cancel.setText(i18n['popup.button.cancel']) self.bt_cancel.setCursor(QCursor(Qt.PointingHandCursor)) self.bt_cancel.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.bt_cancel.setObjectName('bt_cancel') self.bt_cancel.clicked.connect(self.close) self.lower_bar.addWidget(self.bt_cancel) self.lower_bar.addWidget(new_spacer()) self.valid = False self.password = None self.validate_password = ValidatePassword() self.validate_password.signal_valid.connect(self._handle_password_validated) def _validate_password(self): password = self.input_password.text() if isinstance(password, str): self.password = password self.tries += 1 self.bt_ok.setEnabled(False) self.bt_cancel.setEnabled(False) self.input_password.setEnabled(False) self.label_error.setText('') QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) self.validate_password.password = password self.validate_password.start() def _handle_password_validated(self, valid: bool): self.valid = valid QApplication.restoreOverrideCursor() tries_ended = self.tries == self.max_tries if not self.valid: self.label_error.show() self.bt_cancel.setEnabled(True) if tries_ended: self.bt_cancel.setText(self.i18n['close'].capitalize()) self.label_error.setText(self.i18n['popup.root.bad_password.last_try']) self.bt_cancel.setFocus() else: self.label_error.setText(self.i18n['popup.root.bad_password.body']) self.bt_ok.setEnabled(True) self.input_password.setEnabled(True) self.input_password.setFocus() else: self.close() @staticmethod def ask_password(context: ApplicationContext, i18n: I18n, app_config: Optional[dict] = None, comp_manager: Optional[QtComponentsManager] = None, tries: int = 3) -> Tuple[bool, Optional[str]]: current_config = CoreConfigManager().get_config() if not app_config else app_config store_password = bool(current_config['store_root_password']) if store_password and isinstance(context.root_password, str) and validate_password(context.root_password): return True, context.root_password if comp_manager: comp_manager.save_states(state_id=ACTION_ASK_ROOT, only_visible=True) comp_manager.disable_visible() diag = RootDialog(i18n=i18n, max_tries=tries) diag.exec() password = diag.password if comp_manager: comp_manager.restore_state(ACTION_ASK_ROOT) if isinstance(password, str) and store_password: context.root_password = password return (True, password) if diag.valid else (False, None) def is_root(): return os.getuid() == 0 def validate_password(password: str) -> bool: clean = new_subprocess(['sudo', '-k']).stdout echo = new_subprocess(['echo', password], stdin=clean).stdout validate = new_subprocess(['sudo', '-S', '-v'], stdin=echo) for o in validate.stdout: pass for o in validate.stderr: if o: line = o.decode() if 'incorrect password attempt' in line: return False return True