/* * Copyright (C) by Krzesimir Nowak * Copyright (C) by Klaas Freitag * * 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 #include #include #include #include #include "creds/shibbolethcredentials.h" #include "creds/shibboleth/shibbolethwebview.h" #include "creds/shibboleth/shibbolethrefresher.h" #include "creds/shibbolethcredentials.h" #include "shibboleth/shibbolethuserjob.h" #include "creds/credentialscommon.h" #include "mirall/mirallaccessmanager.h" #include "mirall/account.h" #include "mirall/theme.h" #include "mirall/cookiejar.h" #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) #include #else #include #endif using namespace QKeychain; namespace Mirall { namespace { // Not "user" because it has a special meaning for http const char userC[] = "shib_user"; const char shibCookieNameC[] = "_shibsession_"; int shibboleth_redirect_callback(CSYNC* csync_ctx, const char* uri) { if (!csync_ctx || !uri) { return 1; } const QString qurl(QString::fromLatin1(uri)); QRegExp shibbolethyWords ("SAML|wayf"); shibbolethyWords.setCaseSensitivity (Qt::CaseInsensitive); if (!qurl.contains(shibbolethyWords)) { return 1; } QMutex mutex; QMutexLocker locker(&mutex); Account *account = AccountManager::instance()->account(); ShibbolethCredentials* creds = qobject_cast(account->credentials()); if (!creds) { qDebug() << "Not a Shibboleth creds instance!"; return 1; } ShibbolethRefresher refresher(account, creds, csync_ctx); // blocks refresher.refresh(); return creds->ready() ? 0 : 1; } } // ns ShibbolethCredentials::ShibbolethCredentials() : AbstractCredentials(), _url(), _ready(false), _stillValid(false), _browser(0) {} void ShibbolethCredentials::syncContextPreInit(CSYNC* ctx) { csync_set_auth_callback (ctx, handleNeonSSLProblems); } QByteArray ShibbolethCredentials::prepareCookieData() const { QString cookiesAsString; // TODO: This should not be a part of this method, but we don't // have any way to get "session_key" module property from // csync. Had we have it, then we could just append shibboleth // cookies to the "session_key" value and set it in csync module. Account *account = AccountManager::instance()->account(); QList cookies = accountCookies(account); foreach(const QNetworkCookie &cookie, cookies) { cookiesAsString += cookie.toRawForm(QNetworkCookie::NameAndValueOnly) + QLatin1String("; "); } return cookiesAsString.toLatin1(); } void ShibbolethCredentials::syncContextPreStart (CSYNC* ctx) { typedef int (*csync_owncloud_redirect_callback_t)(CSYNC* ctx, const char* uri); csync_owncloud_redirect_callback_t cb = shibboleth_redirect_callback; csync_set_module_property(ctx, "session_key", prepareCookieData().data()); csync_set_module_property(ctx, "redirect_callback", &cb); } bool ShibbolethCredentials::changed(AbstractCredentials* credentials) const { ShibbolethCredentials* other(dynamic_cast< ShibbolethCredentials* >(credentials)); if (_shibCookie != other->_shibCookie || _user != other->_user) { return true; } return false; } QString ShibbolethCredentials::authType() const { return QString::fromLatin1("shibboleth"); } QString ShibbolethCredentials::user() const { return _user; } QNetworkAccessManager* ShibbolethCredentials::getQNAM() const { QNetworkAccessManager* qnam(new MirallAccessManager); connect(qnam, SIGNAL(finished(QNetworkReply*)), this, SLOT(slotReplyFinished(QNetworkReply*))); return qnam; } void ShibbolethCredentials::slotReplyFinished(QNetworkReply* r) { QVariant target = r->attribute(QNetworkRequest::RedirectionTargetAttribute); if (target.isValid()) { _stillValid = false; qWarning() << Q_FUNC_INFO << "detected redirect, will open Login Window"; // will be done in NetworkJob's finished signal } else { //_stillValid = true; // gets set when reading from keychain or getting it from browser } } bool ShibbolethCredentials::ready() const { return _ready; } void ShibbolethCredentials::fetch(Account *account) { if (_user.isEmpty()) { _user = account->credentialSetting(QLatin1String(userC)).toString(); } if (_ready) { Q_EMIT fetched(); } else { if (account) { _url = account->url(); } ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName()); job->setSettings(account->settingsWithGroup(Theme::instance()->appName(), job)); job->setInsecureFallback(false); job->setKey(keychainKey(account->url().toString(), "shibAssertion")); job->setProperty("account", QVariant::fromValue(account)); connect(job, SIGNAL(finished(QKeychain::Job*)), SLOT(slotReadJobDone(QKeychain::Job*))); job->start(); } } bool ShibbolethCredentials::stillValid(QNetworkReply *reply) { Q_UNUSED(reply) return _stillValid; } void ShibbolethCredentials::persist(Account* account) { storeShibCookie(_shibCookie, account); if (!_user.isEmpty()) { account->setCredentialSetting(QLatin1String(userC), _user); } } // only used by Application::slotLogout(). Use invalidateAndFetch for normal usage void ShibbolethCredentials::invalidateToken(Account *account) { Q_UNUSED(account) if (!removeFromCookieJar(_shibCookie)) { qDebug() << "invalidateToken() called but no shibCookie in in cookie jar!"; } removeShibCookie(account); _shibCookie = QNetworkCookie(); // ### access to ctx missing, but might not be required at all //csync_set_module_property(ctx, "session_key", ""); } void ShibbolethCredentials::onShibbolethCookieReceived(const QNetworkCookie& shibCookie, Account *account) { storeShibCookie(shibCookie, account); _shibCookie = shibCookie; addToCookieJar(shibCookie); // Now fetch the user... // But we must first do a request to webdav so the session is enabled. // (because for some reason we wan't access the API without that.. a bug in the server maybe?) EntityExistsJob* job = new EntityExistsJob(account, account->davPath(), this); connect(job, SIGNAL(exists(QNetworkReply*)), this, SLOT(slotFetchUser())); job->setIgnoreCredentialFailure(true); job->start(); } void ShibbolethCredentials::slotFetchUser() { AbstractNetworkJob* oldjob = qobject_cast(sender()); Q_ASSERT(oldjob); ShibbolethUserJob *job = new ShibbolethUserJob(oldjob->account(), this); connect(job, SIGNAL(userFetched(QString)), this, SLOT(slotUserFetched(QString))); job->start(); } void ShibbolethCredentials::slotUserFetched(const QString &user) { ShibbolethUserJob *job = qobject_cast(sender()); Q_ASSERT(job); if (_user.isEmpty()) { _user = user; } else if (user != _user) { qDebug() << "Wrong user: " << user << "!=" << _user; QMessageBox::warning(_browser, tr("Login Error"), tr("You must sign in as user %1").arg(_user)); invalidateToken(job->account()); showLoginWindow(job->account()); return; } _stillValid = true; _ready = true; Q_EMIT fetched(); } void ShibbolethCredentials::slotBrowserAccepted() { _ready = false; Q_EMIT fetched(); } void ShibbolethCredentials::invalidateAndFetch(Account* account) { _ready = false; // delete the credentials, then in the slot fetch them again (which will trigger browser) DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName()); job->setProperty("account", QVariant::fromValue(account)); job->setSettings(account->settingsWithGroup(Theme::instance()->appName(), job)); connect(job, SIGNAL(finished(QKeychain::Job*)), SLOT(slotInvalidateAndFetchInvalidateDone(QKeychain::Job*))); job->setKey(keychainKey(account->url().toString(), "shibAssertion")); job->start(); } void ShibbolethCredentials::slotInvalidateAndFetchInvalidateDone(QKeychain::Job* job) { Account *account = qvariant_cast(job->property("account")); connect (this, SIGNAL(fetched()), this, SLOT(onFetched())); // small hack to support the ShibbolethRefresher hack // we already rand fetch() with a valid account object, // and hence know the url on refresh fetch(account); } void ShibbolethCredentials::onFetched() { disconnect (this, SIGNAL(fetched()), this, SLOT(onFetched())); Q_EMIT invalidatedAndFetched(prepareCookieData()); } void ShibbolethCredentials::slotReadJobDone(QKeychain::Job *job) { Account *account = qvariant_cast(job->property("account")); if (job->error() == QKeychain::NoError) { ReadPasswordJob *readJob = static_cast(job); delete readJob->settings(); QList cookies = QNetworkCookie::parseCookies(readJob->textData().toUtf8()); if (cookies.count() > 0) { _shibCookie = cookies.first(); addToCookieJar(_shibCookie); } // access job->setSettings(account->settingsWithGroup(Theme::instance()->appName(), job)); _ready = true; _stillValid = true; Q_EMIT fetched(); } else { showLoginWindow(account); } } void ShibbolethCredentials::showLoginWindow(Account* account) { if (!_browser.isNull()) { _browser->activateWindow(); _browser->raise(); // FIXME On OS X this does not raise properly return; } _browser = new ShibbolethWebView(account); connect(_browser, SIGNAL(shibbolethCookieReceived(QNetworkCookie, Account*)), this, SLOT(onShibbolethCookieReceived(QNetworkCookie, Account*))); connect(_browser, SIGNAL(accepted()), this, SLOT(slotBrowserAccepted())); // FIXME If the browser was hidden (e.g. user closed it) without us logging in, the logic gets stuck // and can only be unstuck by restarting the app or pressing "Sign in" (we should switch to offline but we don't) _browser->show(); } QList ShibbolethCredentials::accountCookies(Account *account) { return account->networkAccessManager()->cookieJar()->cookiesForUrl(account->url()); } QNetworkCookie ShibbolethCredentials::findShibCookie(Account *account, QList cookies) { if(cookies.isEmpty()) { cookies = accountCookies(account); } Q_FOREACH(QNetworkCookie cookie, cookies) { if (cookie.name().startsWith(shibCookieNameC)) { return cookie; } } return QNetworkCookie(); } QByteArray ShibbolethCredentials::shibCookieName() { return QByteArray(shibCookieNameC); } void ShibbolethCredentials::storeShibCookie(const QNetworkCookie &cookie, Account *account) { WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName()); job->setSettings(account->settingsWithGroup(Theme::instance()->appName(), job)); // we don't really care if it works... //connect(job, SIGNAL(finished(QKeychain::Job*)), SLOT(slotWriteJobDone(QKeychain::Job*))); job->setKey(keychainKey(account->url().toString(), "shibAssertion")); job->setTextData(QString::fromUtf8(cookie.toRawForm())); job->start(); } void ShibbolethCredentials::removeShibCookie(Account *account) { DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName()); job->setSettings(account->settingsWithGroup(Theme::instance()->appName(), job)); job->setKey(keychainKey(account->url().toString(), "shibAssertion")); job->start(); } void ShibbolethCredentials::addToCookieJar(const QNetworkCookie &cookie) { QList cookies; cookies << cookie; Account *account = AccountManager::instance()->account(); QNetworkCookieJar *jar = account->networkAccessManager()->cookieJar(); jar->blockSignals(true); // otherwise we'd call ourselves jar->setCookiesFromUrl(cookies, account->url()); jar->blockSignals(false); } bool ShibbolethCredentials::removeFromCookieJar(const QNetworkCookie &cookie) { Account *account = AccountManager::instance()->account(); CookieJar *jar = qobject_cast(account->networkAccessManager()->cookieJar()); return jar->deleteCookie(cookie); } } // ns Mirall