client/securedrop_client/gui/auth/dialog.py (137 lines of code) (raw):
"""
A dialog that allows users to sign in, or use the application offline.
Copyright (C) 2021 The Freedom of the Press Foundation.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging
from gettext import gettext as _
from PyQt5.QtCore import QSize, Qt
from PyQt5.QtGui import QBrush, QPalette
from PyQt5.QtWidgets import (
QDialog,
QGraphicsOpacityEffect,
QHBoxLayout,
QLabel,
QLineEdit,
QSizePolicy,
QVBoxLayout,
QWidget,
)
from securedrop_client import __version__
from securedrop_client.gui.auth.sign_in import LoginErrorBar, SignInButton
from securedrop_client.gui.auth.use_offline import LoginOfflineLink
from securedrop_client.gui.base import PasswordEdit
from securedrop_client.gui.base.checkbox import SDCheckBox
from securedrop_client.logic import Controller
from securedrop_client.resources import load_image, load_relative_css
logger = logging.getLogger(__name__)
class LoginDialog(QDialog):
"""
A dialog to display the login form.
"""
MIN_PASSWORD_LEN = 14 # Journalist.MIN_PASSWORD_LEN on server
MAX_PASSWORD_LEN = 128 # Journalist.MAX_PASSWORD_LEN on server
MIN_JOURNALIST_USERNAME = 3 # Journalist.MIN_USERNAME_LEN on server
def __init__(self, parent: QWidget) -> None:
super().__init__(parent)
# Set modal
self.setModal(True)
# Set layout
layout = QVBoxLayout(self)
self.setLayout(layout)
# Set margins and spacing
layout.setContentsMargins(0, 274, 0, 20)
layout.setSpacing(0)
# Set background
self.setAutoFillBackground(True)
palette = QPalette()
palette.setBrush(QPalette.Background, QBrush(load_image("login_bg.svg")))
self.setPalette(palette)
self.setFixedSize(QSize(596, 671)) # Set to size provided in the login_bg.svg file
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
# Create error bar
self.error_bar = LoginErrorBar()
# Create form widget
form = QWidget()
form.setObjectName("LoginDialog_form")
self.setStyleSheet(load_relative_css(__file__, "dialog.css"))
form_layout = QVBoxLayout()
form.setLayout(form_layout)
form_layout.setContentsMargins(80, 0, 80, 0)
form_layout.setSpacing(8)
self.username_label = QLabel(_("Username"))
self.username_field = QLineEdit()
self.password_label = QLabel(_("Passphrase"))
self.password_field = PasswordEdit(self)
self.check = SDCheckBox()
self.check.checkbox.stateChanged.connect(self.password_field.on_toggle_password_Action)
self.tfa_label = QLabel(_("Two-Factor Code"))
self.tfa_field = QLineEdit()
self.opacity_effect = QGraphicsOpacityEffect()
buttons = QWidget()
buttons_layout = QHBoxLayout()
buttons.setLayout(buttons_layout)
buttons_layout.setContentsMargins(0, 20, 0, 0)
self.submit = SignInButton()
self.submit.clicked.connect(self.validate)
self.offline_mode = LoginOfflineLink()
buttons_layout.addWidget(self.offline_mode)
buttons_layout.addStretch()
buttons_layout.addWidget(self.submit)
form_layout.addWidget(self.username_label)
form_layout.addWidget(self.username_field)
form_layout.addWidget(QWidget(self))
form_layout.addWidget(self.password_label)
form_layout.addWidget(self.password_field)
form_layout.addWidget(self.check, alignment=Qt.AlignRight)
form_layout.addWidget(self.tfa_label)
form_layout.addWidget(self.tfa_field)
form_layout.addWidget(buttons)
# Create widget to display application name and version
application_version = QLabel(_("SecureDrop Client v{}").format(__version__))
application_version.setAlignment(Qt.AlignHCenter)
application_version.setObjectName("LoginDialog_app_version_label")
# Add widgets
layout.addWidget(self.error_bar)
layout.addStretch()
layout.addWidget(form)
layout.addStretch()
layout.addWidget(application_version)
self.submit.setDefault(True)
def setup(self, controller: Controller) -> None:
self.controller = controller
self.offline_mode.clicked.connect(self.controller.login_offline_mode)
def reset(self) -> None:
"""
Resets the login form to the default state.
"""
self.username_field.setText("")
self.username_field.setFocus()
self.password_field.setText("")
self.tfa_field.setText("")
self.setDisabled(False)
self.error_bar.clear_message()
def error(self, message: str) -> None:
"""
Ensures the passed in message is displayed as an error message.
"""
self.setDisabled(False)
self.submit.setText(_("SIGN IN"))
self.error_bar.set_message(message)
self.opacity_effect.setOpacity(1)
self.offline_mode.setGraphicsEffect(self.opacity_effect)
def validate(self) -> None:
"""
Validate the user input -- we expect values for:
* username (free text)
* password (free text)
* TFA token (numerals)
"""
self.setDisabled(True)
username = self.username_field.text()
password = self.password_field.text()
tfa_token = self.tfa_field.text().replace(" ", "")
if username and password and tfa_token:
# Validate username
if len(username) < self.MIN_JOURNALIST_USERNAME:
self.setDisabled(False)
self.error(
_("That username won't work.\n" "It should be at least 3 characters long.")
)
return
# Validate password
if len(password) < self.MIN_PASSWORD_LEN or len(password) > self.MAX_PASSWORD_LEN:
self.setDisabled(False)
self.error(
_(
"That passphrase won't work.\n"
"It should be between 14 and 128 characters long."
)
)
return
# Validate 2FA token
try:
int(tfa_token)
except ValueError:
self.setDisabled(False)
self.error(
_("That two-factor code won't work.\n" "It should only contain numerals.")
)
return
self.submit.setText(_("SIGNING IN"))
# Changing the opacity of the link to .4 alpha of its' 100% value when authenticated
self.opacity_effect.setOpacity(0.4)
self.offline_mode.setGraphicsEffect(self.opacity_effect)
# If authentication is successful, clear error messages displayed priorly
self.error_bar.clear_message()
self.controller.login(username, password, tfa_token)
else:
self.setDisabled(False)
self.error(_("Please enter a username, passphrase and " "two-factor code."))