[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+
This commit is contained in:
Alessandro Pasotti 2017-03-21 10:31:00 +01:00
parent 669fa87eb4
commit 090d5305e5
31 changed files with 714 additions and 18 deletions

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,51 @@
# Find QtKeychain
# ~~~~~~~~~~~~~~~
# Copyright (c) 2016, Boundless Spatial
# Author: Larry Shaffer <lshaffer (at) boundlessgeo (dot) com>
#
# 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)

View File

@ -89,6 +89,7 @@ INCLUDE_DIRECTORIES(SYSTEM
${QEXTSERIALPORT_INCLUDE_DIR}
${QSCINTILLA_INCLUDE_DIR}
${QCA_INCLUDE_DIR}
${QTKEYCHAIN_INCLUDE_DIR}
${SQLITE3_INCLUDE_DIR}
)
INCLUDE_DIRECTORIES(

View File

@ -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)

View File

@ -578,6 +578,7 @@ INCLUDE_DIRECTORIES(SYSTEM
${GDAL_INCLUDE_DIR}
${QWTPOLAR_INCLUDE_DIR}
${QCA_INCLUDE_DIR}
${QTKEYCHAIN_INCLUDE_DIR}
)
IF(ENABLE_MODELTEST)

View File

@ -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,13 +12291,18 @@ 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;
{
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()
{

View File

@ -24,6 +24,7 @@ INCLUDE_DIRECTORIES (
)
INCLUDE_DIRECTORIES (SYSTEM
${QCA_INCLUDE_DIR}
${QTKEYCHAIN_INCLUDE_DIR}
)
INCLUDE_DIRECTORIES (
../../gui

View File

@ -24,6 +24,7 @@ INCLUDE_DIRECTORIES (
)
INCLUDE_DIRECTORIES (SYSTEM
${QCA_INCLUDE_DIR}
${QTKEYCHAIN_INCLUDE_DIR}
)
INCLUDE_DIRECTORIES (
../../gui

View File

@ -24,6 +24,7 @@ INCLUDE_DIRECTORIES (
)
INCLUDE_DIRECTORIES (SYSTEM
${QCA_INCLUDE_DIR}
${QTKEYCHAIN_INCLUDE_DIR}
)
INCLUDE_DIRECTORIES (
../../gui

View File

@ -24,6 +24,7 @@ INCLUDE_DIRECTORIES (
)
INCLUDE_DIRECTORIES (SYSTEM
${QCA_INCLUDE_DIR}
${QTKEYCHAIN_INCLUDE_DIR}
)
INCLUDE_DIRECTORIES (
../../gui

View File

@ -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}

View File

@ -37,6 +37,10 @@
#include <QSslConfiguration>
#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<QString, QSet<QSslError::SslError> >() )
, 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 <b>used from now</b> on to store and retrieve the master password." )
.arg( AUTH_PASSWORD_HELPER_DISPLAY_NAME ) :
tr( "Your %1 will <b>not be used anymore</b> 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;
bool storedPasswordIsValid = false;
bool ok = false;
// 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();
bool ok = creds->getMasterPassword( pass, masterPasswordHashInDatabase() );
pass.clear();
ok = creds->getMasterPassword( pass, masterPasswordHashInDatabase() );
creds->unlock();
}
if ( ok && !pass.isEmpty() && !masterPasswordSame( pass ) )
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;

View File

@ -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<QString, QSet<QSslError::SslError> > 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

View File

@ -762,6 +762,7 @@ INCLUDE_DIRECTORIES(
)
INCLUDE_DIRECTORIES(SYSTEM
${QCA_INCLUDE_DIR}
${QTKEYCHAIN_INCLUDE_DIR}
${QWT_INCLUDE_DIR}
${SQLITE3_INCLUDE_DIR}
${QSCINTILLA_INCLUDE_DIR}

View File

@ -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;

View File

@ -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

View File

@ -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 <b>used from now</b> on to store and retrieve the master password." )
.arg( QgsAuthManager::AUTH_PASSWORD_HELPER_DISPLAY_NAME ) :
QObject::tr( "Your %1 will <b>not be used anymore</b> 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 );
}

View File

@ -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

View File

@ -19,9 +19,9 @@
#include "qgsauthmanager.h"
#include "qgslogger.h"
#include "qgssettings.h"
#include <QPushButton>
#include <QSettings>
#include <QThread>
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;
}

View File

@ -49,6 +49,7 @@ INCLUDE_DIRECTORIES(SYSTEM
${GEOS_INCLUDE_DIR}
${QSCINTILLA_INCLUDE_DIR}
${QCA_INCLUDE_DIR}
${QTKEYCHAIN_INCLUDE_DIR}
)
INCLUDE_DIRECTORIES(
../../core

View File

@ -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})

View File

@ -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}
)

View File

@ -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})

View File

@ -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

View File

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>277</width>
<width>396</width>
<height>289</height>
</rect>
</property>
@ -30,7 +30,7 @@
<item row="1" column="0" colspan="2">
<widget class="QStackedWidget" name="stackedWidget">
<property name="currentIndex">
<number>0</number>
<number>1</number>
</property>
<widget class="QWidget" name="page">
<layout class="QFormLayout" name="formLayout_2">
@ -154,6 +154,13 @@
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="chkbxPasswordHelperEnable">
<property name="text">
<string>Store master password in your password manager</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="lblDontForget">
<property name="styleSheet">

View File

@ -32,6 +32,7 @@ INCLUDE_DIRECTORIES(SYSTEM
${PROJ_INCLUDE_DIR}
${GEOS_INCLUDE_DIR}
${QCA_INCLUDE_DIR}
${QTKEYCHAIN_INCLUDE_DIR}
${QSCINTILLA_INCLUDE_DIR}
)

View File

@ -26,6 +26,7 @@ INCLUDE_DIRECTORIES(SYSTEM
${PROJ_INCLUDE_DIR}
${GEOS_INCLUDE_DIR}
${QCA_INCLUDE_DIR}
${QTKEYCHAIN_INCLUDE_DIR}
${SQLITE3_INCLUDE_DIR}
)

View File

@ -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<QgsAuthMethodConfig> 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"

View File

@ -30,6 +30,7 @@ INCLUDE_DIRECTORIES(SYSTEM
${GEOS_INCLUDE_DIR}
${QWT_INCLUDE_DIR}
${QCA_INCLUDE_DIR}
${QTKEYCHAIN_INCLUDE_DIR}
${QSCINTILLA_INCLUDE_DIR}
)

View File

@ -20,6 +20,7 @@ INCLUDE_DIRECTORIES(SYSTEM
${PROJ_INCLUDE_DIR}
${GEOS_INCLUDE_DIR}
${QCA_INCLUDE_DIR}
${QTKEYCHAIN_INCLUDE_DIR}
${POSTGRES_INCLUDE_DIR}
)