From 090d5305e5134b99fcccce2d4602bbe1478d6d8f Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Tue, 21 Mar 2017 10:31:00 +0100 Subject: [PATCH] [feature][needs-docs] Master Password integration with OS password manager This PR adds (optional) synchronization of the master password with the OS password manager (AKA wallet/keychain). A set of new menu items has been added in the options -> authentication -> utilities to manage the new behavior. Notifications are handled by the message bar unless the password r/w operation is triggered from a modal dialog, in this case the notifications will be routed through the recently exposed QgisApp::showSystemNotification that uses the OS tray notifications. This new feature requires libqt5keychain, and was tested with v. 0.5+ --- CMakeLists.txt | 2 + ci/travis/linux/before_install.sh | 2 +- cmake/FindQtKeychain.cmake | 51 +++++ python/CMakeLists.txt | 1 + python/core/auth/qgsauthmanager.sip | 24 ++- src/app/CMakeLists.txt | 1 + src/app/qgisapp.cpp | 17 +- src/auth/basic/CMakeLists.txt | 1 + src/auth/identcert/CMakeLists.txt | 1 + src/auth/pkipaths/CMakeLists.txt | 1 + src/auth/pkipkcs12/CMakeLists.txt | 1 + src/core/CMakeLists.txt | 3 +- src/core/auth/qgsauthmanager.cpp | 272 +++++++++++++++++++++++++- src/core/auth/qgsauthmanager.h | 110 ++++++++++- src/gui/CMakeLists.txt | 1 + src/gui/auth/qgsautheditorwidgets.cpp | 45 +++++ src/gui/auth/qgsautheditorwidgets.h | 16 ++ src/gui/auth/qgsauthguiutils.cpp | 70 +++++++ src/gui/auth/qgsauthguiutils.h | 13 ++ src/gui/qgscredentialdialog.cpp | 12 +- src/providers/postgres/CMakeLists.txt | 1 + src/providers/wcs/CMakeLists.txt | 1 + src/providers/wfs/CMakeLists.txt | 1 + src/providers/wms/CMakeLists.txt | 1 + src/server/CMakeLists.txt | 1 + src/ui/qgscredentialdialog.ui | 11 +- tests/src/app/CMakeLists.txt | 1 + tests/src/core/CMakeLists.txt | 1 + tests/src/core/testqgsauthmanager.cpp | 68 ++++++- tests/src/gui/CMakeLists.txt | 1 + tests/src/providers/CMakeLists.txt | 1 + 31 files changed, 714 insertions(+), 18 deletions(-) create mode 100644 cmake/FindQtKeychain.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index e1ce0906cb33..1eebc4b0fc5e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -278,6 +278,8 @@ ENDIF (WITH_QTMOBILITY) # search for QScintilla2 (C++ lib) FIND_PACKAGE(QScintilla REQUIRED) +# Password helper +FIND_PACKAGE(QtKeychain REQUIRED) # Master password hash and authentication encryption FIND_PACKAGE(QCA REQUIRED) # Check for runtime dependency of qca-ossl plugin diff --git a/ci/travis/linux/before_install.sh b/ci/travis/linux/before_install.sh index f18d3e661e19..cc7c7aef7f2f 100755 --- a/ci/travis/linux/before_install.sh +++ b/ci/travis/linux/before_install.sh @@ -22,7 +22,7 @@ pushd ${HOME} # fetching data from github should be just as fast as S3 -curl -s -S -L https://github.com/opengisch/osgeo4travis/archive/qt5bin.tar.gz | tar --strip-components=1 -xz -C /home/travis & +curl -s -S -L https://github.com/opengisch/osgeo4travis/archive/qt55bin.tar.gz | tar --strip-components=1 -xz -C /home/travis & SETUP_OSGEO4W_PID=$! mkdir /home/travis/osgeo4travis diff --git a/cmake/FindQtKeychain.cmake b/cmake/FindQtKeychain.cmake new file mode 100644 index 000000000000..c2b55fc4a696 --- /dev/null +++ b/cmake/FindQtKeychain.cmake @@ -0,0 +1,51 @@ +# Find QtKeychain +# ~~~~~~~~~~~~~~~ +# Copyright (c) 2016, Boundless Spatial +# Author: Larry Shaffer +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. +# +# CMake module to search for QtKeychain library from: +# https://github.com/frankosterfeld/qtkeychain +# +# If it's found it sets QTKEYCHAIN_FOUND to TRUE +# and following variables are set: +# QTKEYCHAIN_INCLUDE_DIR +# QTKEYCHAIN_LIBRARY + +FIND_PATH(QTKEYCHAIN_INCLUDE_DIR keychain.h + PATHS + ${LIB_DIR}/include + "$ENV{LIB_DIR}/include" + $ENV{INCLUDE} + /usr/local/include + /usr/include + PATH_SUFFIXES qt5keychain qtkeychain +) + +FIND_LIBRARY(QTKEYCHAIN_LIBRARY NAMES qt5keychain qtkeychain + PATHS + ${LIB_DIR} + "$ENV{LIB_DIR}" + $ENV{LIB} + /usr/local/lib + /usr/lib +) + + +IF (QTKEYCHAIN_INCLUDE_DIR AND QTKEYCHAIN_LIBRARY) + SET(QTKEYCHAIN_FOUND TRUE) +ELSE() + SET(QTKEYCHAIN_FOUND FALSE) +ENDIF (QTKEYCHAIN_INCLUDE_DIR AND QTKEYCHAIN_LIBRARY) + +IF (QTKEYCHAIN_FOUND) + IF (NOT QTKEYCHAIN_FIND_QUIETLY) + MESSAGE(STATUS "Found QtKeychain: ${QTKEYCHAIN_LIBRARY}") + ENDIF (NOT QTKEYCHAIN_FIND_QUIETLY) +ELSE (QTKEYCHAIN_FOUND) + IF (QTKEYCHAIN_FIND_REQUIRED) + MESSAGE(FATAL_ERROR "Could not find QtKeychain") + ENDIF (QTKEYCHAIN_FIND_REQUIRED) +ENDIF (QTKEYCHAIN_FOUND) diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 52b092ffe52c..6f5da3bd70c9 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -89,6 +89,7 @@ INCLUDE_DIRECTORIES(SYSTEM ${QEXTSERIALPORT_INCLUDE_DIR} ${QSCINTILLA_INCLUDE_DIR} ${QCA_INCLUDE_DIR} + ${QTKEYCHAIN_INCLUDE_DIR} ${SQLITE3_INCLUDE_DIR} ) INCLUDE_DIRECTORIES( diff --git a/python/core/auth/qgsauthmanager.sip b/python/core/auth/qgsauthmanager.sip index 0e76112718df..32951fed8396 100644 --- a/python/core/auth/qgsauthmanager.sip +++ b/python/core/auth/qgsauthmanager.sip @@ -442,6 +442,19 @@ class QgsAuthManager : QObject QMutex *mutex(); signals: + + /** + * Signals emitted on password helper failure, + * mainly used in the tests to exit main application loop + */ + void passwordHelperFailure(); + + /** + * Signals emitted on password helper success, + * mainly used in the tests to exit main application loop + */ + void passwordHelperSuccess(); + /** * Custom logging signal to relay to console output and QgsMessageLog * @see QgsMessageLog @@ -449,7 +462,16 @@ class QgsAuthManager : QObject * @param tag Associated tag (title) * @param level Message log level */ - void messageOut( const QString& message, const QString& tag, QgsAuthManager::MessageLevel level = INFO ) const; + void messageOut( const QString& message, const QString& tag = QgsAuthManager::AUTH_MAN_TAG, QgsAuthManager::MessageLevel level = INFO ) const; + + /** + * Custom logging signal to inform the user about master password <-> password manager interactions + * @see QgsMessageLog + * @param message Message to send + * @param tag Associated tag (title) + * @param level Message log level + */ + void passwordHelperMessageOut( const QString &message, const QString &tag = QgsAuthManager::AUTH_MAN_TAG, QgsAuthManager::MessageLevel level = INFO ) const; /** * Emitted when a password has been verify (or not) diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 242f2bd76181..ea4233248a0c 100755 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -578,6 +578,7 @@ INCLUDE_DIRECTORIES(SYSTEM ${GDAL_INCLUDE_DIR} ${QWTPOLAR_INCLUDE_DIR} ${QCA_INCLUDE_DIR} + ${QTKEYCHAIN_INCLUDE_DIR} ) IF(ENABLE_MODELTEST) diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 5bd797c58fb5..62bcef8e6923 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -12254,6 +12254,8 @@ void QgisApp::masterPasswordSetup() { connect( QgsAuthManager::instance(), &QgsAuthManager::messageOut, this, &QgisApp::authMessageOut ); + connect( QgsAuthManager::instance(), &QgsAuthManager::passwordHelperMessageOut, + this, &QgisApp::authMessageOut ); connect( QgsAuthManager::instance(), &QgsAuthManager::authDatabaseEraseRequested, this, &QgisApp::eraseAuthenticationDatabase ); } @@ -12289,12 +12291,17 @@ void QgisApp::eraseAuthenticationDatabase() void QgisApp::authMessageOut( const QString &message, const QString &authtag, QgsAuthManager::MessageLevel level ) { - // only if main window is active window + // Use system notifications if the main window is not the active one, + // push message to the message bar if the main window is active if ( qApp->activeWindow() != this ) - return; - - int levelint = static_cast< int >( level ); - messageBar()->pushMessage( authtag, message, static_cast< QgsMessageBar::MessageLevel >( levelint ), 7 ); + { + showSystemNotification( tr( "QGIS Authentication" ), message ); + } + else + { + int levelint = static_cast< int >( level ); + messageBar()->pushMessage( authtag, message, static_cast< QgsMessageBar::MessageLevel >( levelint ), 7 ); + } } void QgisApp::completeInitialization() diff --git a/src/auth/basic/CMakeLists.txt b/src/auth/basic/CMakeLists.txt index cca6c9d43a4d..889346993066 100644 --- a/src/auth/basic/CMakeLists.txt +++ b/src/auth/basic/CMakeLists.txt @@ -24,6 +24,7 @@ INCLUDE_DIRECTORIES ( ) INCLUDE_DIRECTORIES (SYSTEM ${QCA_INCLUDE_DIR} + ${QTKEYCHAIN_INCLUDE_DIR} ) INCLUDE_DIRECTORIES ( ../../gui diff --git a/src/auth/identcert/CMakeLists.txt b/src/auth/identcert/CMakeLists.txt index 0819efd5014c..9391efdec58e 100644 --- a/src/auth/identcert/CMakeLists.txt +++ b/src/auth/identcert/CMakeLists.txt @@ -24,6 +24,7 @@ INCLUDE_DIRECTORIES ( ) INCLUDE_DIRECTORIES (SYSTEM ${QCA_INCLUDE_DIR} + ${QTKEYCHAIN_INCLUDE_DIR} ) INCLUDE_DIRECTORIES ( ../../gui diff --git a/src/auth/pkipaths/CMakeLists.txt b/src/auth/pkipaths/CMakeLists.txt index 093078f347a6..e7c33bad30eb 100644 --- a/src/auth/pkipaths/CMakeLists.txt +++ b/src/auth/pkipaths/CMakeLists.txt @@ -24,6 +24,7 @@ INCLUDE_DIRECTORIES ( ) INCLUDE_DIRECTORIES (SYSTEM ${QCA_INCLUDE_DIR} + ${QTKEYCHAIN_INCLUDE_DIR} ) INCLUDE_DIRECTORIES ( ../../gui diff --git a/src/auth/pkipkcs12/CMakeLists.txt b/src/auth/pkipkcs12/CMakeLists.txt index 39009b860b2f..d1ed476acb9b 100644 --- a/src/auth/pkipkcs12/CMakeLists.txt +++ b/src/auth/pkipkcs12/CMakeLists.txt @@ -24,6 +24,7 @@ INCLUDE_DIRECTORIES ( ) INCLUDE_DIRECTORIES (SYSTEM ${QCA_INCLUDE_DIR} + ${QTKEYCHAIN_INCLUDE_DIR} ) INCLUDE_DIRECTORIES ( ../../gui diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index afd9c459a785..ad02afbefc55 100755 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -980,6 +980,7 @@ INCLUDE_DIRECTORIES(SYSTEM ${SQLITE3_INCLUDE_DIR} ${SPATIALITE_INCLUDE_DIR} ${QCA_INCLUDE_DIR} + ${QTKEYCHAIN_INCLUDE_DIR} ) #for PAL classes @@ -1071,7 +1072,7 @@ TARGET_LINK_LIBRARIES(qgis_core ${OPTIONAL_QTWEBKIT} ${QT_QTSQL_LIBRARY} ${QCA_LIBRARY} - + ${QTKEYCHAIN_LIBRARY} ${PROJ_LIBRARY} ${GEOS_LIBRARY} ${GDAL_LIBRARY} diff --git a/src/core/auth/qgsauthmanager.cpp b/src/core/auth/qgsauthmanager.cpp index 9e4efd810748..cd2673f6d677 100644 --- a/src/core/auth/qgsauthmanager.cpp +++ b/src/core/auth/qgsauthmanager.cpp @@ -37,6 +37,10 @@ #include #endif +// QtKeyChain library +#include "keychain.h" + +// QGIS includes #include "qgsapplication.h" #include "qgsauthcertutils.h" #include "qgsauthcrypto.h" @@ -45,6 +49,8 @@ #include "qgsauthmethodregistry.h" #include "qgscredentials.h" #include "qgslogger.h" +#include "qgsmessagelog.h" +#include "qgssettings.h" QgsAuthManager *QgsAuthManager::sInstance = nullptr; @@ -59,6 +65,24 @@ const QString QgsAuthManager::AUTH_MAN_TAG = QObject::tr( "Authentication Manage const QString QgsAuthManager::AUTH_CFG_REGEX = QStringLiteral( "authcfg=([a-z]|[A-Z]|[0-9]){7}" ); +const QLatin1String QgsAuthManager::AUTH_PASSWORD_HELPER_KEY_NAME( "QGIS-Master-Password" ); +const QLatin1String QgsAuthManager::AUTH_PASSWORD_HELPER_FOLDER_NAME( "QGIS" ); + +#if defined(Q_OS_MAC) +const QString QgsAuthManager::AUTH_PASSWORD_HELPER_DISPLAY_NAME( "Keychain" ); +static const QString sDescription = QObject::tr( "Master Password <-> KeyChain storage plugin. Store and retrieve your master password in your KeyChain" ); +#elif defined(Q_OS_WIN) +const QString QgsAuthManager::AUTH_PASSWORD_HELPER_DISPLAY_NAME( "Password Manager" ); +static const QString sDescription = QObject::tr( "Master Password <-> Password Manager storage plugin. Store and retrieve your master password in your Password Manager" ); +#elif defined(Q_OS_LINUX) +const QString QgsAuthManager::AUTH_PASSWORD_HELPER_DISPLAY_NAME( "Wallet/KeyRing" ); +static const QString sDescription = QObject::tr( "Master Password <-> Wallet/KeyRing storage plugin. Store and retrieve your master password in your Wallet/KeyRing" ); +#else +const QString QgsAuthManager::AUTH_PASSWORD_HELPER_DISPLAY_NAME( "Password Manager" ); +static const QString sDescription = QObject::tr( "Master Password <-> KeyChain storage plugin. Store and retrieve your master password in your Wallet/KeyChain/Password Manager" ); +#endif + + QgsAuthManager *QgsAuthManager::instance() { if ( !sInstance ) @@ -2719,6 +2743,15 @@ const QByteArray QgsAuthManager::getTrustedCaCertsPemText() return capem; } +bool QgsAuthManager::passwordHelperSync() +{ + if ( masterPasswordIsSet( ) ) + { + return passwordHelperWrite( mMasterPass ); + } + return false; +} + ////////////////// Certificate calls - end /////////////////////// @@ -2821,6 +2854,12 @@ QgsAuthManager::QgsAuthManager() , mScheduledDbEraseRequestCount( 0 ) , mMutex( nullptr ) , mIgnoredSslErrorsCache( QHash >() ) + , mPasswordHelperVerificationError( false ) + , mPasswordHelperErrorMessage( "" ) + , mPasswordHelperErrorCode( QKeychain::NoError ) + , mPasswordHelperLoggingEnabled( false ) + , mPasswordHelperFailedInit( false ) + { mMutex = new QMutex( QMutex::Recursive ); connect( this, &QgsAuthManager::messageOut, @@ -2847,20 +2886,243 @@ QgsAuthManager::~QgsAuthManager() QSqlDatabase::removeDatabase( QStringLiteral( "authentication.configs" ) ); } + +QString QgsAuthManager::passwordHelperName() const +{ + return tr( "Password Helper" ); +} + + +void QgsAuthManager::passwordHelperLog( const QString &msg ) const +{ + if ( passwordHelperLoggingEnabled( ) ) + { + QgsMessageLog::logMessage( msg, passwordHelperName() ); + } +} + +bool QgsAuthManager::passwordHelperDelete() +{ + passwordHelperLog( tr( "Opening %1 for DELETE ..." ).arg( AUTH_PASSWORD_HELPER_DISPLAY_NAME ) ); + bool result; + QKeychain::DeletePasswordJob job( AUTH_PASSWORD_HELPER_FOLDER_NAME ); + QgsSettings settings; + job.setInsecureFallback( settings.value( QStringLiteral( "password_helper_insecure_fallback" ), false, QgsSettings::Section::Auth ).toBool( ) ); + job.setAutoDelete( false ); + job.setKey( AUTH_PASSWORD_HELPER_KEY_NAME ); + QEventLoop loop; + job.connect( &job, SIGNAL( finished( QKeychain::Job * ) ), &loop, SLOT( quit() ) ); + job.start(); + loop.exec(); + if ( job.error() ) + { + mPasswordHelperErrorCode = job.error(); + mPasswordHelperErrorMessage = tr( "Delete password failed: %1." ).arg( job.errorString() ); + // Signals used in the tests to exit main application loop + emit passwordHelperFailure(); + result = false; + } + else + { + // Signals used in the tests to exit main application loop + emit passwordHelperSuccess(); + result = true; + } + passwordHelperProcessError(); + return result; +} + +QString QgsAuthManager::passwordHelperRead() +{ + // Retrieve it! + QString password( "" ); + passwordHelperLog( tr( "Opening %1 for READ ..." ).arg( AUTH_PASSWORD_HELPER_DISPLAY_NAME ) ); + QKeychain::ReadPasswordJob job( AUTH_PASSWORD_HELPER_FOLDER_NAME ); + QgsSettings settings; + job.setInsecureFallback( settings.value( QStringLiteral( "password_helper_insecure_fallback" ), false, QgsSettings::Section::Auth ).toBool( ) ); + job.setAutoDelete( false ); + job.setKey( AUTH_PASSWORD_HELPER_KEY_NAME ); + QEventLoop loop; + job.connect( &job, SIGNAL( finished( QKeychain::Job * ) ), &loop, SLOT( quit() ) ); + job.start(); + loop.exec(); + if ( job.error() ) + { + mPasswordHelperErrorCode = job.error(); + mPasswordHelperErrorMessage = tr( "Retrieving password from your %1 failed: %2." ).arg( AUTH_PASSWORD_HELPER_DISPLAY_NAME, job.errorString() ); + // Signals used in the tests to exit main application loop + emit passwordHelperFailure(); + } + else + { + password = job.textData(); + // Password is there but it is empty, treat it like if it was not found + if ( password.isEmpty() ) + { + mPasswordHelperErrorCode = QKeychain::EntryNotFound; + mPasswordHelperErrorMessage = tr( "Empty password retrieved from your %1." ).arg( AUTH_PASSWORD_HELPER_DISPLAY_NAME ); + // Signals used in the tests to exit main application loop + emit passwordHelperFailure(); + } + else + { + // Signals used in the tests to exit main application loop + emit passwordHelperSuccess(); + } + } + passwordHelperProcessError(); + return password; +} + +bool QgsAuthManager::passwordHelperWrite( const QString &password ) +{ + Q_ASSERT( !password.isEmpty() ); + bool result; + passwordHelperLog( tr( "Opening %1 for WRITE ..." ).arg( AUTH_PASSWORD_HELPER_DISPLAY_NAME ) ); + QKeychain::WritePasswordJob job( AUTH_PASSWORD_HELPER_FOLDER_NAME ); + QgsSettings settings; + job.setInsecureFallback( settings.value( QStringLiteral( "password_helper_insecure_fallback" ), false, QgsSettings::Section::Auth ).toBool( ) ); + job.setAutoDelete( false ); + job.setKey( AUTH_PASSWORD_HELPER_KEY_NAME ); + job.setTextData( password ); + QEventLoop loop; + job.connect( &job, SIGNAL( finished( QKeychain::Job * ) ), &loop, SLOT( quit() ) ); + job.start(); + loop.exec(); + if ( job.error() ) + { + mPasswordHelperErrorCode = job.error(); + mPasswordHelperErrorMessage = tr( "Storing password in your %1 failed: %2." ).arg( AUTH_PASSWORD_HELPER_DISPLAY_NAME, job.errorString() ); + // Signals used in the tests to exit main application loop + emit passwordHelperFailure(); + result = false; + } + else + { + passwordHelperClearErrors(); + // Signals used in the tests to exit main application loop + emit passwordHelperSuccess(); + result = true; + } + passwordHelperProcessError( ); + return result; +} + +bool QgsAuthManager::passwordHelperEnabled() const +{ + // Does the user want to store the password in the wallet? + QgsSettings settings; + return settings.value( QStringLiteral( "use_password_helper" ), true, QgsSettings::Section::Auth ).toBool( ); +} + +void QgsAuthManager::setPasswordHelperEnabled( const bool enabled ) +{ + QgsSettings settings; + settings.setValue( QStringLiteral( "use_password_helper" ), enabled, QgsSettings::Section::Auth ); + emit messageOut( enabled ? tr( "Your %1 will be used from now on to store and retrieve the master password." ) + .arg( AUTH_PASSWORD_HELPER_DISPLAY_NAME ) : + tr( "Your %1 will not be used anymore to store and retrieve the master password." ) + .arg( AUTH_PASSWORD_HELPER_DISPLAY_NAME ) ); +} + +bool QgsAuthManager::passwordHelperLoggingEnabled() const +{ + // Does the user want to store the password in the wallet? + QgsSettings settings; + return settings.value( QStringLiteral( "password_helper_logging" ), false, QgsSettings::Section::Auth ).toBool( ); +} + +void QgsAuthManager::setPasswordHelperLoggingEnabled( const bool enabled ) +{ + QgsSettings settings; + settings.setValue( QStringLiteral( "password_helper_logging" ), enabled, QgsSettings::Section::Auth ); +} + +void QgsAuthManager::passwordHelperClearErrors() +{ + mPasswordHelperErrorCode = QKeychain::NoError; + mPasswordHelperErrorMessage = ""; +} + +void QgsAuthManager::passwordHelperProcessError() +{ + if ( mPasswordHelperErrorCode == QKeychain::AccessDenied || + mPasswordHelperErrorCode == QKeychain::AccessDeniedByUser || + mPasswordHelperErrorCode == QKeychain::NoBackendAvailable || + mPasswordHelperErrorCode == QKeychain::NotImplemented ) + { + // If the error is permanent or the user denied access to the wallet + // we also want to disable the wallet system to prevent annoying + // notification on each subsequent access. + setPasswordHelperEnabled( false ); + mPasswordHelperErrorMessage = tr( "There was an error and integration with your %1 system has been disabled. " + "You can re-enable it at any time through the \"Utilities\" menu " + "in the Authentication pane of the options dialog. %2" ) + .arg( AUTH_PASSWORD_HELPER_DISPLAY_NAME ) + .arg( mPasswordHelperErrorMessage ); + } + if ( mPasswordHelperErrorCode != QKeychain::NoError ) + { + // We've got an error from the wallet + passwordHelperLog( tr( "Error in %1: %2" ).arg( AUTH_PASSWORD_HELPER_DISPLAY_NAME, mPasswordHelperErrorMessage ) ); + emit passwordHelperMessageOut( mPasswordHelperErrorMessage, authManTag(), CRITICAL ); + } + passwordHelperClearErrors(); +} + + bool QgsAuthManager::masterPasswordInput() { if ( isDisabled() ) return false; QString pass; - QgsCredentials *creds = QgsCredentials::instance(); - creds->lock(); - bool ok = creds->getMasterPassword( pass, masterPasswordHashInDatabase() ); - creds->unlock(); + bool storedPasswordIsValid = false; + bool ok = false; - if ( ok && !pass.isEmpty() && !masterPasswordSame( pass ) ) + // Read the password from the wallet + if ( passwordHelperEnabled( ) ) + { + pass = passwordHelperRead( ); + if ( ! pass.isEmpty( ) && ( mPasswordHelperErrorCode == QKeychain::NoError ) ) + { + // Let's check the password! + if ( verifyMasterPassword( pass ) ) + { + ok = true; + storedPasswordIsValid = true; + emit passwordHelperMessageOut( tr( "Master password has been successfully read from your %1" ).arg( AUTH_PASSWORD_HELPER_DISPLAY_NAME ), authManTag(), INFO ); + } + else + { + emit passwordHelperMessageOut( tr( "Master password stored in your %1 is not valid" ).arg( AUTH_PASSWORD_HELPER_DISPLAY_NAME ), authManTag(), WARNING ); + } + } + } + + if ( ! ok ) + { + QgsCredentials *creds = QgsCredentials::instance(); + creds->lock(); + pass.clear(); + ok = creds->getMasterPassword( pass, masterPasswordHashInDatabase() ); + creds->unlock(); + } + + if ( ok && !pass.isEmpty() && mMasterPass != pass ) { mMasterPass = pass; + if ( passwordHelperEnabled( ) && ! storedPasswordIsValid ) + { + if ( passwordHelperWrite( pass ) ) + { + emit passwordHelperMessageOut( tr( "Master password has been successfully written to your %1" ).arg( AUTH_PASSWORD_HELPER_DISPLAY_NAME ), authManTag(), INFO ); + } + else + { + emit passwordHelperMessageOut( tr( "Master password could not be written to your %1" ).arg( AUTH_PASSWORD_HELPER_DISPLAY_NAME ), authManTag(), WARNING ); + } + } return true; } return false; diff --git a/src/core/auth/qgsauthmanager.h b/src/core/auth/qgsauthmanager.h index 6f54e5a26750..c4db2192b9ca 100644 --- a/src/core/auth/qgsauthmanager.h +++ b/src/core/auth/qgsauthmanager.h @@ -37,6 +37,9 @@ #include "qgsauthconfig.h" #include "qgsauthmethod.h" +// Qt5KeyChain library +#include "keychain.h" + namespace QCA { class Initializer; @@ -496,8 +499,54 @@ class CORE_EXPORT QgsAuthManager : public QObject //! Return pointer to mutex QMutex *mutex() { return mMutex; } + //! Error message getter + //! @note not available in Python bindings + const QString passwordHelperErrorMessage() { return mPasswordHelperErrorMessage; } + + //! Delete master password from wallet + //! @note not available in Python bindings + bool passwordHelperDelete(); + + //! Password helper enabled getter + //! @note not available in Python bindings + bool passwordHelperEnabled() const; + + //! Password helper enabled setter + //! @note not available in Python bindings + void setPasswordHelperEnabled( const bool enabled ); + + //! Password helper logging enabled getter + //! @note not available in Python bindings + bool passwordHelperLoggingEnabled() const; + + //! Password helper logging enabled setter + //! @note not available in Python bindings + void setPasswordHelperLoggingEnabled( const bool enabled ); + + //! Store the password manager into the wallet + //! @note not available in Python bindings + bool passwordHelperSync( ); + + //! The display name of the password helper (platform dependent) + static const QString AUTH_PASSWORD_HELPER_DISPLAY_NAME; + + //! The display name of the Authentication Manager + static const QString AUTH_MAN_TAG; + signals: + /** + * Signals emitted on password helper failure, + * mainly used in the tests to exit main application loop + */ + void passwordHelperFailure(); + + /** + * Signals emitted on password helper success, + * mainly used in the tests to exit main application loop + */ + void passwordHelperSuccess(); + /** * Custom logging signal to relay to console output and QgsMessageLog * \see QgsMessageLog @@ -507,6 +556,16 @@ class CORE_EXPORT QgsAuthManager : public QObject */ void messageOut( const QString &message, const QString &tag = AUTH_MAN_TAG, QgsAuthManager::MessageLevel level = INFO ) const; + /** + * Custom logging signal to inform the user about master password <-> password manager interactions + * @see QgsMessageLog + * @param message Message to send + * @param tag Associated tag (title) + * @param level Message log level + */ + void passwordHelperMessageOut( const QString &message, const QString &tag = AUTH_MAN_TAG, QgsAuthManager::MessageLevel level = INFO ) const; + + /** * Emitted when a password has been verify (or not) * \param verified The state of password's verification @@ -544,6 +603,31 @@ class CORE_EXPORT QgsAuthManager : public QObject private: + ////////////////////////////////////////////////////////////////////////////// + // Password Helper methods + + //! Return name for logging + QString passwordHelperName() const; + + //! Print a debug message in QGIS + void passwordHelperLog( const QString &msg ) const; + + //! Read Master password from the wallet + QString passwordHelperRead(); + + //! Store Master password in the wallet + bool passwordHelperWrite( const QString &password ); + + //! Error message setter + void passwordHelperSetErrorMessage( const QString errorMessage ) { mPasswordHelperErrorMessage = errorMessage; } + + //! Clear error code and message + void passwordHelperClearErrors(); + + //! Process the error: show it and/or disable the password helper system in case of + //! access denied or no backend, reset error flags at the end + void passwordHelperProcessError(); + bool createConfigTables(); bool createCertTables(); @@ -604,7 +688,6 @@ class CORE_EXPORT QgsAuthManager : public QObject static const QString AUTH_SERVERS_TABLE; static const QString AUTH_AUTHORITIES_TABLE; static const QString AUTH_TRUST_TABLE; - static const QString AUTH_MAN_TAG; static const QString AUTH_CFG_REGEX; bool mAuthInit; @@ -637,6 +720,31 @@ class CORE_EXPORT QgsAuthManager : public QObject // cache of SSL errors to be ignored in network connections, per sha-hostport QHash > mIgnoredSslErrorsCache; #endif + + ////////////////////////////////////////////////////////////////////////////// + // Password Helper Variables + + //! Master password verification has failed + bool mPasswordHelperVerificationError; + + //! Store last error message + QString mPasswordHelperErrorMessage; + + //! Store last error code (enum) + QKeychain::Error mPasswordHelperErrorCode; + + //! Enable logging + bool mPasswordHelperLoggingEnabled; + + //! Whether the keychain bridge failed to initialize + bool mPasswordHelperFailedInit; + + //! Master password name in the wallets + static const QLatin1String AUTH_PASSWORD_HELPER_KEY_NAME; + + //! password helper folder in the wallets + static const QLatin1String AUTH_PASSWORD_HELPER_FOLDER_NAME; + }; #endif // QGSAUTHMANAGER_H diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index ff09a264877b..0296d731ec54 100755 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -762,6 +762,7 @@ INCLUDE_DIRECTORIES( ) INCLUDE_DIRECTORIES(SYSTEM ${QCA_INCLUDE_DIR} + ${QTKEYCHAIN_INCLUDE_DIR} ${QWT_INCLUDE_DIR} ${SQLITE3_INCLUDE_DIR} ${QSCINTILLA_INCLUDE_DIR} diff --git a/src/gui/auth/qgsautheditorwidgets.cpp b/src/gui/auth/qgsautheditorwidgets.cpp index 13b49b700ee9..f91155d118ac 100644 --- a/src/gui/auth/qgsautheditorwidgets.cpp +++ b/src/gui/auth/qgsautheditorwidgets.cpp @@ -152,6 +152,20 @@ void QgsAuthEditorWidgets::setupUtilitiesMenu() mActionRemoveAuthConfigs = new QAction( QStringLiteral( "Remove all authentication configurations" ), this ); mActionEraseAuthDatabase = new QAction( QStringLiteral( "Erase authentication database" ), this ); + mActionPasswordHelperSync = new QAction( tr( "Store/update the master password in your %1" ) + .arg( QgsAuthManager::AUTH_PASSWORD_HELPER_DISPLAY_NAME ), this ); + mActionPasswordHelperDelete = new QAction( tr( "Clear the master password from your %1" ) + .arg( QgsAuthManager::AUTH_PASSWORD_HELPER_DISPLAY_NAME ), this ); + mActionPasswordHelperEnable = new QAction( tr( "Integrate master password with your %1" ) + .arg( QgsAuthManager::AUTH_PASSWORD_HELPER_DISPLAY_NAME ), this ); + mActionPasswordHelperLoggingEnable = new QAction( tr( "Enable password helper debug log" ), this ); + + mActionPasswordHelperEnable->setCheckable( true ); + mActionPasswordHelperEnable->setChecked( QgsAuthManager::instance()->passwordHelperEnabled( ) ); + + mActionPasswordHelperLoggingEnable->setCheckable( true ); + mActionPasswordHelperLoggingEnable->setChecked( QgsAuthManager::instance()->passwordHelperLoggingEnabled( ) ); + connect( mActionSetMasterPassword, &QAction::triggered, this, &QgsAuthEditorWidgets::setMasterPassword ); connect( mActionClearCachedMasterPassword, &QAction::triggered, this, &QgsAuthEditorWidgets::clearCachedMasterPassword ); connect( mActionResetMasterPassword, &QAction::triggered, this, &QgsAuthEditorWidgets::resetMasterPassword ); @@ -159,11 +173,21 @@ void QgsAuthEditorWidgets::setupUtilitiesMenu() connect( mActionRemoveAuthConfigs, &QAction::triggered, this, &QgsAuthEditorWidgets::removeAuthenticationConfigs ); connect( mActionEraseAuthDatabase, &QAction::triggered, this, &QgsAuthEditorWidgets::eraseAuthenticationDatabase ); + connect( mActionPasswordHelperSync, &QAction::triggered, this, &QgsAuthEditorWidgets::passwordHelperSync ); + connect( mActionPasswordHelperDelete, &QAction::triggered, this, &QgsAuthEditorWidgets::passwordHelperDelete ); + connect( mActionPasswordHelperEnable, &QAction::triggered, this, &QgsAuthEditorWidgets::passwordHelperEnableTriggered ); + connect( mActionPasswordHelperLoggingEnable, &QAction::triggered, this, &QgsAuthEditorWidgets::passwordHelperLoggingEnableTriggered ); + mAuthUtilitiesMenu = new QMenu( this ); mAuthUtilitiesMenu->addAction( mActionSetMasterPassword ); mAuthUtilitiesMenu->addAction( mActionClearCachedMasterPassword ); mAuthUtilitiesMenu->addAction( mActionResetMasterPassword ); mAuthUtilitiesMenu->addSeparator(); + mAuthUtilitiesMenu->addAction( mActionPasswordHelperEnable ); + mAuthUtilitiesMenu->addAction( mActionPasswordHelperSync ); + mAuthUtilitiesMenu->addAction( mActionPasswordHelperDelete ); + mAuthUtilitiesMenu->addAction( mActionPasswordHelperLoggingEnable ); + mAuthUtilitiesMenu->addSeparator(); mAuthUtilitiesMenu->addAction( mActionClearCachedAuthConfigs ); mAuthUtilitiesMenu->addAction( mActionRemoveAuthConfigs ); mAuthUtilitiesMenu->addSeparator(); @@ -208,6 +232,27 @@ void QgsAuthEditorWidgets::authMessageOut( const QString &message, const QString messageBar()->pushMessage( authtag, message, ( QgsMessageBar::MessageLevel )levelint, 7 ); } +void QgsAuthEditorWidgets::passwordHelperDelete() +{ + QgsAuthGuiUtils::passwordHelperDelete( messageBar(), messageTimeout(), this ); +} + +void QgsAuthEditorWidgets::passwordHelperSync() +{ + QgsAuthGuiUtils::passwordHelperSync( messageBar(), messageTimeout() ); +} + +void QgsAuthEditorWidgets::passwordHelperEnableTriggered() +{ + // Only fire on real changes + QgsAuthGuiUtils::passwordHelperEnable( mActionPasswordHelperEnable->isChecked(), messageBar(), messageTimeout() ); +} + +void QgsAuthEditorWidgets::passwordHelperLoggingEnableTriggered() +{ + QgsAuthGuiUtils::passwordHelperLoggingEnable( mActionPasswordHelperLoggingEnable->isChecked(), messageBar(), messageTimeout() ); +} + QgsMessageBar *QgsAuthEditorWidgets::messageBar() { return mMsgBar; diff --git a/src/gui/auth/qgsautheditorwidgets.h b/src/gui/auth/qgsautheditorwidgets.h index 89f7ca6a123c..e2ac4daa1a71 100644 --- a/src/gui/auth/qgsautheditorwidgets.h +++ b/src/gui/auth/qgsautheditorwidgets.h @@ -88,6 +88,18 @@ class GUI_EXPORT QgsAuthEditorWidgets : public QWidget, private Ui::QgsAuthEdito //! Relay messages to widget's messagebar void authMessageOut( const QString &message, const QString &authtag, QgsAuthManager::MessageLevel level ); + //! Remove master password from wallet + void passwordHelperDelete( ); + + //! Store master password into the wallet + void passwordHelperSync( ); + + //! Toggle password helper (enable/disable) + void passwordHelperEnableTriggered( ); + + //! Toggle password helper logging (enable/disable) + void passwordHelperLoggingEnableTriggered( ); + private: void setupUtilitiesMenu(); @@ -101,6 +113,10 @@ class GUI_EXPORT QgsAuthEditorWidgets : public QWidget, private Ui::QgsAuthEdito QAction *mActionClearCachedAuthConfigs = nullptr; QAction *mActionRemoveAuthConfigs = nullptr; QAction *mActionEraseAuthDatabase = nullptr; + QAction *mActionPasswordHelperDelete = nullptr; + QAction *mActionPasswordHelperSync = nullptr; + QAction *mActionPasswordHelperEnable = nullptr; + QAction *mActionPasswordHelperLoggingEnable = nullptr; }; #endif // QGSAUTHEDITORWIDGETS_H diff --git a/src/gui/auth/qgsauthguiutils.cpp b/src/gui/auth/qgsauthguiutils.cpp index 9d141c3fb3f9..5ababdac6e69 100644 --- a/src/gui/auth/qgsauthguiutils.cpp +++ b/src/gui/auth/qgsauthguiutils.cpp @@ -260,3 +260,73 @@ QString QgsAuthGuiUtils::getOpenFileName( QWidget *parent, const QString &title, } return f; } + +void QgsAuthGuiUtils::passwordHelperDelete( QgsMessageBar *msgbar, int timeout, QWidget *parent ) +{ + if ( QMessageBox::warning( parent, + QObject::tr( "Delete confirmation" ), + QObject::tr( "Do you really want to delete the master password from your %1?" ) + .arg( QgsAuthManager::AUTH_PASSWORD_HELPER_DISPLAY_NAME ), + QMessageBox::Ok | QMessageBox::Cancel, + QMessageBox::Cancel ) == QMessageBox::Cancel ) + { + return; + } + QString msg; + QgsMessageBar::MessageLevel level; + if ( ! QgsAuthManager::instance()->passwordHelperDelete() ) + { + msg = QgsAuthManager::instance()->passwordHelperErrorMessage(); + level = QgsMessageBar::WARNING; + } + else + { + msg = QObject::tr( "Master password was successfully deleted from your %1" ) + .arg( QgsAuthManager::AUTH_PASSWORD_HELPER_DISPLAY_NAME ); + + level = QgsMessageBar::INFO; + } + msgbar->pushMessage( QObject::tr( "Password helper delete" ), msg, level, timeout ); +} + +void QgsAuthGuiUtils::passwordHelperSync( QgsMessageBar *msgbar, int timeout ) +{ + QString msg; + QgsMessageBar::MessageLevel level; + if ( ! QgsAuthManager::instance()->masterPasswordIsSet( ) ) + { + msg = QObject::tr( "Master password is not set and cannot be stored in your %1" ) + .arg( QgsAuthManager::AUTH_PASSWORD_HELPER_DISPLAY_NAME ); + level = QgsMessageBar::WARNING; + } + else if ( ! QgsAuthManager::instance()->passwordHelperSync() ) + { + msg = QgsAuthManager::instance()->passwordHelperErrorMessage(); + level = QgsMessageBar::WARNING; + } + else + { + msg = QObject::tr( "Master password has been successfully stored in your %1" ) + .arg( QgsAuthManager::AUTH_PASSWORD_HELPER_DISPLAY_NAME ); + + level = QgsMessageBar::INFO; + } + msgbar->pushMessage( QObject::tr( "Password helper write" ), msg, level, timeout ); +} + +void QgsAuthGuiUtils::passwordHelperEnable( bool enabled, QgsMessageBar *msgbar, int timeout ) +{ + QgsAuthManager::instance()->setPasswordHelperEnabled( enabled ); + QString msg = enabled ? QObject::tr( "Your %1 will be used from now on to store and retrieve the master password." ) + .arg( QgsAuthManager::AUTH_PASSWORD_HELPER_DISPLAY_NAME ) : + QObject::tr( "Your %1 will not be used anymore to store and retrieve the master password." ) + .arg( QgsAuthManager::AUTH_PASSWORD_HELPER_DISPLAY_NAME ); + msgbar->pushMessage( QObject::tr( "Password helper write" ), msg, QgsMessageBar::INFO, timeout ); +} + +void QgsAuthGuiUtils::passwordHelperLoggingEnable( bool enabled, QgsMessageBar *msgbar, int timeout ) +{ + Q_UNUSED( msgbar ); + Q_UNUSED( timeout ); + QgsAuthManager::instance()->setPasswordHelperLoggingEnabled( enabled ); +} diff --git a/src/gui/auth/qgsauthguiutils.h b/src/gui/auth/qgsauthguiutils.h index e1ad64e39673..8dd18bb9830d 100644 --- a/src/gui/auth/qgsauthguiutils.h +++ b/src/gui/auth/qgsauthguiutils.h @@ -80,6 +80,19 @@ class GUI_EXPORT QgsAuthGuiUtils //! Open file dialog for auth associated widgets static QString getOpenFileName( QWidget *parent, const QString &title, const QString &extfilter ); + + //! Remove master password from wallet + static void passwordHelperDelete( QgsMessageBar *msgbar, int timeout = 0, QWidget *parent = nullptr ); + + //! Store master password into the wallet + static void passwordHelperSync( QgsMessageBar *msgbar, int timeout = 0 ); + + //! Set password helper enabled (enable/disable) + static void passwordHelperEnable( bool enabled, QgsMessageBar *msgbar, int timeout = 0 ); + + //! Set password helper logging enabled (enable/disable) + static void passwordHelperLoggingEnable( bool enabled, QgsMessageBar *msgbar, int timeout = 0 ); + }; #endif // QGSAUTHGUIUTILS_H diff --git a/src/gui/qgscredentialdialog.cpp b/src/gui/qgscredentialdialog.cpp index 448760597307..201f63743193 100644 --- a/src/gui/qgscredentialdialog.cpp +++ b/src/gui/qgscredentialdialog.cpp @@ -19,9 +19,9 @@ #include "qgsauthmanager.h" #include "qgslogger.h" +#include "qgssettings.h" #include -#include #include static QString invalidStyle_( const QString &selector = QStringLiteral( "QLineEdit" ) ) @@ -43,6 +43,8 @@ QgsCredentialDialog::QgsCredentialDialog( QWidget *parent, Qt::WindowFlags fl ) Qt::BlockingQueuedConnection ); mOkButton = buttonBox->button( QDialogButtonBox::Ok ); leMasterPass->setPlaceholderText( tr( "Required" ) ); + chkbxPasswordHelperEnable->setText( tr( "Store/update the master password in your %1" ) + .arg( QgsAuthManager::AUTH_PASSWORD_HELPER_DISPLAY_NAME ) ); leUsername->setFocus(); } @@ -68,6 +70,7 @@ void QgsCredentialDialog::requestCredentials( const QString &realm, QString *use QgsDebugMsg( "Entering." ); stackedWidget->setCurrentIndex( 0 ); + chkbxPasswordHelperEnable->setChecked( QgsAuthManager::instance()->passwordHelperEnabled( ) ); labelRealm->setText( realm ); leUsername->setText( *username ); lePassword->setText( *password ); @@ -121,6 +124,8 @@ void QgsCredentialDialog::requestCredentialsMasterPassword( QString *password, b QString titletxt( stored ? tr( "Enter CURRENT master authentication password" ) : tr( "Set NEW master authentication password" ) ); lblPasswordTitle->setText( titletxt ); + chkbxPasswordHelperEnable->setChecked( QgsAuthManager::instance()->passwordHelperEnabled( ) ); + leMasterPassVerify->setVisible( !stored ); lblDontForget->setVisible( !stored ); @@ -170,6 +175,11 @@ void QgsCredentialDialog::requestCredentialsMasterPassword( QString *password, b else { *password = leMasterPass->text(); + // Let's store user's preferences to use the password helper + if ( chkbxPasswordHelperEnable->isChecked() != QgsAuthManager::instance()->passwordHelperEnabled( ) ) + { + QgsAuthManager::instance()->setPasswordHelperEnabled( chkbxPasswordHelperEnable->isChecked() ); + } } break; } diff --git a/src/providers/postgres/CMakeLists.txt b/src/providers/postgres/CMakeLists.txt index 601a1de3b092..8eefdd5e1a36 100644 --- a/src/providers/postgres/CMakeLists.txt +++ b/src/providers/postgres/CMakeLists.txt @@ -49,6 +49,7 @@ INCLUDE_DIRECTORIES(SYSTEM ${GEOS_INCLUDE_DIR} ${QSCINTILLA_INCLUDE_DIR} ${QCA_INCLUDE_DIR} + ${QTKEYCHAIN_INCLUDE_DIR} ) INCLUDE_DIRECTORIES( ../../core diff --git a/src/providers/wcs/CMakeLists.txt b/src/providers/wcs/CMakeLists.txt index 542fa59832b6..565ec17e28c8 100644 --- a/src/providers/wcs/CMakeLists.txt +++ b/src/providers/wcs/CMakeLists.txt @@ -32,6 +32,7 @@ INCLUDE_DIRECTORIES( INCLUDE_DIRECTORIES(SYSTEM ${GDAL_INCLUDE_DIR} ${QCA_INCLUDE_DIR} + ${QTKEYCHAIN_INCLUDE_DIR} ) ADD_LIBRARY(wcsprovider MODULE ${WCS_SRCS} ${WCS_MOC_SRCS}) diff --git a/src/providers/wfs/CMakeLists.txt b/src/providers/wfs/CMakeLists.txt index befa85fa96d4..ea11207254e3 100644 --- a/src/providers/wfs/CMakeLists.txt +++ b/src/providers/wfs/CMakeLists.txt @@ -53,6 +53,7 @@ INCLUDE_DIRECTORIES(SYSTEM ${EXPAT_INCLUDE_DIR} ${QSCINTILLA_INCLUDE_DIR} ${QCA_INCLUDE_DIR} + ${QTKEYCHAIN_INCLUDE_DIR} ${GDAL_INCLUDE_DIR} # needed by qgsvectorfilewriter.h ${SQLITE3_INCLUDE_DIR} ) diff --git a/src/providers/wms/CMakeLists.txt b/src/providers/wms/CMakeLists.txt index b2195812ff15..42a06896cdaf 100644 --- a/src/providers/wms/CMakeLists.txt +++ b/src/providers/wms/CMakeLists.txt @@ -43,6 +43,7 @@ INCLUDE_DIRECTORIES(SYSTEM ${GEOS_INCLUDE_DIR} ${QT_QTSCRIPT_INCLUDE_DIR} ${QCA_INCLUDE_DIR} + ${QTKEYCHAIN_INCLUDE_DIR} ) ADD_LIBRARY(wmsprovider_a STATIC ${WMS_SRCS} ${WMS_MOC_SRCS}) diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt index e111f01d711d..27d83941f80b 100644 --- a/src/server/CMakeLists.txt +++ b/src/server/CMakeLists.txt @@ -105,6 +105,7 @@ INCLUDE_DIRECTORIES(SYSTEM ${QT_INCLUDE_DIR} ${QGIS_INCLUDE_DIR} ${QCA_INCLUDE_DIR} + ${QTKEYCHAIN_INCLUDE_DIR} ) INCLUDE_DIRECTORIES( ${CMAKE_BINARY_DIR}/src/core diff --git a/src/ui/qgscredentialdialog.ui b/src/ui/qgscredentialdialog.ui index 1fd3f077ad20..c592cefe03b9 100644 --- a/src/ui/qgscredentialdialog.ui +++ b/src/ui/qgscredentialdialog.ui @@ -6,7 +6,7 @@ 0 0 - 277 + 396 289 @@ -30,7 +30,7 @@ - 0 + 1 @@ -154,6 +154,13 @@ + + + + Store master password in your password manager + + + diff --git a/tests/src/app/CMakeLists.txt b/tests/src/app/CMakeLists.txt index e0fd497770e5..b1470479a117 100644 --- a/tests/src/app/CMakeLists.txt +++ b/tests/src/app/CMakeLists.txt @@ -32,6 +32,7 @@ INCLUDE_DIRECTORIES(SYSTEM ${PROJ_INCLUDE_DIR} ${GEOS_INCLUDE_DIR} ${QCA_INCLUDE_DIR} + ${QTKEYCHAIN_INCLUDE_DIR} ${QSCINTILLA_INCLUDE_DIR} ) diff --git a/tests/src/core/CMakeLists.txt b/tests/src/core/CMakeLists.txt index 13a870f30cd5..b70b95d5b5b7 100644 --- a/tests/src/core/CMakeLists.txt +++ b/tests/src/core/CMakeLists.txt @@ -26,6 +26,7 @@ INCLUDE_DIRECTORIES(SYSTEM ${PROJ_INCLUDE_DIR} ${GEOS_INCLUDE_DIR} ${QCA_INCLUDE_DIR} + ${QTKEYCHAIN_INCLUDE_DIR} ${SQLITE3_INCLUDE_DIR} ) diff --git a/tests/src/core/testqgsauthmanager.cpp b/tests/src/core/testqgsauthmanager.cpp index c975eafb33ee..7d254f0af5f9 100644 --- a/tests/src/core/testqgsauthmanager.cpp +++ b/tests/src/core/testqgsauthmanager.cpp @@ -27,6 +27,7 @@ #include "qgsapplication.h" #include "qgsauthmanager.h" #include "qgsauthconfig.h" +#include "qgssettings.h" /** \ingroup UnitTests * Unit tests for QgsAuthManager @@ -38,15 +39,20 @@ class TestQgsAuthManager: public QObject public: TestQgsAuthManager(); + public slots: + + void doSync(); + private slots: void initTestCase(); void cleanupTestCase(); void init(); - void cleanup() {} + void cleanup(); void testMasterPassword(); void testAuthConfigs(); void testAuthMethods(); + void testPasswordHelper(); private: void cleanupTempDir(); @@ -129,6 +135,13 @@ void TestQgsAuthManager::initTestCase() // all tests should now have a valid qgis-auth.db and stored/set master password } +void TestQgsAuthManager::cleanup() +{ + // Restore password_helper_insecure_fallback value + QgsSettings settings; + settings.setValue( QStringLiteral( "password_helper_insecure_fallback" ), false, QgsSettings::Section::Auth ); +} + void TestQgsAuthManager::cleanupTempDir() { QDir tmpDir = QDir( mTempDir ); @@ -402,5 +415,58 @@ QList TestQgsAuthManager::registerAuthConfigs() return configs; } + +void TestQgsAuthManager::doSync() +{ + QgsAuthManager *authm = QgsAuthManager::instance(); + QVERIFY( authm->passwordHelperSync( ) ); +} + +void TestQgsAuthManager::testPasswordHelper() +{ + + QgsAuthManager *authm = QgsAuthManager::instance(); + authm->clearMasterPassword(); + + QgsSettings settings; + settings.setValue( QStringLiteral( "password_helper_insecure_fallback" ), true, QgsSettings::Section::Auth ); + + // Test enable/disable + // It should be enabled by default + QVERIFY( authm->passwordHelperEnabled() ); + authm->setPasswordHelperEnabled( false ); + QVERIFY( ! authm->passwordHelperEnabled() ); + authm->setPasswordHelperEnabled( true ); + QVERIFY( authm->passwordHelperEnabled() ); + + // Sync with wallet + QVERIFY( authm->setMasterPassword( mPass, true ) ); + QVERIFY( authm->masterPasswordIsSet( ) ); + QObject::connect( authm, &QgsAuthManager::passwordHelperSuccess, + QApplication::instance(), &QCoreApplication::quit ); + QObject::connect( authm, &QgsAuthManager::passwordHelperFailure, + QApplication::instance(), &QCoreApplication::quit ); + QMetaObject::invokeMethod( this, "doSync", Qt::QueuedConnection ); + qApp->exec(); + authm->clearMasterPassword(); + QVERIFY( authm->setMasterPassword( ) ); + QVERIFY( authm->masterPasswordIsSet( ) ); + + // Delete from wallet + authm->clearMasterPassword(); + QVERIFY( authm->passwordHelperDelete( ) ); + QVERIFY( ! authm->setMasterPassword( ) ); + QVERIFY( ! authm->masterPasswordIsSet( ) ); + + // Re-sync + QVERIFY( authm->setMasterPassword( mPass, true ) ); + QMetaObject::invokeMethod( this, "doSync", Qt::QueuedConnection ); + qApp->exec(); + authm->clearMasterPassword(); + QVERIFY( authm->setMasterPassword( ) ); + QVERIFY( authm->masterPasswordIsSet( ) ); + +} + QGSTEST_MAIN( TestQgsAuthManager ) #include "testqgsauthmanager.moc" diff --git a/tests/src/gui/CMakeLists.txt b/tests/src/gui/CMakeLists.txt index 0fe606a638c2..bf8b68cd226b 100644 --- a/tests/src/gui/CMakeLists.txt +++ b/tests/src/gui/CMakeLists.txt @@ -30,6 +30,7 @@ INCLUDE_DIRECTORIES(SYSTEM ${GEOS_INCLUDE_DIR} ${QWT_INCLUDE_DIR} ${QCA_INCLUDE_DIR} + ${QTKEYCHAIN_INCLUDE_DIR} ${QSCINTILLA_INCLUDE_DIR} ) diff --git a/tests/src/providers/CMakeLists.txt b/tests/src/providers/CMakeLists.txt index d5a6b46a22a7..2b6ebf3d932f 100644 --- a/tests/src/providers/CMakeLists.txt +++ b/tests/src/providers/CMakeLists.txt @@ -20,6 +20,7 @@ INCLUDE_DIRECTORIES(SYSTEM ${PROJ_INCLUDE_DIR} ${GEOS_INCLUDE_DIR} ${QCA_INCLUDE_DIR} + ${QTKEYCHAIN_INCLUDE_DIR} ${POSTGRES_INCLUDE_DIR} )