File indexing completed on 2024-12-08 04:33:09

0001 /*
0002 
0003  * SPDX-FileCopyrightText: 2020 Alessandro Ambrosano <alessandro.ambrosano@gmail.com>
0004  *
0005  * SPDX-License-Identifier: LGPL-2.0-or-later
0006  *
0007  */
0008 
0009 #include "ddpapi/ddpauthenticationmanager.h"
0010 
0011 #include "ddpapi/ddpauthenticationmanagerutils.h"
0012 #include "ddpapi/ddpclient.h"
0013 
0014 #include "ruqola_ddpapi_debug.h"
0015 
0016 #include "utils.h"
0017 
0018 #include <QByteArray>
0019 #include <QJsonArray>
0020 
0021 #define sl(x) QStringLiteral(x)
0022 
0023 QString DDPAuthenticationManager::METHOD_LOGIN = sl("login");
0024 QString DDPAuthenticationManager::METHOD_SEND_OTP = sl("login");
0025 QString DDPAuthenticationManager::METHOD_LOGOUT = sl("logout");
0026 QString DDPAuthenticationManager::METHOD_LOGOUT_CLEAN_UP = sl("logoutCleanUp");
0027 
0028 DDPAuthenticationManager::DDPAuthenticationManager(DDPClient *ddpClient, QObject *parent)
0029     : DDPManager(ddpClient, parent)
0030 {
0031     connect(ddpClient, &DDPClient::connectedChanged, this, &DDPAuthenticationManager::clientConnectedChangedSlot);
0032     connect(ddpClient, &DDPClient::connecting, this, [this]() {
0033         setLoginStatus(LoginStatus::Connecting);
0034     });
0035 }
0036 
0037 DDPAuthenticationManager::~DDPAuthenticationManager() = default;
0038 
0039 void DDPAuthenticationManager::setAuthToken(const QString &authToken)
0040 {
0041     mAuthToken = authToken;
0042 }
0043 
0044 void DDPAuthenticationManager::login()
0045 {
0046     if (mAuthToken.isNull()) {
0047         qCWarning(RUQOLA_DDPAPI_LOG) << "No auth token available, can't login.";
0048         return;
0049     }
0050 
0051     loginImpl(DDPAuthenticationManagerUtils::loginResume(mAuthToken));
0052 }
0053 
0054 void DDPAuthenticationManager::login(const QString &user, const QString &password)
0055 {
0056     loginImpl(DDPAuthenticationManagerUtils::login(user, password));
0057 }
0058 
0059 void DDPAuthenticationManager::loginLDAP(const QString &user, const QString &password)
0060 {
0061     loginImpl(DDPAuthenticationManagerUtils::loginLdap(user, password));
0062 }
0063 
0064 void DDPAuthenticationManager::loginOAuth(const QString &credentialToken, const QString &credentialSecret)
0065 {
0066     loginImpl(DDPAuthenticationManagerUtils::loginOAuth(credentialToken, credentialSecret));
0067 }
0068 
0069 void DDPAuthenticationManager::loginImpl(const QJsonArray &params)
0070 {
0071     if (checkGenericError()) {
0072         return;
0073     }
0074 
0075     if (mLoginStatus == LoginOngoing) {
0076         qCWarning(RUQOLA_DDPAPI_LOG) << "A login operation is already ongoing, dropping request.";
0077         return;
0078     }
0079 
0080     if (mLoginStatus == LoggedIn) {
0081         qCWarning(RUQOLA_DDPAPI_LOG) << "User is already logged in on this server, ignoring.";
0082         return;
0083     }
0084 
0085     // TODO: sanity checks on params
0086 
0087     mLastLoginPayload = params[0].toObject();
0088     ddpClient()->invokeMethodAndRegister(METHOD_LOGIN, params, this, static_cast<int>(Method::Login));
0089     setLoginStatus(LoginStatus::LoginOngoing);
0090 }
0091 
0092 void DDPAuthenticationManager::sendOTP(const QString &otpCode)
0093 {
0094     if (checkGenericError()) {
0095         return;
0096     }
0097 
0098     if (mLoginStatus == LoginStatus::LoginOtpAuthOngoing) {
0099         qCWarning(RUQOLA_DDPAPI_LOG) << Q_FUNC_INFO << "Another OTP authentication is going on.";
0100         return;
0101     }
0102 
0103     //    if ((mLoginStatus != LoginStatus::LoginOtpRequired) && (mLoginStatus != LoginStatus::LoginFailedInvalidOtp)) {
0104     //        qCWarning(RUQOLA_DDPAPI_LOG) << Q_FUNC_INFO << "Trying to send OTP but none was requested by the server.";
0105     //        return;
0106     //    }
0107     ddpClient()->invokeMethodAndRegister(METHOD_SEND_OTP,
0108                                          DDPAuthenticationManagerUtils::sendOTP(otpCode, mLastLoginPayload),
0109                                          this,
0110                                          static_cast<int>(Method::SendOtp));
0111     setLoginStatus(LoginStatus::LoginOtpAuthOngoing);
0112 }
0113 
0114 void DDPAuthenticationManager::logout()
0115 {
0116     if (checkGenericError()) {
0117         return;
0118     }
0119 
0120     if (mLoginStatus == LoginStatus::LogoutOngoing) {
0121         qCWarning(RUQOLA_DDPAPI_LOG) << Q_FUNC_INFO << "Another logout operation is ongoing.";
0122         return;
0123     }
0124 
0125     if (isLoggedOut()) {
0126         qCWarning(RUQOLA_DDPAPI_LOG) << Q_FUNC_INFO << "User is already logged out.";
0127         return;
0128     }
0129 
0130     const QString params = sl("[]");
0131 
0132     ddpClient()->invokeMethodAndRegister(METHOD_LOGOUT, Utils::strToJsonArray(params), this, static_cast<int>(Method::SendOtp));
0133     setLoginStatus(LoginStatus::LogoutOngoing);
0134 }
0135 
0136 QString DDPAuthenticationManager::userId() const
0137 {
0138     return mUserId;
0139 }
0140 
0141 QString DDPAuthenticationManager::authToken() const
0142 {
0143     return mAuthToken;
0144 }
0145 
0146 void DDPAuthenticationManager::processMethodResponseImpl(int operationId, const QJsonObject &response)
0147 {
0148     switch (static_cast<Method>(operationId)) {
0149     case Method::Login: // intentional fall-through
0150     case Method::SendOtp:
0151         if (response.contains(sl("result"))) {
0152             const QJsonObject result = response[sl("result")].toObject();
0153             mAuthToken = result[sl("token")].toString();
0154             mUserId = result[sl("id")].toString();
0155             mTokenExpires = result[sl("tokenExpires")].toObject()[sl("$date")].toDouble();
0156             setLoginStatus(LoggedIn);
0157         }
0158 
0159         if (response.contains(sl("error"))) {
0160             const QJsonValue errorCode = response[sl("error")].toObject()[sl("error")];
0161             qCWarning(RUQOLA_DDPAPI_LOG) << "Login Error: " << response;
0162             // TODO: to be more user friendly, there would need to be more context
0163             // in case of a 403 error, as it may be received in different cases:
0164             //   - When logging in with user and password -> invalid username or password
0165             //   - When resuming an older login with an invalid / expired auth token -> invalid or expired token
0166             //   - When logging in with an invalid / expired OAuth token (e.g. google, facebook, etc.) -> invalid or expired token
0167             if (errorCode.isDouble() && errorCode.toInt() == 403) {
0168                 qCWarning(RUQOLA_DDPAPI_LOG) << "Invalid username or password.";
0169                 setLoginStatus(LoginFailedInvalidUserOrPassword);
0170             } else if (errorCode.isString() && errorCode.toString() == sl("totp-required")) {
0171                 qCWarning(RUQOLA_DDPAPI_LOG) << "Two factor authentication is enabled on the server."
0172                                              << "A one-time password is required to complete the login procedure.";
0173                 setLoginStatus(LoginOtpRequired);
0174             } else if (errorCode.isString() && errorCode.toString() == sl("totp-invalid")) {
0175                 qCWarning(RUQOLA_DDPAPI_LOG) << "Invalid OTP code.";
0176                 setLoginStatus(LoginFailedInvalidOtp);
0177             } else if (errorCode.isString() && errorCode.toString() == sl("error-user-is-not-activated")) {
0178                 qCWarning(RUQOLA_DDPAPI_LOG) << "User is not activated.";
0179                 setLoginStatus(LoginFailedUserNotActivated);
0180             } else if (errorCode.isString() && errorCode.toString() == sl("error-login-blocked-for-ip")) {
0181                 qCWarning(RUQOLA_DDPAPI_LOG) << "Login has been temporarily blocked For IP.";
0182                 setLoginStatus(LoginFailedLoginBlockForIp);
0183             } else if (errorCode.isString() && errorCode.toString() == sl("error-login-blocked-for-user")) {
0184                 qCWarning(RUQOLA_DDPAPI_LOG) << "Login has been temporarily blocked For User.";
0185                 setLoginStatus(LoginFailedLoginBlockedForUser);
0186             } else if (errorCode.isString() && errorCode.toString() == sl("error-app-user-is-not-allowed-to-login")) {
0187                 qCWarning(RUQOLA_DDPAPI_LOG) << "App user is not allowed to login.";
0188                 setLoginStatus(LoginFailedLoginAppNotAllowedToLogin);
0189             } else {
0190                 qCWarning(RUQOLA_DDPAPI_LOG) << "Generic error during login. Couldn't process" << response;
0191                 setLoginStatus(GenericError);
0192             }
0193             return;
0194         }
0195 
0196         break;
0197 
0198     case Method::Logout:
0199         // Don't really expect any errors here, except maybe when logging out without
0200         // being logged in. That is being taken care of in DDPAuthenticationManager::logout.
0201         // Printing any error message that may come up just in case, and preventing any other
0202         // operations by switching to GenericError state.
0203         if (response.contains(sl("error"))) {
0204             qCWarning(RUQOLA_DDPAPI_LOG) << "Error while logging out. Server response:" << response;
0205             setLoginStatus(GenericError);
0206             return;
0207         }
0208 
0209         setLoginStatus(LoggedOut);
0210         break;
0211 
0212     case Method::LogoutCleanUp:
0213         // Maybe the clean up request payload is corrupted
0214         if (response.contains(sl("error"))) {
0215             const QJsonValue errorCode = response[sl("error")].toObject()[sl("error")];
0216             qCWarning(RUQOLA_DDPAPI_LOG) << "Couldn't clean up on logout. Server response:" << response << " error code " << errorCode;
0217             // If we get here we're likely getting something wrong from the UI.
0218             // Need to prevent any further operation from now on.
0219             setLoginStatus(GenericError);
0220             return;
0221         }
0222 
0223         setLoginStatus(LoggedOutAndCleanedUp);
0224         break;
0225     }
0226 }
0227 
0228 DDPAuthenticationManager::LoginStatus DDPAuthenticationManager::loginStatus() const
0229 {
0230     return mLoginStatus;
0231 }
0232 
0233 bool DDPAuthenticationManager::isLoggedIn() const
0234 {
0235     return mLoginStatus == DDPAuthenticationManager::LoggedIn;
0236 }
0237 
0238 bool DDPAuthenticationManager::isLoggedOut() const
0239 {
0240     return mLoginStatus == DDPAuthenticationManager::LoggedOut || mLoginStatus == DDPAuthenticationManager::LogoutCleanUpOngoing
0241         || mLoginStatus == DDPAuthenticationManager::LoggedOutAndCleanedUp;
0242 }
0243 
0244 void DDPAuthenticationManager::setLoginStatus(DDPAuthenticationManager::LoginStatus status)
0245 {
0246     if (mLoginStatus != status) {
0247         mLoginStatus = status;
0248         Q_EMIT loginStatusChanged();
0249     }
0250 }
0251 
0252 qint64 DDPAuthenticationManager::tokenExpires() const
0253 {
0254     return mTokenExpires;
0255 }
0256 
0257 void DDPAuthenticationManager::clientConnectedChangedSlot()
0258 {
0259     if (mLoginStatus == DDPAuthenticationManager::FailedToLoginPluginProblem) {
0260         return;
0261     }
0262     if (checkGenericError()) {
0263         return;
0264     }
0265     // Just connected -> not logged in yet -> state = LoggedOut
0266     // Just disconnected -> whatever state we're in, need to change to LoggedOut
0267     setLoginStatus(LoginStatus::LoggedOut);
0268 }
0269 
0270 bool DDPAuthenticationManager::checkGenericError() const
0271 {
0272     if (mLoginStatus == LoginStatus::GenericError) {
0273         qCWarning(RUQOLA_DDPAPI_LOG) << Q_FUNC_INFO << "The authentication manager is in an irreversible error state and can't perform any operation.";
0274     }
0275 
0276     return mLoginStatus == LoginStatus::GenericError;
0277 }
0278 
0279 #undef sl
0280 
0281 #include "moc_ddpauthenticationmanager.cpp"