Exemplo n.º 1
0
def test_LoginDialog_error():
    """
    Any error message passed in is assigned as the text for the error label.
    """
    mock_controller = mock.MagicMock()
    ld = LoginDialog(None)
    ld.setup(mock_controller)
    ld.error_label = mock.MagicMock()
    ld.error('foo')
    ld.error_label.setText.assert_called_once_with('foo')
Exemplo n.º 2
0
def test_LoginDialog_validate_no_input():
    """
    If the user doesn't provide input, tell them and give guidance.
    """
    mock_controller = mock.MagicMock()
    ld = LoginDialog(None)
    ld.setup(mock_controller)
    ld.username_field.text = mock.MagicMock(return_value='')
    ld.password_field.text = mock.MagicMock(return_value='')
    ld.tfa_field.text = mock.MagicMock(return_value='')
    ld.setDisabled = mock.MagicMock()
    ld.error = mock.MagicMock()
    ld.validate()
    assert ld.setDisabled.call_count == 2
    assert ld.error.call_count == 1
Exemplo n.º 3
0
def test_LoginDialog_validate_input_ok():
    """
    Valid input from the user causes a call to the controller's login method.
    """
    mock_controller = mock.MagicMock()
    ld = LoginDialog(None)
    ld.setup(mock_controller)
    ld.username_field.text = mock.MagicMock(return_value='foo')
    ld.password_field.text = mock.MagicMock(return_value='bar')
    ld.tfa_field.text = mock.MagicMock(return_value='123456')
    ld.setDisabled = mock.MagicMock()
    ld.error = mock.MagicMock()
    ld.validate()
    assert ld.setDisabled.call_count == 1
    assert ld.error.call_count == 0
    mock_controller.login.assert_called_once_with('foo', 'bar', '123456')
Exemplo n.º 4
0
def test_LoginDialog_validate_too_short_password(mocker):
    """
    If the password is too small, we show an informative error message.
    """
    mock_controller = mocker.MagicMock()

    ld = LoginDialog(None)
    ld.setup(mock_controller)
    ld.username_field.text = mocker.MagicMock(return_value='foo')
    ld.password_field.text = mocker.MagicMock(return_value='bar')
    ld.tfa_field.text = mocker.MagicMock(return_value='123456')
    ld.setDisabled = mocker.MagicMock()
    ld.error = mocker.MagicMock()

    ld.validate()

    assert ld.setDisabled.call_count == 2
    assert ld.error.call_count == 1
    assert mock_controller.login.call_count == 0
Exemplo n.º 5
0
def test_LoginDialog_validate_input_non_numeric_2fa(mocker):
    """
    If the user doesn't provide numeric 2fa input, tell them and give
    guidance.
    """
    mock_controller = mocker.MagicMock()

    ld = LoginDialog(None)
    ld.setup(mock_controller)
    ld.username_field.text = mocker.MagicMock(return_value='foo')
    ld.password_field.text = mocker.MagicMock(return_value='nicelongpassword')
    ld.tfa_field.text = mocker.MagicMock(return_value='baz')
    ld.setDisabled = mocker.MagicMock()
    ld.error = mocker.MagicMock()

    ld.validate()

    assert ld.setDisabled.call_count == 2
    assert ld.error.call_count == 1
    assert mock_controller.login.call_count == 0
Exemplo n.º 6
0
class Window(QMainWindow):
    """
    Represents the application's main window that will contain the UI widgets.
    All interactions with the UI go through the object created by this class.
    """

    icon = "icon.png"

    def __init__(self) -> None:
        """
        Create the default start state. The window contains a root widget into
        which is placed:

        * A status bar widget at the top, containing curent user / status
          information.
        * A main-view widget, itself containing a list view for sources and a
          place for details / message contents / forms.
        """
        super().__init__()

        load_font("Montserrat")
        load_font("Source_Sans_Pro")
        self.setStyleSheet(load_css("sdclient.css"))
        self.setWindowTitle(_("SecureDrop Client {}").format(__version__))
        self.setWindowIcon(load_icon(self.icon))

        # Top Pane to display activity and error messages
        self.top_pane = TopPane()

        # Main Pane to display everything else
        self.main_pane = QWidget()
        layout = QHBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        self.main_pane.setLayout(layout)
        self.left_pane = LeftPane()
        self.main_view = MainView(self.main_pane)
        layout.addWidget(self.left_pane)
        layout.addWidget(self.main_view)

        # Set the main window's central widget to show Top Pane and Main Pane
        self.central_widget = QWidget()
        central_widget_layout = QVBoxLayout()
        central_widget_layout.setContentsMargins(0, 0, 0, 0)
        central_widget_layout.setSpacing(0)
        self.central_widget.setLayout(central_widget_layout)
        self.setCentralWidget(self.central_widget)
        central_widget_layout.addWidget(self.top_pane)
        central_widget_layout.addWidget(self.main_pane)

    def setup(self, controller):
        """
        Create references to the controller logic and instantiate the various
        views used in the UI.
        """
        self.controller = controller  # Reference the Controller logic instance.
        self.top_pane.setup(self.controller)
        self.left_pane.setup(self, self.controller)
        self.main_view.setup(self.controller)
        self.show_login()

    def show_main_window(self, db_user: User = None) -> None:
        """
        Show main application window.
        """
        self.autosize_window()
        self.show()

        if db_user:
            self.set_logged_in_as(db_user)

    def autosize_window(self):
        """
        Ensure the application window takes up 100% of the available screen
        (i.e. the whole of the virtualised desktop in Qubes dom)
        """
        screen = QDesktopWidget().screenGeometry()
        self.resize(screen.width(), screen.height())

    def show_login(self, error: str = ""):
        """
        Show the login form.
        """
        self.login_dialog = LoginDialog(self)

        # Always display the login dialog centered in the screen.
        screen_size = QDesktopWidget().screenGeometry()
        login_dialog_size = self.login_dialog.geometry()
        x_center = (screen_size.width() - login_dialog_size.width()) / 2
        y_center = (screen_size.height() - login_dialog_size.height()) / 2
        self.login_dialog.move(x_center, y_center)
        self.login_dialog.setup(self.controller)
        self.login_dialog.reset()
        if error:
            self.login_dialog.error(error)
        self.login_dialog.show()

    def show_login_error(self, error):
        """
        Display an error in the login dialog.
        """
        if self.login_dialog and error:
            self.login_dialog.error(error)

    def hide_login(self):
        """
        Kill the login dialog.
        """
        self.login_dialog.accept()
        self.login_dialog = None

    def refresh_current_source_conversation(self):
        """
        Update the current conversation if the source collection has changed.
        """
        self.main_view.on_source_changed()

    def show_sources(self, sources: List[Source]):
        """
        Update the left hand sources list in the UI with the passed in list of
        sources.
        """
        self.main_view.show_sources(sources)

    def show_last_sync(self, updated_on):
        """
        Display a message indicating the time of last sync with the server.
        """
        if updated_on:
            self.update_activity_status(
                _("Last Refresh: {}").format(updated_on.humanize()))
        else:
            self.update_activity_status(_("Last Refresh: never"))

    def set_logged_in_as(self, db_user: User):
        """
        Update the UI to show user logged in with username.
        """
        self.left_pane.set_logged_in_as(db_user)
        self.top_pane.set_logged_in()

    def logout(self):
        """
        Update the UI to show the user is logged out.
        """
        self.left_pane.set_logged_out()
        self.top_pane.set_logged_out()

    def update_activity_status(self, message: str, duration=0):
        """
        Display an activity status message to the user. Optionally, supply a duration
        (in milliseconds), the default will continuously show the message.
        """
        self.top_pane.update_activity_status(message, duration)

    def update_error_status(self, message: str, duration=10000) -> None:
        """
        Display an error status message to the user. Optionally, supply a duration
        (in milliseconds), the default will continuously show the message.
        """
        self.top_pane.update_error_status(message, duration)

    def clear_error_status(self):
        """
        Clear any message currently in the error status bar.
        """
        self.top_pane.clear_error_status()

    def clear_clipboard(self):
        """
        Purge any clipboard contents.
        """
        cb = QApplication.clipboard()
        cb.clear()
Exemplo n.º 7
0
class Window(QMainWindow):
    """
    Represents the application's main window that will contain the UI widgets.
    All interactions with the UI go through the object created by this class.
    """

    icon = 'icon.png'

    def __init__(self):
        """
        Create the default start state. The window contains a root widget into
        which is placed:

        * A status bar widget at the top, containing curent user / status
          information.
        * A main-view widget, itself containing a list view for sources and a
          place for details / message contents / forms.
        """
        super().__init__()
        self.setWindowTitle(_("SecureDrop Client {}").format(__version__))
        self.setWindowIcon(load_icon(self.icon))
        self.widget = QWidget()
        widget_layout = QVBoxLayout()
        self.widget.setLayout(widget_layout)
        self.tool_bar = ToolBar(self.widget)
        self.main_view = MainView(self.widget)
        self.main_view.source_list.itemSelectionChanged.\
            connect(self.on_source_changed)
        widget_layout.addWidget(self.tool_bar, 1)
        widget_layout.addWidget(self.main_view, 6)
        self.setCentralWidget(self.widget)
        self.show()
        self.autosize_window()

    def setup(self, controller):
        """
        Create references to the controller logic and instantiate the various
        views used in the UI.
        """
        self.controller = controller  # Reference the Client logic instance.
        self.tool_bar.setup(self, controller)
        self.status_bar = QStatusBar(self)
        self.setStatusBar(self.status_bar)
        self.set_status('Started SecureDrop Client. Please sign in.', 20000)
        self.login_dialog = LoginDialog(self)
        self.main_view.setup(self.controller)

    def autosize_window(self):
        """
        Ensure the application window takes up 100% of the available screen
        (i.e. the whole of the virtualised desktop in Qubes dom)
        """
        screen = QDesktopWidget().screenGeometry()
        self.resize(screen.width(), screen.height())

    def show_login(self):
        """
        Show the login form.
        """
        self.login_dialog = LoginDialog(self)
        self.login_dialog.setup(self.controller)
        self.login_dialog.reset()
        self.login_dialog.exec()

    def show_login_error(self, error):
        """
        Display an error in the login dialog.
        """
        if self.login_dialog and error:
            self.login_dialog.error(error)

    def hide_login(self):
        """
        Kill the login dialog.
        """
        self.login_dialog.accept()
        self.login_dialog = None

    def update_error_status(self, error=None):
        """
        Show an error message on the sidebar.
        """
        self.main_view.update_error_status(error)

    def show_sources(self, sources):
        """
        Update the left hand sources list in the UI with the passed in list of
        sources.
        """
        self.main_view.source_list.update(sources)

    def show_sync(self, updated_on):
        """
        Display a message indicating the data-sync state.
        """
        if updated_on:
            self.main_view.status.setText('Last Sync: ' +
                                          updated_on.humanize())
        else:
            self.main_view.status.setText(_('Waiting to Synchronize'))

    def set_logged_in_as(self, username):
        """
        Update the UI to show user logged in with username.
        """
        self.tool_bar.set_logged_in_as(username)

    def logout(self):
        """
        Update the UI to show the user is logged out.
        """
        self.tool_bar.set_logged_out()

    def on_source_changed(self):
        """
        React to when the selected source has changed.
        """
        source_item = self.main_view.source_list.currentItem()
        source_widget = self.main_view.source_list.itemWidget(source_item)
        if source_widget:
            self.show_conversation_for(source_widget.source)

    def show_conversation_for(self, source):
        """
        TODO: Finish this...
        """
        conversation = ConversationView(self)
        conversation.add_message('Source name: {}'.format(
                                 source.journalist_designation))
        conversation.add_message('Hello, hello, is this thing switched on?')
        conversation.add_reply('Yes, I can hear you loud and clear!')
        conversation.add_reply('How can I help?')
        conversation.add_message('I have top secret documents relating to '
                                 'a massive technical scandal at the heart '
                                 ' of the Freedom of the Press Foundation. '
                                 'In a shocking turn of events, it appears '
                                 'they give away all their software for FREE.')
        conversation.add_message("Hello: I’m a nurse at one of the trauma "
                                 "centers in town. We've had many patients in "
                                 "the last several months, all with "
                                 "similar/mysterious respiratory issues. My "
                                 "staff has noticed that most live down-wind "
                                 "from the Dole fields West of 696. Some of "
                                 "the patients say they have complained to "
                                 "local authorities about sewage smells. One "
                                 "said she's spotted a truck spraying a "
                                 "sludge of some kind, on the fields at "
                                 "night. I'm attaching a video from the "
                                 "patient who taped the trucks, and a PDF of "
                                 "redacted police reports that other patients "
                                 "shared. I don’t know if there's much you "
                                 "can do, but if there is I would be happy "
                                 "to help.")
        conversation.add_message("I work at the City Water Department, and a "
                                 "man named Reggie Esters is one of our board "
                                 "directors. I believe Reggie is related to "
                                 "Rep Monica Conyers. He's literally never "
                                 "here, and the resume on file for him makes "
                                 "no sense. I have a hunch he is not in his "
                                 "job legitimately, and think you should look "
                                 "into this. Also: someone I work with heard "
                                 "him on the phone once, talking about his "
                                 "'time' at Jackson—that contradicts his "
                                 "resume. It really seems fishy.",
                                 ['fishy_cv.PDF (234Kb)', ])
        conversation.add_reply("THIS IS IT THIS IS THE TAPE EVERYONE'S "
                               "LOOKING FOR!!!", ['filename.pdf (32Kb)', ])
        conversation.add_reply("Hello: I read your story on Sally Dale, and "
                               "her lawsuit against the St. Joseph's "
                               "Orphanage. My great-aunt was one of the nuns "
                               "there. She is willing to be interviewed, but "
                               "does not want her name, location, or any "
                               "identity details released. She feels "
                               "horrible. She wants the children who survived "
                               "to find peace. Thanks.")
        self.main_view.update_view(conversation)

    def set_status(self, message, duration=5000):
        """
        Display a status message to the user. Optionally, supply a duration
        (in milliseconds), the default value being a duration of 5 seconds.
        """
        self.status_bar.showMessage(message, duration)
Exemplo n.º 8
0
class Window(QMainWindow):
    """
    Represents the application's main window that will contain the UI widgets.
    All interactions with the UI go through the object created by this class.
    """

    icon = 'icon.png'

    def __init__(self):
        """
        Create the default start state. The window contains a root widget into
        which is placed:

        * A status bar widget at the top, containing curent user / status
          information.
        * A main-view widget, itself containing a list view for sources and a
          place for details / message contents / forms.
        """
        super().__init__()

        self.setWindowTitle(_("SecureDrop Controller {}").format(__version__))
        self.setWindowIcon(load_icon(self.icon))

        # Top Pane to display activity and error messages
        self.top_pane = TopPane()

        # Main Pane to display everything else
        self.main_pane = QWidget()
        layout = QHBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        self.main_pane.setLayout(layout)
        self.left_pane = LeftPane()
        self.main_view = MainView(self.main_pane)
        layout.addWidget(self.left_pane, 1)
        layout.addWidget(self.main_view, 8)

        # Set the main window's central widget to show Top Pane and Main Pane
        self.central_widget = QWidget()
        central_widget_layout = QVBoxLayout()
        central_widget_layout.setContentsMargins(0, 0, 0, 0)
        central_widget_layout.setSpacing(0)
        self.central_widget.setLayout(central_widget_layout)
        self.setCentralWidget(self.central_widget)
        central_widget_layout.addWidget(self.top_pane)
        central_widget_layout.addWidget(self.main_pane)

    def setup(self, controller):
        """
        Create references to the controller logic and instantiate the various
        views used in the UI.
        """
        self.controller = controller  # Reference the Controller logic instance.
        self.top_pane.setup(self.controller)
        self.left_pane.setup(self, self.controller)
        self.main_view.setup(self.controller)
        self.show_login()

    def show_main_window(self, username: str = None) -> None:
        self.autosize_window()
        self.show()

        if username:
            self.set_logged_in_as(username)

    def autosize_window(self):
        """
        Ensure the application window takes up 100% of the available screen
        (i.e. the whole of the virtualised desktop in Qubes dom)
        """
        screen = QDesktopWidget().screenGeometry()
        self.resize(screen.width(), screen.height())

    def show_login(self):
        """
        Show the login form.
        """
        self.login_dialog = LoginDialog(self)
        self.login_dialog.move(
            QApplication.desktop().screen().rect().center() -
            self.rect().center())
        self.login_dialog.setup(self.controller)
        self.login_dialog.reset()
        self.login_dialog.exec()

    def show_login_error(self, error):
        """
        Display an error in the login dialog.
        """
        if self.login_dialog and error:
            self.login_dialog.error(error)

    def hide_login(self):
        """
        Kill the login dialog.
        """
        self.login_dialog.accept()
        self.login_dialog = None

    def show_sources(self, sources: List[Source]):
        """
        Update the left hand sources list in the UI with the passed in list of
        sources.
        """
        self.main_view.show_sources(sources)

    def show_sync(self, updated_on):
        """
        Display a message indicating the data-sync state.
        """
        if updated_on:
            self.update_activity_status(
                _('Last refresh: {}').format(updated_on.humanize()))
        else:
            self.update_activity_status(_('Waiting to refresh...'), 5000)

    def set_logged_in_as(self, username):
        """
        Update the UI to show user logged in with username.
        """
        self.left_pane.set_logged_in_as(username)
        self.top_pane.enable_refresh()

    def logout(self):
        """
        Update the UI to show the user is logged out.
        """
        self.left_pane.set_logged_out()
        self.top_pane.disable_refresh()

    def update_activity_status(self, message: str, duration=0):
        """
        Display an activity status message to the user. Optionally, supply a duration
        (in milliseconds), the default will continuously show the message.
        """
        self.top_pane.update_activity_status(message, duration)

    def update_error_status(self, message: str, duration=10000):
        """
        Display an error status message to the user. Optionally, supply a duration
        (in milliseconds), the default will continuously show the message.
        """
        self.top_pane.update_error_status(message, duration)

    def clear_error_status(self):
        """
        Clear any message currently in the error status bar.
        """
        self.top_pane.clear_error_status()
Exemplo n.º 9
0
class Window(QMainWindow):
    """
    Represents the application's main window that will contain the UI widgets.
    All interactions with the UI go through the object created by this class.
    """

    icon = 'icon.png'

    def __init__(self):
        """
        Create the default start state. The window contains a root widget into
        which is placed:

        * A status bar widget at the top, containing curent user / status
          information.
        * A main-view widget, itself containing a list view for sources and a
          place for details / message contents / forms.
        """
        super().__init__()
        self.setWindowTitle(_("SecureDrop Client {}").format(__version__))
        self.setWindowIcon(load_icon(self.icon))
        self.widget = QWidget()
        widget_layout = QVBoxLayout()
        self.widget.setLayout(widget_layout)
        self.tool_bar = ToolBar(self.widget)
        self.main_view = MainView(self.widget)
        self.main_view.source_list.itemSelectionChanged.\
            connect(self.on_source_changed)
        widget_layout.addWidget(self.tool_bar, 1)
        widget_layout.addWidget(self.main_view, 6)
        self.setCentralWidget(self.widget)
        self.current_source = None  # Tracks which source is shown
        self.conversations = {}
        self.show()
        self.autosize_window()

    def setup(self, controller):
        """
        Create references to the controller logic and instantiate the various
        views used in the UI.
        """
        self.controller = controller  # Reference the Client logic instance.
        self.tool_bar.setup(self, controller)
        self.status_bar = QStatusBar(self)
        self.setStatusBar(self.status_bar)
        self.set_status('Started SecureDrop Client. Please sign in.', 20000)
        self.login_dialog = LoginDialog(self)
        self.main_view.setup(self.controller)

    def autosize_window(self):
        """
        Ensure the application window takes up 100% of the available screen
        (i.e. the whole of the virtualised desktop in Qubes dom)
        """
        screen = QDesktopWidget().screenGeometry()
        self.resize(screen.width(), screen.height())

    def show_login(self):
        """
        Show the login form.
        """
        self.login_dialog = LoginDialog(self)
        self.login_dialog.setup(self.controller)
        self.login_dialog.reset()
        self.login_dialog.exec()

    def show_login_error(self, error):
        """
        Display an error in the login dialog.
        """
        if self.login_dialog and error:
            self.login_dialog.error(error)

    def hide_login(self):
        """
        Kill the login dialog.
        """
        self.login_dialog.accept()
        self.login_dialog = None

    def update_error_status(self, error=None):
        """
        Show an error message on the sidebar.
        """
        self.main_view.update_error_status(error)

    def show_sources(self, sources):
        """
        Update the left hand sources list in the UI with the passed in list of
        sources.
        """
        self.main_view.source_list.update(sources)

    def show_sync(self, updated_on):
        """
        Display a message indicating the data-sync state.
        """
        if updated_on:
            self.main_view.status.setText('Last refresh: ' +
                                          updated_on.humanize())
        else:
            self.main_view.status.setText(_('Waiting to refresh...'))

    def set_logged_in_as(self, username):
        """
        Update the UI to show user logged in with username.
        """
        self.tool_bar.set_logged_in_as(username)

    def logout(self):
        """
        Update the UI to show the user is logged out.
        """
        self.tool_bar.set_logged_out()

    def on_source_changed(self):
        """
        React to when the selected source has changed.
        """
        source_item = self.main_view.source_list.currentItem()
        source_widget = self.main_view.source_list.itemWidget(source_item)
        if source_widget:
            self.current_source = source_widget.source
            self.show_conversation_for(self.current_source)

    def add_item_content_or(self, adder, item, default):
        """
        Private helper function to add correct message to conversation widgets
        """
        if item.is_downloaded is False:
            adder(default)
        else:
            adder(item.content)

    def show_conversation_for(self, source):
        """
        Show conversation of messages and replies between a source and
        journalists.
        """
        conversation = ConversationView(self)
        conversation.setup(self.controller)
        conversation.add_message('Source name: {}'.format(
            source.journalist_designation))

        # Display each conversation item in the source collection.
        for conversation_item in source.collection:

            if conversation_item.filename.endswith('msg.gpg'):
                self.add_item_content_or(conversation.add_message,
                                         conversation_item,
                                         "<Message not yet downloaded>")
            elif conversation_item.filename.endswith('reply.gpg'):
                self.add_item_content_or(conversation.add_reply,
                                         conversation_item,
                                         "<Reply not yet downloaded>")
            else:
                conversation.add_file(source, conversation_item)

        self.main_view.update_view(conversation)

    def set_status(self, message, duration=5000):
        """
        Display a status message to the user. Optionally, supply a duration
        (in milliseconds), the default value being a duration of 5 seconds.
        """
        self.status_bar.showMessage(message, duration)