Once client gets 401/403 from the server, check if remote wipe was requested.
authorCamila San <hello@camila.codes>
Wed, 24 Jul 2019 11:56:21 +0000 (13:56 +0200)
committerCamila San <hello@camila.codes>
Thu, 17 Oct 2019 18:11:31 +0000 (20:11 +0200)
- When the the users logs because of 401 or 403 errors, it checks if the
server requested the remote wipe. If yes, locally deletes account and folders
connected to the account and notify the server. If no, proceeds to ask the
user to login again.
- The app password is restored in the keychain.
- WIP: The change also includes a test class for RemoteWipe.

Signed-off-by: Camila San <hello@camila.codes>
22 files changed:
src/gui/CMakeLists.txt
src/gui/accountmanager.cpp
src/gui/accountmanager.h
src/gui/accountsettings.cpp
src/gui/accountsettings.h
src/gui/accountstate.cpp
src/gui/accountstate.h
src/gui/creds/webflowcredentials.cpp
src/gui/folderman.cpp
src/gui/folderman.h
src/gui/remotewipe.cpp [new file with mode: 0644]
src/gui/remotewipe.h [new file with mode: 0644]
src/gui/wizard/flow2authcredspage.cpp
src/libsync/account.cpp
src/libsync/account.h
test/CMakeLists.txt
test/stub.cpp [deleted file]
test/stubfolderman.cpp [new file with mode: 0644]
test/stubremotewipe.cpp [new file with mode: 0644]
test/testfolderman.cpp
test/testhelper.h [new file with mode: 0644]
test/testremotewipe.cpp [new file with mode: 0644]

index 8e7ca45de023d55bc23d9e77c286f8c46a48a18a..2d6e98595997bb50b06b5ad60588cf1126fe7cad 100644 (file)
@@ -104,6 +104,7 @@ set(client_SRCS
     guiutility.cpp
     elidedlabel.cpp
     iconjob.cpp
+    remotewipe.cpp
     creds/credentialsfactory.cpp
     creds/httpcredentialsgui.cpp
     creds/oauth.cpp
index ca00a4e1795cd42a705a55cf6d7d044b5aa0149d..eec375180b484d919388a71c7ddd69702ea98917 100644 (file)
@@ -368,6 +368,7 @@ void AccountManager::shutdown()
     _accounts.clear();
     foreach (const auto &acc, accountsCopy) {
         emit accountRemoved(acc.data());
+        emit removeAccountFolders(acc.data());
     }
 }
 
index 35df3584aca9a2274da010a4caf557c052c1572d..fabc6ca5b620eea4f5edb054bfb2fa9700426c92 100644 (file)
@@ -70,7 +70,6 @@ public:
      */
     void deleteAccount(AccountState *account);
 
-
     /**
      * Creates an account and sets up some basic handlers.
      * Does *not* add the account to the account manager just yet.
@@ -104,6 +103,7 @@ public slots:
 Q_SIGNALS:
     void accountAdded(AccountState *account);
     void accountRemoved(AccountState *account);
+    void removeAccountFolders(AccountState *account);
 
 private:
     AccountManager() {}
index 2fc324573cfa1ac519cef4c996fec6b2ce0713b3..a7b39d60642304d045bd9da63dec819b49df7fe6 100644 (file)
@@ -147,6 +147,8 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent)
     createAccountToolbox();
     connect(AccountManager::instance(), &AccountManager::accountAdded,
         this, &AccountSettings::slotAccountAdded);
+    connect(this, &AccountSettings::removeAccountFolders,
+            AccountManager::instance(), &AccountManager::removeAccountFolders);
     connect(ui->_folderList, &QWidget::customContextMenuRequested,
         this, &AccountSettings::slotCustomContextMenuRequested);
     connect(ui->_folderList, &QAbstractItemView::clicked,
@@ -334,9 +336,9 @@ void AccountSettings::slotEncryptionFlagError(const QByteArray& fileId, int http
 
 void AccountSettings::slotLockForEncryptionSuccess(const QByteArray& fileId, const QByteArray &token)
 {
-               accountsState()->account()->e2e()->setTokenForFolder(fileId, token);
+        accountsState()->account()->e2e()->setTokenForFolder(fileId, token);
 
-               FolderMetadata emptyMetadata(accountsState()->account());
+        FolderMetadata emptyMetadata(accountsState()->account());
     auto encryptedMetadata = emptyMetadata.encryptedMetadata();
     if (encryptedMetadata.isEmpty()) {
       //TODO: Mark the folder as unencrypted as the metadata generation failed.
@@ -348,36 +350,36 @@ void AccountSettings::slotLockForEncryptionSuccess(const QByteArray& fileId, con
       return;
     }
     auto storeMetadataJob = new StoreMetaDataApiJob(accountsState()->account(), fileId, emptyMetadata.encryptedMetadata());
-               connect(storeMetadataJob, &StoreMetaDataApiJob::success,
-                                               this, &AccountSettings::slotUploadMetadataSuccess);
-               connect(storeMetadataJob, &StoreMetaDataApiJob::error,
-                                               this, &AccountSettings::slotUpdateMetadataError);
+        connect(storeMetadataJob, &StoreMetaDataApiJob::success,
+                        this, &AccountSettings::slotUploadMetadataSuccess);
+        connect(storeMetadataJob, &StoreMetaDataApiJob::error,
+                        this, &AccountSettings::slotUpdateMetadataError);
 
-               storeMetadataJob->start();
+        storeMetadataJob->start();
 }
 
 void AccountSettings::slotUploadMetadataSuccess(const QByteArray& folderId)
 {
-       const auto token = accountsState()->account()->e2e()->tokenForFolder(folderId);
-       auto unlockJob = new UnlockEncryptFolderApiJob(accountsState()->account(), folderId, token);
-       connect(unlockJob, &UnlockEncryptFolderApiJob::success,
-                                       this, &AccountSettings::slotUnlockFolderSuccess);
-       connect(unlockJob, &UnlockEncryptFolderApiJob::error,
-                                       this, &AccountSettings::slotUnlockFolderError);
-       unlockJob->start();
+    const auto token = accountsState()->account()->e2e()->tokenForFolder(folderId);
+    auto unlockJob = new UnlockEncryptFolderApiJob(accountsState()->account(), folderId, token);
+    connect(unlockJob, &UnlockEncryptFolderApiJob::success,
+                    this, &AccountSettings::slotUnlockFolderSuccess);
+    connect(unlockJob, &UnlockEncryptFolderApiJob::error,
+                    this, &AccountSettings::slotUnlockFolderError);
+    unlockJob->start();
 }
 
 void AccountSettings::slotUpdateMetadataError(const QByteArray& folderId, int httpReturnCode)
 {
     Q_UNUSED(httpReturnCode);
 
-       const auto token = accountsState()->account()->e2e()->tokenForFolder(folderId);
-       auto unlockJob = new UnlockEncryptFolderApiJob(accountsState()->account(), folderId, token);
-       connect(unlockJob, &UnlockEncryptFolderApiJob::success,
-                                       this, &AccountSettings::slotUnlockFolderSuccess);
-       connect(unlockJob, &UnlockEncryptFolderApiJob::error,
-                                       this, &AccountSettings::slotUnlockFolderError);
-       unlockJob->start();
+    const auto token = accountsState()->account()->e2e()->tokenForFolder(folderId);
+    auto unlockJob = new UnlockEncryptFolderApiJob(accountsState()->account(), folderId, token);
+    connect(unlockJob, &UnlockEncryptFolderApiJob::success,
+                    this, &AccountSettings::slotUnlockFolderSuccess);
+    connect(unlockJob, &UnlockEncryptFolderApiJob::error,
+                    this, &AccountSettings::slotUnlockFolderError);
+    unlockJob->start();
 }
 
 void AccountSettings::slotLockForEncryptionError(const QByteArray& fileId, int httpErrorCode)
index ce1885d2df47ca36065d0e9923d2cadf26dd4c71..e283df26fc096cc02047f3053932f5d4bd107b72 100644 (file)
@@ -63,6 +63,7 @@ signals:
     void openFolderAlias(const QString &);
     void showIssuesList(AccountState *account);
     void requesetMnemonic();
+    void removeAccountFolders(AccountState *account);
 
 public slots:
     void slotOpenOC();
index ff2f7d000dccfd331bb27b5609cb1d9b8fbbccad..7893a7a3b332bef767e692c48931a9bf563edb7f 100644 (file)
@@ -14,6 +14,7 @@
 
 #include "accountstate.h"
 #include "accountmanager.h"
+#include "remotewipe.h"
 #include "account.h"
 #include "creds/abstractcredentials.h"
 #include "creds/httpcredentials.h"
 #include <QTimer>
 #include <qfontmetrics.h>
 
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QNetworkRequest>
+#include <QBuffer>
+
 namespace OCC {
 
 Q_LOGGING_CATEGORY(lcAccountState, "nextcloud.gui.account.state", QtInfoMsg)
@@ -36,11 +42,12 @@ AccountState::AccountState(AccountPtr account)
     , _waitingForNewCredentials(false)
     , _notificationsEtagResponseHeader("*")
     , _maintenanceToConnectedDelay(60000 + (qrand() % (4 * 60000))) // 1-5min delay
+    , _remoteWipe(new RemoteWipe(_account))
 {
     qRegisterMetaType<AccountState *>("AccountState*");
 
     connect(account.data(), &Account::invalidCredentials,
-        this, &AccountState::slotInvalidCredentials);
+        this, &AccountState::slotHandleRemoteWipeCheck);
     connect(account.data(), &Account::credentialsFetched,
         this, &AccountState::slotCredentialsFetched);
     connect(account.data(), &Account::credentialsAsked,
@@ -303,7 +310,7 @@ void AccountState::slotConnectionValidatorResult(ConnectionValidator::Status sta
         break;
     case ConnectionValidator::CredentialsWrong:
     case ConnectionValidator::CredentialsNotReady:
-        slotInvalidCredentials();
+        handleInvalidCredentials();
         break;
     case ConnectionValidator::SslError:
         setState(SignedOut);
@@ -322,7 +329,20 @@ void AccountState::slotConnectionValidatorResult(ConnectionValidator::Status sta
     }
 }
 
-void AccountState::slotInvalidCredentials()
+void AccountState::slotHandleRemoteWipeCheck()
+{
+    // make sure it changes account state and icons
+    signOutByUi();
+
+    qCInfo(lcAccountState) << "Invalid credentials for" << _account->url().toString()
+                           << "checking for remote wipe request";
+
+    _waitingForNewCredentials = false;
+    setState(SignedOut);
+}
+
+
+void AccountState::handleInvalidCredentials()
 {
     if (isSignedOut() || _waitingForNewCredentials)
         return;
@@ -343,6 +363,7 @@ void AccountState::slotInvalidCredentials()
     account()->credentials()->askFromUser();
 }
 
+
 void AccountState::slotCredentialsFetched(AbstractCredentials *)
 {
     // Make a connection attempt, no matter whether the credentials are
index 573b084ff45c39f59067741331e2f7f8876cfa26..6ed9aa2656924d5d8384b128b707bc91cf34f585 100644 (file)
@@ -29,6 +29,7 @@ namespace OCC {
 
 class AccountState;
 class Account;
+class RemoteWipe;
 
 typedef QExplicitlySharedDataPointer<AccountState> AccountStatePtr;
 
@@ -150,6 +151,9 @@ public:
     */
     void setNavigationAppsEtagResponseHeader(const QByteArray &value);
 
+    ///Asks for user credentials
+    void handleInvalidCredentials();
+
 public slots:
     /// Triggers a ping to the server to update state and
     /// connection status and errors.
@@ -164,7 +168,11 @@ signals:
 
 protected Q_SLOTS:
     void slotConnectionValidatorResult(ConnectionValidator::Status status, const QStringList &errors);
-    void slotInvalidCredentials();
+
+    /// When client gets a 401 or 403 checks if server requested remote wipe
+    /// before asking for user credentials again
+    void slotHandleRemoteWipeCheck();
+
     void slotCredentialsFetched(AbstractCredentials *creds);
     void slotCredentialsAsked(AbstractCredentials *creds);
 
@@ -190,6 +198,13 @@ private:
      * Milliseconds for which to delay reconnection after 503/maintenance.
      */
     int _maintenanceToConnectedDelay;
+
+    /**
+     * Connects remote wipe check with the account
+     * the log out triggers the check (loads app password -> create request)
+     */
+    RemoteWipe *_remoteWipe;
+
 };
 }
 
index ce3a1264ea56bc3d5ce9b23293b566d162eff1e1..49c80e1ff751dcbaecca503135dbd6cf3c2aba35 100644 (file)
@@ -358,6 +358,8 @@ void WebFlowCredentials::forgetSensitiveData() {
 
     fetchUser();
 
+    _account->deleteAppPassword();
+
     const QString kck = keychainKey(_account->url().toString(), _user, _account->id());
     if (kck.isEmpty()) {
         qCDebug(lcWebFlowCredentials()) << "InvalidateToken: User is empty, bailing out!";
@@ -416,6 +418,9 @@ void WebFlowCredentials::slotFinished(QNetworkReply *reply) {
 
     if (reply->error() == QNetworkReply::NoError) {
         _credentialsValid = true;
+
+        /// Used later for remote wipe
+        _account->setAppPassword(_password);
     }
 }
 
index ec708d9b7f87c5ca7903508297c6e3cdd459bf5d..af4526545aed65227f55ec3a094ec149dec2f472 100644 (file)
@@ -75,7 +75,7 @@ FolderMan::FolderMan(QObject *parent)
         this, &FolderMan::slotScheduleFolderByTime);
     _timeScheduler.start();
 
-    connect(AccountManager::instance(), &AccountManager::accountRemoved,
+    connect(AccountManager::instance(), &AccountManager::removeAccountFolders,
         this, &FolderMan::slotRemoveFoldersForAccount);
 
     connect(_lockWatcher.data(), &LockWatcher::fileUnlocked,
@@ -443,7 +443,7 @@ void FolderMan::slotFolderSyncPaused(Folder *f, bool paused)
 void FolderMan::slotFolderCanSyncChanged()
 {
     Folder *f = qobject_cast<Folder *>(sender());
-    ASSERT(f);
+     ASSERT(f);
     if (f->canSync()) {
         _socketApi->slotRegisterPath(f->alias());
     } else {
@@ -1084,6 +1084,73 @@ bool FolderMan::startFromScratch(const QString &localFolder)
     return true;
 }
 
+void FolderMan::slotWipeFolderForAccount(AccountState *accountState)
+{
+    QVarLengthArray<Folder *, 16> foldersToRemove;
+    Folder::MapIterator i(_folderMap);
+    while (i.hasNext()) {
+        i.next();
+        Folder *folder = i.value();
+        if (folder->accountState() == accountState) {
+            foldersToRemove.append(folder);
+        }
+    }
+
+    bool success = false;
+    foreach (const auto &f, foldersToRemove) {
+        if (!f) {
+            qCCritical(lcFolderMan) << "Can not remove null folder";
+            return;
+        }
+
+        qCInfo(lcFolderMan) << "Removing " << f->alias();
+
+        const bool currentlyRunning = (_currentSyncFolder == f);
+        if (currentlyRunning) {
+            // abort the sync now
+            terminateSyncProcess();
+        }
+
+        if (_scheduledFolders.removeAll(f) > 0) {
+            emit scheduleQueueChanged();
+        }
+
+        // wipe database
+        f->wipe();
+
+        // wipe data
+        QDir userFolder(f->path());
+        if (userFolder.exists()) {
+            success = userFolder.removeRecursively();
+            if (!success) {
+                qCWarning(lcFolderMan) << "Failed to remove existing folder " << f->path();
+            } else {
+                qCInfo(lcFolderMan) << "wipe: Removed  file " << f->path();
+            }
+
+
+        } else {
+            success = true;
+            qCWarning(lcFolderMan) << "folder does not exist, can not remove.";
+        }
+
+        f->setSyncPaused(true);
+
+        // remove the folder configuration
+        f->removeFromSettings();
+
+        unloadFolder(f);
+        if (currentlyRunning) {
+            delete f;
+        }
+
+        _navigationPaneHelper.scheduleUpdateCloudStorageRegistry();
+    }
+
+    emit folderListChanged(_folderMap);
+    emit wipeDone(accountState, success);
+}
+
 void FolderMan::setDirtyProxy()
 {
     foreach (Folder *f, _folderMap.values()) {
index 7ac16c98c49c9926d61c35a5b95abf1b336ce6e6..97d4b8f45738cc88556db7dcd7cec5488169f09d 100644 (file)
@@ -207,6 +207,11 @@ signals:
      */
     void folderListChanged(const Folder::Map &);
 
+    /**
+     * Emitted once slotRemoveFoldersForAccount is done wiping
+     */
+    void wipeDone(AccountState *account, bool success);
+
 public slots:
 
     /**
@@ -231,6 +236,9 @@ public slots:
     // slot to schedule an ETag job (from Folder only)
     void slotScheduleETagJob(const QString &alias, RequestEtagJob *job);
 
+    /** Wipe folder */
+    void slotWipeFolderForAccount(AccountState *accountState);
+
 private slots:
     void slotFolderSyncPaused(Folder *, bool paused);
     void slotFolderCanSyncChanged();
diff --git a/src/gui/remotewipe.cpp b/src/gui/remotewipe.cpp
new file mode 100644 (file)
index 0000000..8d09f15
--- /dev/null
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) by Camila Ayres <hello@camila.codes>
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "remotewipe.h"
+#include "folderman.h"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QNetworkRequest>
+#include <QBuffer>
+
+namespace OCC {
+
+Q_LOGGING_CATEGORY(lcRemoteWipe, "nextcloud.gui.remotewipe", QtInfoMsg)
+
+RemoteWipe::RemoteWipe(AccountPtr account, QObject *parent)
+    : _account(account),
+      _appPassword(QString()),
+      _accountRemoved(false),
+      _networkManager(nullptr),
+      _networkReplyCheck(nullptr),
+      _networkReplySuccess(nullptr)
+{
+    QObject::connect(AccountManager::instance(), &AccountManager::accountRemoved,
+                     this, [=](AccountState *) {
+        _accountRemoved = true;
+    });
+    QObject::connect(this, &RemoteWipe::authorized, FolderMan::instance(),
+                     &FolderMan::slotWipeFolderForAccount);
+    QObject::connect(FolderMan::instance(), &FolderMan::wipeDone, this,
+                     &RemoteWipe::notifyServerSuccessJob);
+    QObject::connect(_account.data(), &Account::appPasswordRetrieved, this,
+                     &RemoteWipe::startCheckJobWithAppPassword);
+}
+
+void RemoteWipe::startCheckJobWithAppPassword(QString pwd){
+    if(pwd.isEmpty())
+        return;
+
+    _appPassword = pwd;
+    QUrl requestUrl = Utility::concatUrlPath(_account->url().toString(),
+                                             QLatin1String("/index.php/core/wipe/check"));
+    QNetworkRequest request;
+    request.setHeader(QNetworkRequest::ContentTypeHeader,
+                      "application/x-www-form-urlencoded");
+    request.setUrl(requestUrl);
+    request.setSslConfiguration(_account->getOrCreateSslConfig());
+    auto requestBody = new QBuffer;
+    QUrlQuery arguments(QString("token=%1").arg(_appPassword));
+    requestBody->setData(arguments.query(QUrl::FullyEncoded).toLatin1());
+    _networkReplyCheck = _networkManager.post(request, requestBody);
+    QObject::connect(_networkReplyCheck, &QNetworkReply::finished, this,
+                     &RemoteWipe::checkJobSlot);
+}
+
+void RemoteWipe::checkJobSlot()
+{
+    auto jsonData = _networkReplyCheck->readAll();
+    QJsonParseError jsonParseError;
+    QJsonObject json = QJsonDocument::fromJson(jsonData, &jsonParseError).object();
+    bool wipe = false;
+
+    //check for errors
+    if (_networkReplyCheck->error() != QNetworkReply::NoError ||
+            jsonParseError.error != QJsonParseError::NoError) {
+        QString errorReason;
+        QString errorFromJson = json["error"].toString();
+        if (!errorFromJson.isEmpty()) {
+            qCWarning(lcRemoteWipe) << QString("Error returned from the server: <em>%1<em>")
+                                       .arg(errorFromJson.toHtmlEscaped());
+        } else if (_networkReplyCheck->error() != QNetworkReply::NoError) {
+            qCWarning(lcRemoteWipe) << QString("There was an error accessing the 'token' endpoint: <br><em>%1</em>")
+                              .arg(_networkReplyCheck->errorString().toHtmlEscaped());
+        } else if (jsonParseError.error != QJsonParseError::NoError) {
+            qCWarning(lcRemoteWipe) << QString("Could not parse the JSON returned from the server: <br><em>%1</em>")
+                              .arg(jsonParseError.errorString());
+        } else {
+            qCWarning(lcRemoteWipe) <<  QString("The reply from the server did not contain all expected fields");
+        }
+
+    // check for wipe request
+    } else if(!json.value("wipe").isUndefined()){
+        wipe = json["wipe"].toBool();
+    }
+
+    auto manager = AccountManager::instance();
+    auto accountState = manager->account(_account->displayName()).data();
+
+    if(wipe){
+        // delete account
+        manager->deleteAccount(accountState);
+        manager->save();
+
+        // delete data
+        emit authorized(accountState);
+
+    } else {
+        // ask user for his credentials again
+        accountState->handleInvalidCredentials();
+    }
+
+    _networkReplyCheck->deleteLater();
+}
+
+void RemoteWipe::notifyServerSuccessJob(AccountState *accountState, bool dataWiped){
+    if(_accountRemoved && dataWiped && _account == accountState->account()){
+        QUrl requestUrl = Utility::concatUrlPath(_account->url().toString(),
+                                                 QLatin1String("/index.php/core/wipe/success"));
+        QNetworkRequest request;
+        request.setHeader(QNetworkRequest::ContentTypeHeader,
+                          "application/x-www-form-urlencoded");
+        request.setUrl(requestUrl);
+        request.setSslConfiguration(_account->getOrCreateSslConfig());
+        auto requestBody = new QBuffer;
+        QUrlQuery arguments(QString("token=%1").arg(_appPassword));
+        requestBody->setData(arguments.query(QUrl::FullyEncoded).toLatin1());
+        _networkReplySuccess = _networkManager.post(request, requestBody);
+        QObject::connect(_networkReplySuccess, &QNetworkReply::finished, this,
+                         &RemoteWipe::notifyServerSuccessJobSlot);
+    }
+}
+
+void RemoteWipe::notifyServerSuccessJobSlot()
+{
+    auto jsonData = _networkReplySuccess->readAll();
+    QJsonParseError jsonParseError;
+    QJsonObject json = QJsonDocument::fromJson(jsonData, &jsonParseError).object();
+    if (_networkReplySuccess->error() != QNetworkReply::NoError ||
+            jsonParseError.error != QJsonParseError::NoError) {
+        QString errorReason;
+        QString errorFromJson = json["error"].toString();
+        if (!errorFromJson.isEmpty()) {
+            qCWarning(lcRemoteWipe) << QString("Error returned from the server: <em>%1</em>")
+                              .arg(errorFromJson.toHtmlEscaped());
+        } else if (_networkReplySuccess->error() != QNetworkReply::NoError) {
+            qCWarning(lcRemoteWipe) << QString("There was an error accessing the 'success' endpoint: <br><em>%1</em>")
+                              .arg(_networkReplySuccess->errorString().toHtmlEscaped());
+        } else if (jsonParseError.error != QJsonParseError::NoError) {
+            qCWarning(lcRemoteWipe) << QString("Could not parse the JSON returned from the server: <br><em>%1</em>")
+                              .arg(jsonParseError.errorString());
+        } else {
+            qCWarning(lcRemoteWipe) << QString("The reply from the server did not contain all expected fields.");
+        }
+    }
+
+    _networkReplySuccess->deleteLater();
+}
+}
diff --git a/src/gui/remotewipe.h b/src/gui/remotewipe.h
new file mode 100644 (file)
index 0000000..b1353e8
--- /dev/null
@@ -0,0 +1,61 @@
+#ifndef REMOTEWIPE_H
+#define REMOTEWIPE_H
+
+#include "accountmanager.h"
+#include <QNetworkAccessManager>
+
+class QJsonDocument;
+class TestRemoteWipe;
+
+namespace OCC {
+
+class RemoteWipe : public QObject
+{
+    Q_OBJECT
+public:
+    explicit RemoteWipe(AccountPtr account, QObject *parent = nullptr);
+
+signals:
+    /**
+     * Notify if wipe was requested
+     */
+    void authorized(AccountState*);
+
+    /**
+     * Notify if user only needs to login again
+     */
+    void askUserCredentials();
+
+public slots:
+    /**
+     * Once receives a 401 or 403 status response it will do a fetch to
+     * <server>/index.php/core/wipe/check
+     */
+    void startCheckJobWithAppPassword(QString);
+
+private slots:
+    /**
+     * If wipe is requested, delete account and data, if not continue by asking
+     * the user to login again
+     */
+    void checkJobSlot();
+
+    /**
+     * Once the client has wiped all the required data a POST to
+     * <server>/index.php/core/wipe/success
+     */
+    void notifyServerSuccessJob(AccountState *accountState, bool);
+    void notifyServerSuccessJobSlot();
+
+private:
+    AccountPtr _account;
+    QString _appPassword;
+    bool _accountRemoved;
+    QNetworkAccessManager _networkManager;
+    QNetworkReply *_networkReplyCheck;
+    QNetworkReply *_networkReplySuccess;
+
+    friend class ::TestRemoteWipe;
+};
+}
+#endif // REMOTEWIPE_H
\ No newline at end of file
index 165752663e8d7b83d1609917d476c58193463585..85bcb89dc615f08376aed18ee1771582ea0382e5 100644 (file)
@@ -99,6 +99,7 @@ void Flow2AuthCredsPage::asyncAuthResult(Flow2Auth::Result r, const QString &use
         _appPassword = appPassword;
         OwncloudWizard *ocWizard = qobject_cast<OwncloudWizard *>(wizard());
         Q_ASSERT(ocWizard);
+
         emit connectToOCUrl(ocWizard->account()->url().toString());
         break;
     }
index 6a28361fbeddc3f7d79e387b15ac6eb43ce325e5..32e58b2e438a05d4c048e3710526de8a47b68d0c 100644 (file)
 #include <QAuthenticator>
 #include <QStandardPaths>
 
+#include <keychain.h>
+#include "creds/abstractcredentials.h"
+
+using namespace QKeychain;
+
 namespace OCC {
 
 Q_LOGGING_CATEGORY(lcAccount, "nextcloud.sync.account", QtInfoMsg)
+const char app_password[] = "_app-password";
 
 Account::Account(QObject *parent)
     : QObject(parent)
@@ -53,16 +59,17 @@ AccountPtr Account::create()
     AccountPtr acc = AccountPtr(new Account);
     acc->setSharedThis(acc);
 
-               //TODO: This probably needs to have a better
-               // coupling, but it should work for now.
-               acc->e2e()->setAccount(acc);
+        //TODO: This probably needs to have a better
+        // coupling, but it should work for now.
+        acc->e2e()->setAccount(acc);
+
     return acc;
 }
 
 ClientSideEncryption* Account::e2e()
 {
-       // Qt expects everything in the connect to be a pointer, so return a pointer.
-       return &_e2e;
+    // Qt expects everything in the connect to be a pointer, so return a pointer.
+    return &_e2e;
 }
 
 Account::~Account()
@@ -432,6 +439,9 @@ void Account::slotCredentialsAsked()
 
 void Account::handleInvalidCredentials()
 {
+    // Retrieving password will trigger remote wipe check job
+    retrieveAppPassword();
+
     emit invalidCredentials();
 }
 
@@ -503,4 +513,63 @@ void Account::setNonShib(bool nonShib)
     }
 }
 
+void Account::setAppPassword(QString appPassword){
+    const QString kck = AbstractCredentials::keychainKey(
+                url().toString(),
+                davUser() + app_password,
+                id()
+    );
+
+    WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
+    job->setInsecureFallback(false);
+    job->setKey(kck);
+    job->setBinaryData(appPassword.toLatin1());
+    connect(job, &WritePasswordJob::finished, [](Job *) {
+        qCInfo(lcAccount) << "appPassword stored in keychain";
+    });
+    job->start();
+}
+
+void Account::retrieveAppPassword(){
+    const QString kck = AbstractCredentials::keychainKey(
+                url().toString(),
+                credentials()->user() + app_password,
+                id()
+    );
+
+    ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
+    job->setInsecureFallback(false);
+    job->setKey(kck);
+    connect(job, &WritePasswordJob::finished, [this](Job *incoming) {
+        ReadPasswordJob *readJob = static_cast<ReadPasswordJob *>(incoming);
+        QString pwd("");
+        // Error or no valid public key error out
+        if (readJob->error() == NoError &&
+                readJob->binaryData().length() > 0) {
+            pwd = readJob->binaryData();
+        }
+
+        emit appPasswordRetrieved(pwd);
+    });
+    job->start();
+}
+
+void Account::deleteAppPassword(){
+    const QString kck = AbstractCredentials::keychainKey(
+                url().toString(),
+                credentials()->user() + app_password,
+                id()
+    );
+
+    if (kck.isEmpty()) {
+        qCDebug(lcAccount) << "appPassword is empty";
+        return;
+    }
+
+    DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName());
+    job->setInsecureFallback(false);
+    job->setKey(kck);
+    job->start();
+}
+
 } // namespace OCC
index c49be3f58f3e09a8613b5c41ac19a8622f4210d5..1f3e46a96c0269adee11c262334adaf46bed64cf 100644 (file)
@@ -41,6 +41,12 @@ class QNetworkReply;
 class QUrl;
 class QNetworkAccessManager;
 
+namespace QKeychain {
+class Job;
+class WritePasswordJob;
+class ReadPasswordJob;
+}
+
 namespace OCC {
 
 class AbstractCredentials;
@@ -50,7 +56,6 @@ class QuotaInfo;
 class AccessManager;
 class SimpleNetworkJob;
 
-
 /**
  * @brief Reimplement this to handle SSL errors from libsync
  * @ingroup libsync
@@ -236,6 +241,11 @@ public:
 
     ClientSideEncryption* e2e();
 
+    /// Used in RemoteWipe
+    void retrieveAppPassword();
+    void setAppPassword(QString appPassword);
+    void deleteAppPassword();
+
 public slots:
     /// Used when forgetting credentials
     void clearQNAMCache();
@@ -262,6 +272,9 @@ signals:
     void accountChangedAvatar();
     void accountChangedDisplayName();
 
+    /// Used in RemoteWipe
+    void appPasswordRetrieved(QString);
+
 protected Q_SLOTS:
     void slotCredentialsFetched();
     void slotCredentialsAsked();
index fc7e8ef0aa242cbe9ddc798ee494e74e2261fcc2..55e5327e688729f1fca783f0c17fb83fd47511c4 100644 (file)
@@ -60,17 +60,32 @@ nextcloud_add_benchmark(LargeSync "syncenginetestutils.h")
 SET(FolderMan_SRC ../src/gui/folderman.cpp)
 list(APPEND FolderMan_SRC ../src/gui/folder.cpp )
 list(APPEND FolderMan_SRC ../src/gui/socketapi.cpp )
-list(APPEND FolderMan_SRC ../src/gui/accountstate.cpp )
 list(APPEND FolderMan_SRC ../src/gui/syncrunfilelog.cpp )
 list(APPEND FolderMan_SRC ../src/gui/lockwatcher.cpp )
 list(APPEND FolderMan_SRC ../src/gui/guiutility.cpp )
 list(APPEND FolderMan_SRC ../src/gui/navigationpanehelper.cpp )
 list(APPEND FolderMan_SRC ../src/gui/connectionvalidator.cpp )
 list(APPEND FolderMan_SRC ../src/gui/clientproxy.cpp )
+list(APPEND FolderMan_SRC ../src/gui/accountstate.cpp )
+list(APPEND FolderMan_SRC ../src/gui/remotewipe.cpp )
 list(APPEND FolderMan_SRC ${FolderWatcher_SRC})
-list(APPEND FolderMan_SRC stub.cpp )
+list(APPEND FolderMan_SRC stubfolderman.cpp )
 nextcloud_add_test(FolderMan "${FolderMan_SRC}")
 
+SET(RemoteWipe_SRC ../src/gui/remotewipe.cpp)
+list(APPEND RemoteWipe_SRC ../src/gui/clientproxy.cpp )
+list(APPEND RemoteWipe_SRC ../src/gui/guiutility.cpp )
+list(APPEND RemoteWipe_SRC ../src/gui/connectionvalidator.cpp )
+list(APPEND RemoteWipe_SRC ../src/gui/accountstate.cpp )
+list(APPEND RemoteWipe_SRC ../src/gui/socketapi.cpp )
+list(APPEND RemoteWipe_SRC ../src/gui/folder.cpp )
+list(APPEND RemoteWipe_SRC ../src/gui/syncrunfilelog.cpp )
+list(APPEND RemoteWipe_SRC ../src/gui/folderwatcher_linux.cpp )
+list(APPEND RemoteWipe_SRC ../src/gui/folderwatcher.cpp )
+list(APPEND RemoteWipe_SRC ${RemoteWipe_SRC})
+list(APPEND RemoteWipe_SRC stubremotewipe.cpp )
+nextcloud_add_test(RemoteWipe "${RemoteWipe_SRC}")
+
 nextcloud_add_test(OAuth "syncenginetestutils.h;../src/gui/creds/oauth.cpp")
 
 configure_file(test_journal.db "${PROJECT_BINARY_DIR}/bin/test_journal.db" COPYONLY)
diff --git a/test/stub.cpp b/test/stub.cpp
deleted file mode 100644 (file)
index d987b24..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-// stub to prevent linker error
-#include "accountmanager.h"
-OCC::AccountManager *OCC::AccountManager::instance() { return static_cast<AccountManager *>(new QObject); }
-void OCC::AccountManager::saveAccountState(AccountState *) { }
-void OCC::AccountManager::save(bool saveCredentials) { Q_UNUSED(saveCredentials); }
-void OCC::AccountManager::accountRemoved(OCC::AccountState*) { }
-const QMetaObject OCC::AccountManager::staticMetaObject = QObject::staticMetaObject;
diff --git a/test/stubfolderman.cpp b/test/stubfolderman.cpp
new file mode 100644 (file)
index 0000000..2a60ecb
--- /dev/null
@@ -0,0 +1,11 @@
+// stub to prevent linker error
+#include "accountmanager.h"
+
+OCC::AccountManager *OCC::AccountManager::instance() { return static_cast<AccountManager *>(new QObject); }
+void OCC::AccountManager::save(bool) { }
+void OCC::AccountManager::saveAccountState(AccountState *) { }
+void OCC::AccountManager::deleteAccount(AccountState *) { }
+void OCC::AccountManager::accountRemoved(OCC::AccountState*) { }
+OCC::AccountStatePtr OCC::AccountManager::account(const QString &){ return AccountStatePtr(); }
+void OCC::AccountManager::removeAccountFolders(OCC::AccountState*) { }
+const QMetaObject OCC::AccountManager::staticMetaObject = QObject::staticMetaObject;
diff --git a/test/stubremotewipe.cpp b/test/stubremotewipe.cpp
new file mode 100644 (file)
index 0000000..fbb46d2
--- /dev/null
@@ -0,0 +1,30 @@
+// stub to prevent linker error
+#include "accountmanager.h"
+#include "accountstate.h"
+#include "socketapi.h"
+#include "folderman.h"
+
+OCC::AccountManager *OCC::AccountManager::instance() { return static_cast<AccountManager *>(new QObject); }
+void OCC::AccountManager::save(bool) { }
+OCC::AccountState *OCC::AccountManager::addAccount(const AccountPtr& ac) { return new OCC::AccountState(ac); }
+void OCC::AccountManager::deleteAccount(AccountState *) { }
+void OCC::AccountManager::accountRemoved(OCC::AccountState*) { }
+OCC::AccountStatePtr OCC::AccountManager::account(const QString &){ return AccountStatePtr(); }
+const QMetaObject OCC::AccountManager::staticMetaObject = QObject::staticMetaObject;
+
+OCC::FolderMan *OCC::FolderMan::instance() { return static_cast<FolderMan *>(new QObject); }
+void OCC::FolderMan::wipeDone(OCC::AccountState*, bool) { }
+OCC::Folder* OCC::FolderMan::addFolder(OCC::AccountState* as, OCC::FolderDefinition const &f) { return nullptr; }
+void OCC::FolderMan::slotWipeFolderForAccount(OCC::AccountState*) { }
+QString OCC::FolderMan::unescapeAlias(QString const&){ return QString(); }
+QString OCC::FolderMan::escapeAlias(QString const&){ return QString(); }
+void OCC::FolderMan::scheduleFolder(OCC::Folder*){ }
+OCC::SocketApi *OCC::FolderMan::socketApi(){ return new SocketApi;  }
+OCC::Folder::Map OCC::FolderMan::map() { return OCC::Folder::Map(); }
+void OCC::FolderMan::setSyncEnabled(bool) { }
+void OCC::FolderMan::slotSyncOnceFileUnlocks(QString const&) { }
+void OCC::FolderMan::slotScheduleETagJob(QString const&, OCC::RequestEtagJob*){ }
+OCC::Folder *OCC::FolderMan::folderForPath(QString const&) { return nullptr; }
+OCC::Folder* OCC::FolderMan::folder(QString const&) { return nullptr; }
+void OCC::FolderMan::folderSyncStateChange(OCC::Folder*) { }
+const QMetaObject OCC::FolderMan::staticMetaObject = QObject::staticMetaObject;
index 9aec8faf03d0ec0543e8b677ecd064f654a2480e..8af45f0dff4dc243b8c40f7e80f3892ab38f0465 100644 (file)
 #include "account.h"
 #include "accountstate.h"
 #include "configfile.h"
-#include "creds/httpcredentials.h"
+#include "testhelper.h"
 
 using namespace OCC;
-
-class HttpCredentialsTest : public HttpCredentials {
-public:
-    HttpCredentialsTest(const QString& user, const QString& password)
-        : HttpCredentials(user, password)
-    {}
-
-    void askFromUser() Q_DECL_OVERRIDE {
-
-    }
-};
-
-static FolderDefinition folderDefinition(const QString &path) {
-    FolderDefinition d;
-    d.localPath = path;
-    d.targetPath = path;
-    d.alias = path;
-    return d;
-}
-
+class HttpCredentials;
 
 class TestFolderMan: public QObject
 {
diff --git a/test/testhelper.h b/test/testhelper.h
new file mode 100644 (file)
index 0000000..8bd5ca6
--- /dev/null
@@ -0,0 +1,28 @@
+#ifndef TESTHELPER_H
+#define TESTHELPER_H
+
+#include "folder.h"
+#include "creds/httpcredentials.h"
+
+using namespace OCC;
+
+class HttpCredentialsTest : public HttpCredentials {
+public:
+    HttpCredentialsTest(const QString& user, const QString& password)
+    : HttpCredentials(user, password)
+    {}
+
+    void askFromUser() Q_DECL_OVERRIDE {
+
+    }
+};
+
+static FolderDefinition folderDefinition(const QString &path) {
+    FolderDefinition d;
+    d.localPath = path;
+    d.targetPath = path;
+    d.alias = path;
+    return d;
+}
+
+#endif // TESTHELPER_H
diff --git a/test/testremotewipe.cpp b/test/testremotewipe.cpp
new file mode 100644 (file)
index 0000000..5b9555e
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ *    This software is in the public domain, furnished "as is", without technical
+ *    support, and with no warranty, express or implied, as to its usefulness for
+ *    any purpose.
+ *
+ */
+
+#include <qglobal.h>
+#include <QTemporaryDir>
+#include <QtTest>
+
+#include "remotewipe.h"
+
+#include "common/utility.h"
+#include "folderman.h"
+#include "account.h"
+#include "accountstate.h"
+#include "configfile.h"
+
+#include "testhelper.h"
+
+using namespace OCC;
+
+class TestRemoteWipe: public QObject
+{
+    Q_OBJECT
+
+private slots:
+    // TODO
+    void testWipe(){
+//        QTemporaryDir dir;
+//        ConfigFile::setConfDir(dir.path()); // we don't want to pollute the user's config file
+//        QVERIFY(dir.isValid());
+
+//        QDir dirToRemove(dir.path());
+//        QVERIFY(dirToRemove.mkpath("nextcloud"));
+
+//        QString dirPath = dirToRemove.canonicalPath();
+
+//        AccountPtr account = Account::create();
+//        QVERIFY(account);
+
+//        auto manager = AccountManager::instance();
+//        QVERIFY(manager);
+
+//        AccountState *newAccountState = manager->addAccount(account);
+//        manager->save();
+//        QVERIFY(newAccountState);
+
+//        QUrl url("http://example.de");
+//        HttpCredentialsTest *cred = new HttpCredentialsTest("testuser", "secret");
+//        account->setCredentials(cred);
+//        account->setUrl( url );
+
+//        FolderMan *folderman = FolderMan::instance();
+//        folderman->addFolder(newAccountState, folderDefinition(dirPath + "/sub/nextcloud/"));
+
+//        // check if account exists
+//        qDebug() << "Does account exists?!";
+//        QVERIFY(!account->id().isEmpty());
+
+//        manager->deleteAccount(newAccountState);
+//        manager->save();
+
+//        // check if account exists
+//        qDebug() << "Does account exists yet?!";
+//        QVERIFY(account);
+
+//        // check if folder exists
+//        QVERIFY(dirToRemove.exists());
+
+//        // remote folders
+//        qDebug() <<  "Removing folder for account " << newAccountState->account()->url();
+
+//        folderman->slotWipeFolderForAccount(newAccountState);
+
+//        // check if folders dont exist anymore
+//        QCOMPARE(dirToRemove.exists(), false);
+    }
+};
+
+QTEST_APPLESS_MAIN(TestRemoteWipe)
+#include "testremotewipe.moc"