Use CredentialStore to manage user credentials.

This fixes the bug that if the password is not stored in the cfg
file the user could not cancel the auth dialog. Moreover it handles
various credential backends better and gives a better user experience.
This commit is contained in:
Klaas Freitag 2012-11-13 11:15:25 +01:00
parent 78be11b3cf
commit a8dbed989b
10 changed files with 375 additions and 65 deletions

View file

@ -61,6 +61,7 @@ set(libsync_SRCS
mirall/unisonfolder.cpp
mirall/networklocation.cpp
mirall/mirallconfigfile.cpp
mirall/credentialstore.cpp
mirall/csyncfolder.cpp
mirall/owncloudfolder.cpp
mirall/csyncthread.cpp
@ -80,6 +81,7 @@ set(libsync_HEADERS
mirall/owncloudfolder.h
mirall/csyncthread.h
mirall/owncloudinfo.h
mirall/credentialstore.h
)
IF( INOTIFY_FOUND )

View file

@ -31,6 +31,7 @@
#include "mirall/updatedetector.h"
#include "mirall/proxydialog.h"
#include "mirall/version.h"
#include "mirall/credentialstore.h"
#ifdef WITH_CSYNC
#include "mirall/csyncfolder.h"
@ -193,9 +194,6 @@ void Application::slotStartFolderSetup( int result )
connect( ownCloudInfo::instance(),SIGNAL(noOwncloudFound(QNetworkReply*)),
SLOT(slotNoOwnCloudFound(QNetworkReply*)));
connect( ownCloudInfo::instance(),SIGNAL(ownCloudDirExists(QString,QNetworkReply*)),
this,SLOT(slotAuthCheck(QString,QNetworkReply*)));
ownCloudInfo::instance()->checkInstallation();
} else {
_owncloudSetupWizard->startWizard(true); // with intro
@ -225,7 +223,7 @@ void Application::slotOwnCloudFound( const QString& url, const QString& versionS
return;
}
QTimer::singleShot( 0, this, SLOT( slotCheckAuthentication() ));
QTimer::singleShot( 0, this, SLOT( slotFetchCredentials() ));
}
void Application::slotNoOwnCloudFound( QNetworkReply* reply )
@ -256,8 +254,48 @@ void Application::slotNoOwnCloudFound( QNetworkReply* reply )
setupContextMenu();
}
void Application::slotFetchCredentials()
{
connect( CredentialStore::instance(), SIGNAL(fetchCredentialsFinished(bool)),
this, SLOT(slotCredentialsFetched(bool)) );
CredentialStore::instance()->fetchCredentials();
if( CredentialStore::instance()->state() == CredentialStore::TooManyAttempts ) {
QString trayMessage = tr("Too many user attempts to enter password.");
_tray->showMessage(tr("Credentials"), trayMessage);
_actionOpenStatus->setEnabled( false );
_actionAddFolder->setEnabled( false );
}
}
void Application::slotCredentialsFetched(bool ok)
{
qDebug() << "Credentials successfully fetched: " << ok;
if( ! ok ) {
QString trayMessage;
trayMessage = tr("Error: Could not retrieve the password!");
if( CredentialStore::instance()->state() == CredentialStore::UserCanceled ) {
trayMessage = tr("Password dialog was canceled!");
}
if( !trayMessage.isEmpty() ) {
_tray->showMessage(tr("Credentials"), trayMessage);
}
qDebug() << "Could not fetch credentials";
_actionAddFolder->setEnabled( false );
_actionOpenStatus->setEnabled( false );
} else {
// Credential fetched ok.
QTimer::singleShot( 0, this, SLOT( slotCheckAuthentication() ));
}
disconnect( CredentialStore::instance(), SIGNAL(fetchCredentialsFinished(bool)) );
}
void Application::slotCheckAuthentication()
{
connect( ownCloudInfo::instance(),SIGNAL(ownCloudDirExists(QString,QNetworkReply*)),
this,SLOT(slotAuthCheck(QString,QNetworkReply*)));
qDebug() << "# checking for authentication settings.";
ownCloudInfo::instance()->getRequest(QLatin1String("/"), true ); // this call needs to be authenticated.
// simply GET the webdav root, will fail if credentials are wrong.
@ -266,6 +304,8 @@ void Application::slotCheckAuthentication()
void Application::slotAuthCheck( const QString& ,QNetworkReply *reply )
{
bool ok = true;
if( reply->error() == QNetworkReply::AuthenticationRequiredError ) { // returned if the user is wrong.
qDebug() << "******** Password is wrong!";
QMessageBox::warning(0, tr("No %1 Connection").arg(_theme->appName()),
@ -273,6 +313,7 @@ void Application::slotAuthCheck( const QString& ,QNetworkReply *reply )
"<p>Please correct them by starting the configuration dialog from the tray!</p>")
.arg(_theme->appName()));
_actionAddFolder->setEnabled( false );
ok = false;
} else if( reply->error() == QNetworkReply::OperationCanceledError ) {
// the username was wrong and ownCloudInfo was closing the request after a couple of auth tries.
qDebug() << "******** Username or password is wrong!";
@ -280,7 +321,14 @@ void Application::slotAuthCheck( const QString& ,QNetworkReply *reply )
tr("<p>Either your user name or your password are not correct.</p>"
"<p>Please correct it by starting the configuration dialog from the tray!</p>"));
_actionAddFolder->setEnabled( false );
} else {
ok = false;
}
// disconnect from ownCloud Info signals
disconnect( ownCloudInfo::instance(),SIGNAL(ownCloudDirExists(QString,QNetworkReply*)),
this,SLOT(slotAuthCheck(QString,QNetworkReply*)));
if( ok ) {
qDebug() << "######## Credentials are ok!";
int cnt = _folderMan->setupFolders();
if( cnt ) {
@ -296,12 +344,10 @@ void Application::slotAuthCheck( const QString& ,QNetworkReply *reply )
computeOverallSyncStatus();
}
_actionAddFolder->setEnabled( true );
}
// disconnect from ownCloud Info signals
disconnect( ownCloudInfo::instance(),SIGNAL(ownCloudDirExists(QString,QNetworkReply*)),
this,SLOT(slotAuthCheck(QString,QNetworkReply*)));
setupContextMenu();
} else {
slotFetchCredentials();
}
}
void Application::slotSSLFailed( QNetworkReply *reply, QList<QSslError> errors )
@ -539,8 +585,12 @@ void Application::slotTrayClicked( QSystemTrayIcon::ActivationReason reason )
{
// A click on the tray icon should only open the status window on Win and
// Linux, not on Mac. They want a menu entry.
// If the user canceled login, rather open the login window.
if( CredentialStore::instance()->state() == CredentialStore::UserCanceled ) {
slotFetchCredentials();
}
#if defined Q_WS_WIN || defined Q_WS_X11
if( reason == QSystemTrayIcon::Trigger ) {
if( reason == QSystemTrayIcon::Trigger && _actionOpenStatus->isEnabled() ) {
slotOpenStatus();
}
#endif

View file

@ -90,7 +90,8 @@ protected slots:
void slotOpenLogBrowser();
void slotAbout();
void slotSSLFailed( QNetworkReply *reply, QList<QSslError> errors );
void slotFetchCredentials();
void slotCredentialsFetched( bool );
void slotStartUpdateDetector();
private:

View file

@ -0,0 +1,147 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include <QtGui>
#include "mirall/credentialstore.h"
#include "mirall/mirallconfigfile.h"
#include "mirall/theme.h"
namespace Mirall {
CredentialStore *CredentialStore::_instance=0;
CredentialStore::CredState CredentialStore::_state = NotFetched;
QString CredentialStore::_passwd = QString::null;
QString CredentialStore::_user = QString::null;
int CredentialStore::_tries = 0;
CredentialStore::CredentialStore(QObject *parent) :
QObject(parent)
{
}
CredentialStore *CredentialStore::instance()
{
if( !CredentialStore::_instance ) CredentialStore::_instance = new CredentialStore;
return CredentialStore::_instance;
}
QString CredentialStore::password( const QString& ) const
{
return _passwd;
}
QString CredentialStore::user( const QString& ) const
{
return _user;
}
CredentialStore::CredState CredentialStore::state()
{
return _state;
}
void CredentialStore::fetchCredentials()
{
_state = Fetching;
MirallConfigFile cfgFile;
MirallConfigFile::CredentialType t;
if( _tries++ == 3 ) {
qDebug() << "Too many attempts to enter password!";
_state = TooManyAttempts;
return;
}
t = cfgFile.credentialType();
bool ok = false;
QString pwd;
_state = Fetching;
_user = cfgFile.ownCloudUser();
switch( t ) {
case MirallConfigFile::User: {
/* Ask the user for the password */
/* Fixme: Move user interaction out here. */
pwd = QInputDialog::getText(0, QApplication::translate("MirallConfigFile","Password Required"),
QApplication::translate("MirallConfigFile","Please enter your %1 password:")
.arg(Theme::instance()->appName()),
QLineEdit::Password,
QString::null, &ok);
if( !ok ) {
_state = UserCanceled;
}
break;
}
case MirallConfigFile::Settings: {
/* Read from config file. */
pwd = cfgFile.ownCloudPasswd();
ok = true;
break;
}
case MirallConfigFile::KeyChain: {
/* Qt Keychain is not yet implemented. */
#ifdef HAVE_QTKEYCHAIN
if( !_user.isEmpty() ) {
ReadPasswordJoei b job( QLatin1String(Theme::instance()->appName()) );
job.setAutoDelete( false );
job.setKey( _user );
job.connect( &job, SIGNAL(finished(QKeychain::Job*)), this,
SLOT(slotKeyChainFinished(QKeyChain::Job*)));
job.start();
}
#else
qDebug() << "QtKeyChain: Not yet implemented!";
#endif
break;
}
default: {
break;
}
}
if( ok ) {
_passwd = pwd;
_state = Ok;
}
if( !ok && _state == Fetching ) {
_state = Error;
}
emit( fetchCredentialsFinished(ok) );
}
#ifdef HAVE_QTKEYCHAIN
void CredentialsStore::slotKeyChainFinished(QKeyChain::Job* job)
{
if( job ) {
if( job->error() ) {
qDebug() << "Error mit keychain: " << job->errorString();
} else {
_passwd = job.textData();
}
}
}
#endif
QByteArray CredentialStore::basicAuthHeader() const
{
QString concatenated = _user + QLatin1Char(':') + _passwd;
const QString b(QLatin1String("Basic "));
QByteArray data = b.toLocal8Bit() + concatenated.toLocal8Bit().toBase64();
return data;
}
}

View file

@ -0,0 +1,102 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#ifndef CREDENTIALSTORE_H
#define CREDENTIALSTORE_H
#include <QObject>
namespace Mirall {
/*
* This object holds the credential information of the ownCloud connection. It
* is implemented as a singleton.
* At startup of the client, at first the fetchCredentials() method must be called
* which tries to get credentials from one of the supported backends. To determine
* which backend should be used, MirallConfigFile::credentialType() is called as
* the backend is configured in the config file.
*
* The fetchCredentials() call changes the internal state of the credential store
* to one of
* Ok: There are credentials. Note that it's unknown if they are correct!!
* UserCanceled: The fetching involved user interaction and the user canceled
* the operation. No valid credentials are there.
* TooManyAttempts: The user tried to often to enter a password.
* Fetching: The fetching is not yet finished.
* Error: A general error happened.
* After fetching has finished, signal fetchCredentialsFinished(bool) is emitted.
* The result can be retrieved with state() and password() and user() methods.
*/
class CredentialStore : public QObject
{
Q_OBJECT
public:
enum CredState { NotFetched = 0, Ok, UserCanceled, Fetching, Error, TooManyAttempts };
QString password( const QString& connection = QString::null ) const;
QString user( const QString& connection = QString::null ) const;
/**
* @brief state
* @return the state of the Credentialstore.
*/
CredState state();
/**
* @brief fetchCredentials - start to retrieve user credentials.
*
* This method must be called first to retrieve the credentials.
* At the end, this method emits the fetchKeyChainFinished() signal.
*/
void fetchCredentials();
/**
* @brief basicAuthHeader - return a basic authentication header.
* @return a QByteArray with a ready to use Header for HTTP basic auth.
*/
QByteArray basicAuthHeader() const;
/**
* @brief instance - singleton pointer.
* @return the singleton pointer to access the object.
*/
static CredentialStore *instance();
signals:
/**
* @brief fetchCredentialsFinished
*
* emitted as soon as the fetching of the credentials has finished.
* If the parameter is true, there is a password and user. This does
* however, not say if the credentials are valid log in data.
* If false, the user pressed cancel.
*/
void fetchCredentialsFinished(bool);
private slots:
#ifdef HAVE_QTKEYCHAIN
void slotKeyChainFinished(QKeyChain::Job* job);
#endif
private:
explicit CredentialStore(QObject *parent = 0);
static CredentialStore *_instance;
static CredState _state;
static QString _passwd;
static QString _user;
static int _tries;
};
}
#endif // CREDENTIALSTORE_H

View file

@ -313,6 +313,24 @@ int MirallConfigFile::pollTimerExceedFactor( const QString& connection ) const
return pte;
}
MirallConfigFile::CredentialType MirallConfigFile::credentialType() const
{
QString con; /* ( connection ); */
/* if( connection.isEmpty() ) */ con = defaultConnection();
CredentialType ct = Settings;
QSettings settings( configFile(), QSettings::IniFormat );
settings.setIniCodec( "UTF-8" );
settings.beginGroup( con );
bool skipPwd = settings.value( QLatin1String("nostoredpassword"), false ).toBool();
if( skipPwd ) {
ct = User;
}
return ct;
}
QString MirallConfigFile::ownCloudPasswd( const QString& connection ) const
{
QString con( connection );
@ -324,22 +342,6 @@ QString MirallConfigFile::ownCloudPasswd( const QString& connection ) const
QString pwd;
bool skipPwd = settings.value( QLatin1String("nostoredpassword"), false ).toBool();
if( skipPwd ) {
if( ! _askedUser ) {
bool ok;
QString text = QInputDialog::getText(0, QApplication::translate("MirallConfigFile","Password Required"),
QApplication::translate("MirallConfigFile","Please enter your %1 password:")
.arg(Theme::instance()->appName()),
QLineEdit::Password,
QString::null, &ok);
if( ok && !text.isEmpty() ) { // empty password is not allowed on ownCloud
_passwd = text;
_askedUser = true;
}
}
pwd = _passwd;
} else {
QByteArray pwdba = settings.value(QLatin1String("passwd")).toByteArray();
if( pwdba.isEmpty() ) {
// check the password entry, cleartext from before
@ -356,7 +358,6 @@ QString MirallConfigFile::ownCloudPasswd( const QString& connection ) const
}
}
pwd = QString::fromUtf8( QByteArray::fromBase64(pwdba) );
}
return pwd;
}
@ -396,15 +397,6 @@ int MirallConfigFile::maxLogLines() const
return logLines;
}
QByteArray MirallConfigFile::basicAuthHeader() const
{
QString concatenated = ownCloudUser() + QLatin1Char(':') + ownCloudPasswd();
const QString b(QLatin1String("Basic "));
QByteArray data = b.toLocal8Bit() + concatenated.toLocal8Bit().toBase64();
return data;
}
// remove a custom config file.
void MirallConfigFile::cleanupCustomConfig()
{

View file

@ -24,6 +24,9 @@ namespace Mirall {
class MirallConfigFile
{
/* let only CredentialStore read the password from the file. All other classes
* should work with CredentialStore to get the credentials. */
friend class CredentialStore;
public:
MirallConfigFile( const QString& appendix = QString() );
@ -35,6 +38,12 @@ public:
oCSetupResultTop // ownCloud connect result page
};
enum CredentialType {
User = 0,
Settings,
KeyChain
};
QString configPath() const;
QString configFile() const;
QString excludeFile() const;
@ -60,9 +69,7 @@ public:
QByteArray caCerts();
void setCaCerts( const QByteArray& );
QString ownCloudUser( const QString& connection = QString() ) const;
QString ownCloudPasswd( const QString& connection = QString() ) const;
CredentialType credentialType() const;
QString ownCloudVersion() const;
void setOwnCloudVersion( const QString& );
@ -79,8 +86,6 @@ public:
int remotePollInterval( const QString& connection = QString() ) const;
int pollTimerExceedFactor( const QString& connection = QString() ) const;
QByteArray basicAuthHeader() const;
// Custom Config: accept the custom config to become the main one.
void acceptCustomConfig();
// Custom Config: remove the custom config file.
@ -99,6 +104,10 @@ public:
QString proxyUser() const;
QString proxyPassword() const;
protected:
QString ownCloudPasswd( const QString& connection = QString() ) const;
QString ownCloudUser( const QString& connection = QString() ) const;
private:
QVariant getValue(const QString& param, const QString& group) const;

View file

@ -16,6 +16,7 @@
#include "mirall/owncloudfolder.h"
#include "mirall/mirallconfigfile.h"
#include "mirall/owncloudinfo.h"
#include "mirall/credentialstore.h"
#include <csync.h>
@ -166,7 +167,9 @@ void ownCloudFolder::startSync(const QStringList &pathList)
Q_ASSERT(proxies.count() > 0);
QNetworkProxy proxy = proxies.first();
_csync->setConnectionDetails( cfgFile.ownCloudUser(), cfgFile.ownCloudPasswd(), proxy );
_csync->setConnectionDetails( CredentialStore::instance()->user(),
CredentialStore::instance()->password(),
proxy );
connect(_csync, SIGNAL(started()), SLOT(slotCSyncStarted()), Qt::QueuedConnection);
connect(_csync, SIGNAL(finished()), SLOT(slotCSyncFinished()), Qt::QueuedConnection);

View file

@ -16,6 +16,8 @@
#include "mirall/mirallconfigfile.h"
#include "mirall/version.h"
#include "mirall/theme.h"
#include "mirall/credentialstore.h"
#include <QtCore>
#include <QtGui>
#include <QAuthenticator>
@ -85,6 +87,7 @@ bool ownCloudInfo::isConfigured()
void ownCloudInfo::checkInstallation()
{
/* No authentication required for this. */
getRequest( QLatin1String("status.php"), false );
}
@ -143,7 +146,7 @@ void ownCloudInfo::mkdirRequest( const QString& dir )
header.setValue("Connection", "keep-alive");
header.setContentType("application/x-www-form-urlencoded"); //important
header.setContentLength(0);
header.setValue("Authorization", cfgFile.basicAuthHeader());
header.setValue("Authorization", CredentialStore::instance()->basicAuthHeader());
int david = qhttp->request(header,0,0);
//////////////// connect(davinfo, SIGNAL(dataSendProgress(int,int)), this, SLOT(SendStatus(int, int)));
@ -238,8 +241,8 @@ void ownCloudInfo::slotAuthentication( QNetworkReply *reply, QAuthenticator *aut
MirallConfigFile cfgFile( configHandle );
qDebug() << "Authenticating request for " << reply->url();
if( reply->url().toString().startsWith( cfgFile.ownCloudUrl( _connection, true )) ) {
auth->setUser( cfgFile.ownCloudUser( _connection ) );
auth->setPassword( cfgFile.ownCloudPasswd( _connection ));
auth->setUser( CredentialStore::instance()->user() ); //_connection ) );
auth->setPassword( CredentialStore::instance()->password() ); // _connection ));
} else {
qDebug() << "WRN: attempt to authenticate to different url - attempt " <<_authAttempts;
}
@ -428,7 +431,7 @@ void ownCloudInfo::setupHeaders( QNetworkRequest & req, quint64 size )
req.setRawHeader( QByteArray("Host"), url.host().toUtf8() );
req.setRawHeader( QByteArray("User-Agent"), QString::fromLatin1("mirall-%1")
.arg(QLatin1String(MIRALL_STRINGIFY(MIRALL_VERSION))).toAscii());
req.setRawHeader( QByteArray("Authorization"), cfgFile.basicAuthHeader() );
req.setRawHeader( QByteArray("Authorization"), CredentialStore::instance()->basicAuthHeader() );
if (size) {
req.setHeader( QNetworkRequest::ContentLengthHeader, size);

View file

@ -17,6 +17,7 @@
#include "mirall/theme.h"
#include "mirall/owncloudinfo.h"
#include "mirall/mirallconfigfile.h"
#include "mirall/credentialstore.h"
#include <QtCore>
#include <QtGui>
@ -498,7 +499,7 @@ void StatusDialog::slotOCInfo( const QString& url, const QString& versionStr, co
MirallConfigFile cfg;
_ocUrlLabel->setOpenExternalLinks(true);
_ocUrlLabel->setText( tr("Connected to <a href=\"%1\">%1</a> as <i>%2</i>.")
.arg(url).arg(cfg.ownCloudUser()) );
.arg(url).arg( CredentialStore::instance()->user()) );
_ocUrlLabel->setToolTip( tr("Version: %1 (%2)").arg(versionStr).arg(version));
_ButtonAdd->setEnabled(true);