diff --git a/src/gui/aboutdialog.cpp b/src/gui/aboutdialog.cpp index 3416b7e1463..21f0c7b1545 100644 --- a/src/gui/aboutdialog.cpp +++ b/src/gui/aboutdialog.cpp @@ -20,6 +20,7 @@ #include #ifdef WITH_AUTO_UPDATER +#include "config/systemconfig.h" #include "libsync/configfile.h" #include "updater/ocupdater.h" #ifdef Q_OS_MAC @@ -80,7 +81,9 @@ void AboutDialog::setupUpdaterWidget() } } - if (!ConfigFile().skipUpdateCheck() && Updater::instance()) { + ConfigFile().makeQSettings().remove("skipUpdateCheck"); // remove old config key + + if (!SystemConfig().skipUpdateCheck() && Updater::instance()) { // Note: the sparkle-updater is not an OCUpdater if (auto *ocupdater = qobject_cast(Updater::instance())) { auto updateInfo = [ocupdater, this] { diff --git a/src/gui/creds/credentials.cpp b/src/gui/creds/credentials.cpp index 89997b34b06..7d107bb0ab4 100644 --- a/src/gui/creds/credentials.cpp +++ b/src/gui/creds/credentials.cpp @@ -15,7 +15,7 @@ #include "accessmanager.h" #include "account.h" -#include "configfile.h" +#include "config/systemconfig.h" #include "creds/credentialmanager.h" #include "oauth.h" #include "requestauthenticationcontroller.h" @@ -85,6 +85,7 @@ Credentials::Credentials(const QString &token, const QString &refreshToken, Acco , _accessToken(token) , _refreshToken(refreshToken) , _ready(false) + , _openIdConfig(SystemConfig().openIdConfig()) { if (!token.isEmpty() && !refreshToken.isEmpty()) _ready = true; @@ -286,9 +287,10 @@ void Credentials::refreshAccessTokenInternal() { if (!_account) return; + // parent with nam to ensure we reset when the nam is reset // todo: #22 - the parenting here is highly questionable, as is the use of the shared account ptr - _oAuthJob = new AccountBasedOAuth(_account, this); + _oAuthJob = new AccountBasedOAuth(_account, _openIdConfig, this); connect(_oAuthJob, &AccountBasedOAuth::refreshError, this, &Credentials::handleRefreshError); connect(_oAuthJob, &AccountBasedOAuth::refreshFinished, this, &Credentials::handleRefreshSuccess); diff --git a/src/gui/creds/credentials.h b/src/gui/creds/credentials.h index 0aa28955e1e..a6db2d96a09 100644 --- a/src/gui/creds/credentials.h +++ b/src/gui/creds/credentials.h @@ -94,6 +94,7 @@ class OWNCLOUDGUI_EXPORT Credentials : public AbstractCredentials QString _fetchErrorString; bool _ready = false; + const OpenIdConfig _openIdConfig; int _tokenRefreshRetriesCount = 0; QPointer _oAuthJob; diff --git a/src/gui/creds/oauth.cpp b/src/gui/creds/oauth.cpp index 4f09d228eff..743544e149f 100644 --- a/src/gui/creds/oauth.cpp +++ b/src/gui/creds/oauth.cpp @@ -15,6 +15,7 @@ #include "oauth.h" #include "accessmanager.h" +#include "config/systemconfig.h" #include "creds/credentialssupport.h" #include "gui/networkadapters/userinfoadapter.h" #include "libsync/creds/credentialmanager.h" @@ -42,12 +43,13 @@ namespace { const QString wellKnownPathC = QStringLiteral("/.well-known/openid-configuration"); -auto defaultOauthPromptValue() +OAuth::PromptValuesSupportedFlags defaultOauthPromptValue(const OpenIdConfig& config) { - static const auto promptValue = [] { + static const auto promptValue = [config] { + auto prompt = config.prompt(); OAuth::PromptValuesSupportedFlags out = OAuth::PromptValuesSupported::none; // convert the legacy openIdConnectPrompt() to QFlags - for (const auto &x : Theme::instance()->openIdConnectPrompt().split(QLatin1Char(' '))) { + for (const auto &x : prompt.split(QLatin1Char(' '))) { out |= Utility::stringToEnum(x); } return out; @@ -77,15 +79,14 @@ QVariant getRequiredField(const QVariantMap &json, const QString &s, QString *er } } -OAuth::OAuth(const QUrl &serverUrl, const QString &davUser, QNetworkAccessManager *networkAccessManager, QObject *parent) +OAuth::OAuth(const QUrl &serverUrl, const QString &davUser, const OpenIdConfig& openIdConfig, QNetworkAccessManager *networkAccessManager, QObject *parent) : QObject(parent) , _serverUrl(serverUrl) , _davUser(davUser) + , _openIdConfig(openIdConfig) , _networkAccessManager(networkAccessManager) - , _clientId(Theme::instance()->oauthClientId()) - , _clientSecret(Theme::instance()->oauthClientSecret()) , _redirectUrl(QString("http://localhost")) - , _supportedPromptValues(defaultOauthPromptValue()) + , _supportedPromptValues(defaultOauthPromptValue(openIdConfig)) { } @@ -96,8 +97,7 @@ void OAuth::startAuthentication() qCDebug(lcOauth) << "starting authentication"; // Listen on the socket to get a port which will be used in the redirect_uri - - QList ports = Theme::instance()->oauthPorts(); + QList ports = _openIdConfig.ports(); for (const auto port : std::as_const(ports)) { if (_server.listen(QHostAddress::LocalHost, port)) { break; @@ -326,17 +326,17 @@ QNetworkReply *OAuth::postTokenRequest(QUrlQuery &&queryItems) req.setTransferTimeout(defaultTimeoutMs()); switch (_endpointAuthMethod) { case TokenEndpointAuthMethods::client_secret_basic: - req.setRawHeader("Authorization", "Basic " + QStringLiteral("%1:%2").arg(_clientId, _clientSecret).toUtf8().toBase64()); + req.setRawHeader("Authorization", "Basic " + QStringLiteral("%1:%2").arg(_openIdConfig.clientId(), _openIdConfig.clientSecret()).toUtf8().toBase64()); break; case TokenEndpointAuthMethods::client_secret_post: - queryItems.addQueryItem(QStringLiteral("client_id"), _clientId); - queryItems.addQueryItem(QStringLiteral("client_secret"), _clientSecret); + queryItems.addQueryItem(QStringLiteral("client_id"), _openIdConfig.clientId()); + queryItems.addQueryItem(QStringLiteral("client_secret"), _openIdConfig.clientSecret()); break; } req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded; charset=UTF-8")); req.setAttribute(DontAddCredentialsAttribute, true); - queryItems.addQueryItem(QStringLiteral("scope"), QString::fromUtf8(QUrl::toPercentEncoding(Theme::instance()->openIdConnectScopes()))); + queryItems.addQueryItem(QStringLiteral("scope"), QString::fromUtf8(QUrl::toPercentEncoding(_openIdConfig.scopes()))); req.setUrl(requestTokenUrl); return _networkAccessManager->post(req, queryItems.toString(QUrl::FullyEncoded).toUtf8()); } @@ -360,10 +360,10 @@ QUrl OAuth::authorisationLink() const const QByteArray code_challenge = QCryptographicHash::hash(_pkceCodeVerifier, QCryptographicHash::Sha256).toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); - QUrlQuery query{{QStringLiteral("response_type"), QStringLiteral("code")}, {QStringLiteral("client_id"), _clientId}, + QUrlQuery query{{QStringLiteral("response_type"), QStringLiteral("code")}, {QStringLiteral("client_id"), _openIdConfig.clientId()}, {QStringLiteral("redirect_uri"), QStringLiteral("%1:%2").arg(_redirectUrl, QString::number(_server.serverPort()))}, {QStringLiteral("code_challenge"), QString::fromLatin1(code_challenge)}, {QStringLiteral("code_challenge_method"), QStringLiteral("S256")}, - {QStringLiteral("scope"), QString::fromUtf8(QUrl::toPercentEncoding(Theme::instance()->openIdConnectScopes()))}, + {QStringLiteral("scope"), QString::fromUtf8(QUrl::toPercentEncoding(_openIdConfig.scopes()))}, {QStringLiteral("prompt"), QString::fromUtf8(QUrl::toPercentEncoding(toString(_supportedPromptValues)))}, {QStringLiteral("state"), QString::fromUtf8(_state)}}; @@ -416,8 +416,8 @@ void OAuth::fetchWellKnown() _supportedPromptValues = PromptValuesSupported::none; for (const auto &x : promptValuesSupported) { const auto flag = Utility::stringToEnum(x.toString()); - // only use flags present in Theme::instance()->openIdConnectPrompt() - if (flag & defaultOauthPromptValue()) + // only use flags present in _openIdConfig->openIdConnectPrompt() + if (flag & defaultOauthPromptValue(_openIdConfig)) _supportedPromptValues |= flag; } } @@ -478,8 +478,8 @@ void OAuth::openBrowser() // todo: I was contemplating how we can make sure the passed account isn't null before we use it // to seed the OAuth ctr, and really, I'm not sure this should be a subclass of oauth in the first place. Instead it could simply use an // oauth instance to complete the tasks it can't do itself -> this could possibly be a "has a" not an "is a" impl -AccountBasedOAuth::AccountBasedOAuth(Account *account, QObject *parent) - : OAuth(account->url(), account->davUser(), account->accessManager(), parent) +AccountBasedOAuth::AccountBasedOAuth(Account *account, const OpenIdConfig& openIdConfig, QObject *parent) + : OAuth(account->url(), account->davUser(), openIdConfig, account->accessManager(), parent) , _account(account) { } diff --git a/src/gui/creds/oauth.h b/src/gui/creds/oauth.h index b6d089c4260..45661aa2314 100644 --- a/src/gui/creds/oauth.h +++ b/src/gui/creds/oauth.h @@ -17,6 +17,7 @@ #include "owncloudlib.h" #include "account.h" +#include "config/openidconfig.h" #include #include @@ -65,7 +66,7 @@ class OAuth : public QObject Q_ENUM(PromptValuesSupported) Q_DECLARE_FLAGS(PromptValuesSupportedFlags, PromptValuesSupported) - OAuth(const QUrl &serverUrl, const QString &davUser, QNetworkAccessManager *networkAccessManager, QObject *parent); + OAuth(const QUrl &serverUrl, const QString &davUser, const OpenIdConfig& openIdConfig, QNetworkAccessManager *networkAccessManager, QObject *parent); ~OAuth() override; virtual void startAuthentication(); @@ -91,13 +92,10 @@ class OAuth : public QObject QUrl _serverUrl; QString _davUser; + OpenIdConfig _openIdConfig; QNetworkAccessManager *_networkAccessManager; bool _isRefreshingToken = false; - QString _clientId; - QString _clientSecret; - - virtual void fetchWellKnown(); QNetworkReply *postTokenRequest(QUrlQuery &&queryItems); @@ -148,7 +146,7 @@ class AccountBasedOAuth : public OAuth Q_OBJECT public: - explicit AccountBasedOAuth(Account *account, QObject *parent); + explicit AccountBasedOAuth(Account *account, const OpenIdConfig& openIdConfig, QObject *parent); void startAuthentication() override; diff --git a/src/gui/creds/requestauthenticationcontroller.cpp b/src/gui/creds/requestauthenticationcontroller.cpp index 9ec7f1c7eda..315a21aff9f 100644 --- a/src/gui/creds/requestauthenticationcontroller.cpp +++ b/src/gui/creds/requestauthenticationcontroller.cpp @@ -21,6 +21,8 @@ #include "accountmodalwidget.h" #include "settingsdialog.h" +#include + namespace OCC { /** @@ -58,8 +60,9 @@ void RequestAuthenticationController::startAuthentication(Account *account) delete _oauth; _oauth = nullptr; } + SystemConfig systemConfig; _account = account; - _oauth = new AccountBasedOAuth(_account, this); + _oauth = new AccountBasedOAuth(_account, systemConfig.openIdConfig(), this); connect(_oauth, &OAuth::authorisationLinkChanged, this, &RequestAuthenticationController::authUrlReady); connect(_oauth, &OAuth::result, this, &RequestAuthenticationController::handleOAuthResult); if (_widget && _modalWidget == nullptr) { // first show of the gui diff --git a/src/gui/newaccountwizard/oauthpagecontroller.cpp b/src/gui/newaccountwizard/oauthpagecontroller.cpp index e6d6c9e7c9c..80541e1e45d 100644 --- a/src/gui/newaccountwizard/oauthpagecontroller.cpp +++ b/src/gui/newaccountwizard/oauthpagecontroller.cpp @@ -15,6 +15,7 @@ #include "accessmanager.h" #include "accountmanager.h" +#include "config/systemconfig.h" #include "networkadapters/fetchcapabilitiesadapter.h" #include "networkadapters/userinfoadapter.h" #include "networkadapters/webfingerlookupadapter.h" @@ -208,7 +209,8 @@ bool OAuthPageController::validate() _authEndpoint.clear(); _urlField->clear(); - _oauth = new OAuth(_authUrl, {}, _accessManager.get(), this); + SystemConfig systemConfig; + _oauth = new OAuth(_authUrl, {}, systemConfig.openIdConfig(), _accessManager.get(), this); // if we ever need to split out the auth link calculation, it's coming from fetchWellKnown which is a subset of // the "full" authentication routine in the oauth impl connect(_oauth, &OAuth::authorisationLinkChanged, this, &OAuthPageController::authUrlReady); diff --git a/src/gui/newaccountwizard/urlpagecontroller.cpp b/src/gui/newaccountwizard/urlpagecontroller.cpp index e1acdce40fd..4a99edc7f23 100644 --- a/src/gui/newaccountwizard/urlpagecontroller.cpp +++ b/src/gui/newaccountwizard/urlpagecontroller.cpp @@ -14,6 +14,7 @@ #include "urlpagecontroller.h" #include "accessmanager.h" +#include "config/systemconfig.h" #include "networkadapters/determineauthtypeadapter.h" #include "networkadapters/discoverwebfingerserviceadapter.h" #include "networkadapters/resolveurladapter.h" @@ -34,10 +35,21 @@ UrlPageController::UrlPageController(QWizardPage *page, AccessManager *accessMan { buildPage(); - QString themeUrl = Theme::instance()->overrideServerUrlV2(); - if (_urlField && !themeUrl.isEmpty()) { - setUrl(themeUrl); - // The theme provides the url, don't let the user change it! + if (_urlField == nullptr) { + return; + } + + SystemConfig systemConfig; + QString serverUrl = systemConfig.serverUrl(); + // no server url was given by any means, so the user has to provide one + if (serverUrl.isEmpty()) { + return; + } + setUrl(serverUrl); + + // The system admin provides the url, don't let the user change it! + bool allowServerUrlChange = systemConfig.allowServerUrlChange(); + if (!allowServerUrlChange) { _urlField->setEnabled(false); _instructionLabel->setText(tr("Your web browser will be opened to complete sign in.")); } diff --git a/src/gui/newaccountwizard/urlpagecontroller.h b/src/gui/newaccountwizard/urlpagecontroller.h index d944881056e..71e0cc60266 100644 --- a/src/gui/newaccountwizard/urlpagecontroller.h +++ b/src/gui/newaccountwizard/urlpagecontroller.h @@ -95,9 +95,9 @@ class UrlPageController : public QObject, public WizardPageValidator QPointer _page; QPointer _accessManager; - QLabel *_instructionLabel; - QLineEdit *_urlField; - QLabel *_errorField; + QLabel *_instructionLabel = nullptr; + QLineEdit *_urlField = nullptr; + QLabel *_errorField = nullptr; UrlPageResults _results; bool _urlValidated = false; diff --git a/src/gui/updater/ocupdater.cpp b/src/gui/updater/ocupdater.cpp index a4a9d9ffb8b..5b3c2dedf49 100644 --- a/src/gui/updater/ocupdater.cpp +++ b/src/gui/updater/ocupdater.cpp @@ -16,6 +16,7 @@ #include "application.h" #include "common/utility.h" #include "common/version.h" +#include "config/systemconfig.h" #include "configfile.h" #include "theme.h" @@ -39,6 +40,7 @@ namespace OCC { UpdaterScheduler::UpdaterScheduler(Application *app, QObject *parent) : QObject(parent) + , _skipUpdateCheck(SystemConfig().skipUpdateCheck()) { connect(&_updateCheckTimer, &QTimer::timeout, this, &UpdaterScheduler::slotTimerFired); @@ -85,8 +87,8 @@ void UpdaterScheduler::slotTimerFired() } // consider the skipUpdateCheck flag in the config. - if (cfg.skipUpdateCheck()) { - qCInfo(lcUpdater) << "Skipping update check because of config file"; + if (_skipUpdateCheck) { + qCInfo(lcUpdater) << "Skipping update check because of system config"; return; } diff --git a/src/gui/updater/ocupdater.h b/src/gui/updater/ocupdater.h index 8e8a945ee56..95d9d11945f 100644 --- a/src/gui/updater/ocupdater.h +++ b/src/gui/updater/ocupdater.h @@ -89,6 +89,7 @@ private Q_SLOTS: void slotTimerFired(); private: + const bool skipUpdateCheck; QTimer _updateCheckTimer; /** Timer for the regular update check. */ // make sure we are going to show only one of them at once diff --git a/src/libsync/CMakeLists.txt b/src/libsync/CMakeLists.txt index 531f77e4b88..1fb21eae1a1 100644 --- a/src/libsync/CMakeLists.txt +++ b/src/libsync/CMakeLists.txt @@ -52,6 +52,8 @@ set(libsync_SRCS abstractcorejob.cpp appprovider.cpp + config/systemconfig.cpp + config/openidconfig.cpp ) if(WIN32) diff --git a/src/libsync/config/openidconfig.cpp b/src/libsync/config/openidconfig.cpp new file mode 100644 index 00000000000..2ef1154af16 --- /dev/null +++ b/src/libsync/config/openidconfig.cpp @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2026 Thomas Müller + +#include "openidconfig.h" + +namespace OCC { + +OpenIdConfig::OpenIdConfig() +{ +} + +OpenIdConfig::OpenIdConfig(const QString &clientId, const QString &clientSecret, const QList &ports, const QString &scopes, const QString &prompt) + : _clientId(clientId) + , _clientSecret(clientSecret) + , _ports(ports) + , _scopes(scopes) + , _prompt(prompt) +{ +} + +QString OpenIdConfig::clientId() const { + return _clientId; +} + +QString OpenIdConfig::clientSecret() const { + return _clientSecret; +} + +QList OpenIdConfig::ports() const { + return _ports; +} + +QString OpenIdConfig::scopes() const { + return _scopes; +} + +QString OpenIdConfig::prompt() const { + return _prompt; +} + +bool OpenIdConfig::isValid() const +{ + return !_clientId.isEmpty() && !_ports.isEmpty(); +} + +} diff --git a/src/libsync/config/openidconfig.h b/src/libsync/config/openidconfig.h new file mode 100644 index 00000000000..515723d8e78 --- /dev/null +++ b/src/libsync/config/openidconfig.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2026 Thomas Müller + +#pragma once +#include "owncloudlib.h" + +#include +#include +#include + +namespace OCC { + +/** + * @brief The OpenIdConfig class + * @ingroup libsync + * @note This class encapsulates the configuration settings required for OpenID Connect authentication. + */ +class OWNCLOUDSYNC_EXPORT OpenIdConfig +{ +public: + OpenIdConfig(); + explicit OpenIdConfig(const QString &clientId, const QString &clientSecret, const QList &ports, const QString &scopes, const QString &prompt); + + QString clientId() const; + QString clientSecret() const; + QList ports() const; + QString scopes() const; + QString prompt() const; + + bool isValid() const; + +private: + QString _clientId; + QString _clientSecret; + QList _ports; + QString _scopes; + QString _prompt; +}; +} diff --git a/src/libsync/config/systemconfig.cpp b/src/libsync/config/systemconfig.cpp new file mode 100644 index 00000000000..f71273299dc --- /dev/null +++ b/src/libsync/config/systemconfig.cpp @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2026 Thomas Müller + +#include "systemconfig.h" + +#include "../theme.h" +#include "common/utility.h" +#include "openidconfig.h" + +#include +#include +#include + +namespace OCC { + +Q_LOGGING_CATEGORY(lcSystemConfig, "sync.systemconfig", QtInfoMsg) + +namespace chrono = std::chrono; + +SystemConfig::SystemConfig() +{ + _serverUrl = Theme::instance()->overrideServerUrlV2(); + // If a theme provides a hardcoded URL, do not allow for URL change. + _allowServerURLChange = Theme::instance()->overrideServerUrlV2().isEmpty(); + _skipUpdateCheck = false; + _openIdConfig = loadOpenIdConfigFromTheme(); + + if (!Theme::instance()->allowSystemConfigOverrides()) + return; + + // Load all overrides + + auto format = Utility::isWindows() ? QSettings::NativeFormat : QSettings::IniFormat; + const QSettings system(configPath(QOperatingSystemVersion::currentType(), *Theme::instance()), format); + + _serverUrl = system.value(SetupServerUrlKey, QString()).toString(); + if (system.contains(SetupAllowServerUrlChangeKey)) { + _allowServerURLChange = system.value(SetupAllowServerUrlChangeKey).toBool(); + } + + _skipUpdateCheck = system.value(UpdaterSkipUpdateCheckKey, false).toBool(); + + OpenIdConfig systemConfig = loadOpenIdConfigFromSystemConfig(system); + if (systemConfig.isValid()) { + qCInfo(lcSystemConfig()) << "Using OpenID config from system config"; + _openIdConfig = systemConfig; + } +} + +OpenIdConfig SystemConfig::loadOpenIdConfigFromTheme() +{ + Theme *theme = Theme::instance(); + + QString clientId = theme->oauthClientId(); + QString clientSecret = theme->oauthClientSecret(); + QVector ports = theme->oauthPorts(); + QString scopes = theme->openIdConnectScopes(); + QString prompt = theme->openIdConnectPrompt(); + + OpenIdConfig cfg(clientId, clientSecret, ports, scopes, prompt); + OC_ASSERT(cfg.isValid()); + + return cfg; +} + +OpenIdConfig SystemConfig::loadOpenIdConfigFromSystemConfig(const QSettings &system) +{ + QString clientId = system.value(OidcClientIdKey, QString()).toString(); + QString clientSecret = system.value(OidcClientSecretKey, QString()).toString(); + QString scopes = system.value(OidcScopesKey, QString()).toString(); + QString prompt = system.value(OidcPortsKey, QString()).toString(); + + QVector ports; + QVariant portsVar = system.value(OidcPortsKey, QString()).toString(); + const auto parts = portsVar.toString().split(QLatin1Char(','), Qt::SkipEmptyParts); + for (const QString &p : parts) { + bool ok = false; + const quint16 val = static_cast(p.trimmed().toUInt(&ok)); + if (ok) { + ports.append(val); + } + } + + if (ports.isEmpty()) + ports.append(0); // 0 means any port + + return OpenIdConfig(clientId, clientSecret, ports, scopes, prompt); +} + +QString SystemConfig::configPath(const QOperatingSystemVersion::OSType& os, const Theme& theme) +{ + // Important: these paths conform to how names typically work on the systems on which they are used. This includes usage of upper-/lowercase. + + if (os == QOperatingSystemVersion::Windows) { + // We use HKEY_LOCAL_MACHINE\Software\Policies since this is the location where GPO operates. + // Note: use of uppercase/camelcase is common. + return QString("HKEY_LOCAL_MACHINE\\Software\\Policies\\%1\\%2").arg(theme.vendor(), theme.appNameGUI()); + } + + if (os == QOperatingSystemVersion::MacOS) { + // We use a subfolder to have one common location where in the future more files can be stored (like icons, images and such) + // ini is used on macOS in contrary to plist because they are easier to maintain. + // Note: rev-domain notation and lowercase is typically used. + return QString("/Library/Preferences/%1/%2.ini").arg(theme.orgDomainName(), theme.appName()); + } + + // On Unix style systems, the application name in lowercase is typically used. + return QString("/etc/%1/%1.ini").arg(theme.appName()); +} + +bool SystemConfig::allowServerUrlChange() const +{ + return _allowServerURLChange; +} + +QString SystemConfig::serverUrl() const +{ + return _serverUrl; +} + +bool SystemConfig::skipUpdateCheck() const +{ + return _skipUpdateCheck; +} + +OpenIdConfig SystemConfig::openIdConfig() const +{ + return _openIdConfig; +} + +} // OCC namespace diff --git a/src/libsync/config/systemconfig.h b/src/libsync/config/systemconfig.h new file mode 100644 index 00000000000..0a0c6a82a01 --- /dev/null +++ b/src/libsync/config/systemconfig.h @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2026 Thomas Müller + +#pragma once + +#include "../theme.h" +#include "owncloudlib.h" +#include "openidconfig.h" + +#include +#include +#include + + +namespace OCC { + +/** + * @brief The SystemConfig class + * @ingroup libsync + * @note This class provides access to system-wide configuration settings. + * These settings are typically read-only and affect the behavior of the application globally. + * On Windows, settings are read from the registry path: + * HKEY_LOCAL_MACHINE\Software\Policies\\ + * On macOS, settings are read from the file: + * /Library/Preferences//.ini + * On Linux and other Unix-like systems, settings are read from the file: + * /etc//.ini + * + * @example owncloud.ini + * [Setup] + * ServerUrl=https://cloud.example.com + * AllowServerUrlChange=false + * MoveToTrash=true + * + * [Updater] + * SkipUpdateCheck=true + * + * [OpenIDConnect] + * ClientId=your-client-id + * ClientSecret=your-client-secret + * Ports=8080,8443 + * Scopes=openid offline_access email profile + * Prompt=select_account consent + * + * @example owncloud.reg (Windows Registry) + * Windows Registry Editor Version 5.00 + * + * [HKEY_LOCAL_MACHINE\Software\Policies\ownCloud\ownCloud] + * + * [HKEY_LOCAL_MACHINE\Software\Policies\ownCloud\ownCloud\Setup] + * "ServerUrl"="https://cloud.example.com" + * "AllowServerUrlChange"=dword:00000000 + * "MoveToTrash"=dword:00000001 + * + * [HKEY_LOCAL_MACHINE\Software\Policies\ownCloud\ownCloud\Updater] + * "SkipUpdateCheck"=dword:00000001 + * + * [HKEY_LOCAL_MACHINE\Software\Policies\ownCloud\ownCloud\OpenIDConnect] + * "ClientId"="your-client-id" + * "ClientSecret"="your-client-secret" + * "Ports"="8080,8443" + * "Scopes"="openid offline_access email profile" + * "Prompt"="select_account consent" + * + */ +class OWNCLOUDSYNC_EXPORT SystemConfig +{ +public: + explicit SystemConfig(); + /** + * Determine if changing the server URL is allowed based on system configuration. + * This value is only relevant if SystemConfig::serverUrl() returns a non-empty string. + * @return True if changing the server URL is allowed, false otherwise. + */ + bool allowServerUrlChange() const; + + /** + * Retrieve the server URL from the system configuration or theme. + * @return The server URL as a QString. If not set, returns an empty string. + */ + QString serverUrl() const; + + /** + * Retrieve the OpenID Connect configuration from the system configuration or the theme. + * The configuration includes client ID, client secret, ports, scopes, and prompt settings. + * The returned OpenIdConfig object may have empty values if not set in the system configuration. + * @return An OpenIdConfig object containing the OpenID Connect settings. + */ + OpenIdConfig openIdConfig() const; + + /** + * Determine if update checks should be skipped based on system configuration. + * @return True if update checks should be skipped, false otherwise. + */ + bool skipUpdateCheck() const; + + /** + * Get the path to the system configuration file or registry path based on the operating system and theme. + * @param os operating system type + * @param theme the theme instance + * @return the path to the system configuration file or registry path + * @internal kept public for testing purposes + */ + static QString configPath(const QOperatingSystemVersion::OSType& os, const Theme& theme); + +private: + static OpenIdConfig loadOpenIdConfigFromTheme(); + static OpenIdConfig loadOpenIdConfigFromSystemConfig(const QSettings &system); + +private: // System settings keys + // Setup related keys + inline static const QString SetupAllowServerUrlChangeKey = QStringLiteral("Setup/AllowServerUrlChange"); + inline static const QString SetupServerUrlKey = QStringLiteral("Setup/ServerUrl"); + // Updater related keys + inline static const QString UpdaterSkipUpdateCheckKey = QStringLiteral("Updater/SkipUpdateCheck"); + // OpenID Connect related keys + inline static const QString OidcClientIdKey = QStringLiteral("OpenIDConnect/ClientId"); + inline static const QString OidcClientSecretKey = QStringLiteral("OpenIDConnect/ClientSecret"); + inline static const QString OidcPortsKey = QStringLiteral("OpenIDConnect/Ports"); + inline static const QString OidcScopesKey = QStringLiteral("OpenIDConnect/Scopes"); + inline static const QString OidcPromptKey = QStringLiteral("OpenIDConnect/Prompt"); + +private: + bool _allowServerURLChange; + QString _serverUrl; + bool _skipUpdateCheck; + OpenIdConfig _openIdConfig; +}; + +} // OCC namespace diff --git a/src/libsync/configfile.cpp b/src/libsync/configfile.cpp index 526ad5c0dbf..dcab035d71c 100644 --- a/src/libsync/configfile.cpp +++ b/src/libsync/configfile.cpp @@ -15,6 +15,8 @@ #include "common/asserts.h" #include "common/utility.h" #include "common/version.h" + +#include "config/systemconfig.h" #ifdef Q_OS_WIN #include "common/utility_win.h" #endif @@ -57,7 +59,6 @@ const QString optionalDesktopNotificationsC() { return QStringLiteral("optionalDesktopNotifications"); } -const QString skipUpdateCheckC() { return QStringLiteral("skipUpdateCheck"); } const QString updateCheckIntervalC() { return QStringLiteral("updateCheckInterval"); } const QString uiLanguageC() { return QStringLiteral("uiLanguage"); } const QString geometryC() { return QStringLiteral("geometry"); } @@ -210,26 +211,6 @@ bool ConfigFile::restoreGeometryHeader(QHeaderView *header) return false; } -QVariant ConfigFile::getPolicySetting(const QString &setting, const QVariant &defaultValue) const -{ - if (Utility::isWindows()) { - // check for policies first and return immediately if a value is found. - QSettings userPolicy(QStringLiteral("HKEY_CURRENT_USER\\Software\\Policies\\%1\\%2").arg(Theme::instance()->vendor(), Theme::instance()->appNameGUI()), - QSettings::NativeFormat); - if (userPolicy.contains(setting)) { - return userPolicy.value(setting); - } - - QSettings machinePolicy( - QStringLiteral("HKEY_LOCAL_MACHINE\\Software\\Policies\\%1\\%2").arg(Theme::instance()->vendor(), Theme::instance()->appNameGUI()), - QSettings::NativeFormat); - if (machinePolicy.contains(setting)) { - return machinePolicy.value(setting); - } - } - return defaultValue; -} - QString ConfigFile::configPath() { if (_confDir.isEmpty()) { @@ -462,32 +443,6 @@ chrono::milliseconds ConfigFile::updateCheckInterval(const QString &connection) return interval; } -bool ConfigFile::skipUpdateCheck(const QString &connection) const -{ - QString con(connection); - if (connection.isEmpty()) - con = defaultConnection(); - - QVariant fallback = getValue(skipUpdateCheckC(), con, false); - fallback = getValue(skipUpdateCheckC(), QString(), fallback); - - QVariant value = getPolicySetting(skipUpdateCheckC(), fallback); - return value.toBool(); -} - -void ConfigFile::setSkipUpdateCheck(bool skip, const QString &connection) -{ - QString con(connection); - if (connection.isEmpty()) - con = defaultConnection(); - - auto settings = makeQSettings(); - settings.beginGroup(con); - - settings.setValue(skipUpdateCheckC(), QVariant(skip)); - settings.sync(); -} - QString ConfigFile::uiLanguage() const { auto settings = makeQSettings(); @@ -518,33 +473,11 @@ void ConfigFile::setProxyType(QNetworkProxy::ProxyType proxyType, const QString QVariant ConfigFile::getValue(const QString ¶m, const QString &group, const QVariant &defaultValue) const { - QVariant systemSetting; - if (Utility::isMac()) { - QSettings systemSettings(QStringLiteral("/Library/Preferences/%1.plist").arg(Theme::instance()->orgDomainName()), QSettings::NativeFormat); - if (!group.isEmpty()) { - systemSettings.beginGroup(group); - } - systemSetting = systemSettings.value(param, defaultValue); - } else if (Utility::isUnix()) { - QSettings systemSettings(QStringLiteral(SYSCONFDIR "/%1/%1.conf").arg(Theme::instance()->appName()), QSettings::NativeFormat); - if (!group.isEmpty()) { - systemSettings.beginGroup(group); - } - systemSetting = systemSettings.value(param, defaultValue); - } else { // Windows - QSettings systemSettings( - QStringLiteral("HKEY_LOCAL_MACHINE\\Software\\%1\\%2").arg(Theme::instance()->vendor(), Theme::instance()->appNameGUI()), QSettings::NativeFormat); - if (!group.isEmpty()) { - systemSettings.beginGroup(group); - } - systemSetting = systemSettings.value(param, defaultValue); - } - auto settings = makeQSettings(); if (!group.isEmpty()) settings.beginGroup(group); - return settings.value(param, systemSetting); + return settings.value(param, defaultValue); } void ConfigFile::setValue(const QString &key, const QVariant &value) @@ -594,7 +527,7 @@ void ConfigFile::setPauseSyncWhenMetered(bool isChecked) bool ConfigFile::moveToTrash() const { - auto defaultValue = Theme::instance()->moveToTrashDefaultValue(); + bool defaultValue = Theme::instance()->moveToTrashDefaultValue(); return getValue(moveToTrashC(), QString(), defaultValue).toBool(); } diff --git a/src/libsync/configfile.h b/src/libsync/configfile.h index e241f35aff9..1ac372530f5 100644 --- a/src/libsync/configfile.h +++ b/src/libsync/configfile.h @@ -144,9 +144,6 @@ class OWNCLOUDSYNC_EXPORT ConfigFile // how often the check about new versions runs std::chrono::milliseconds updateCheckInterval(const QString &connection = QString()) const; - bool skipUpdateCheck(const QString &connection = QString()) const; - void setSkipUpdateCheck(bool, const QString &); - QString uiLanguage() const; void setUiLanguage(const QString &uiLanguage); @@ -165,7 +162,6 @@ class OWNCLOUDSYNC_EXPORT ConfigFile static void setupDefaultExcludeFilePaths(ExcludedFiles &excludedFiles); protected: - QVariant getPolicySetting(const QString &policy, const QVariant &defaultValue = QVariant()) const; void storeData(const QString &group, const QString &key, const QVariant &value); void removeData(const QString &group, const QString &key); bool dataExists(const QString &group, const QString &key) const; diff --git a/src/libsync/owncloudtheme.cpp b/src/libsync/owncloudtheme.cpp index 55e4ff906c9..b71f44a43fe 100644 --- a/src/libsync/owncloudtheme.cpp +++ b/src/libsync/owncloudtheme.cpp @@ -51,4 +51,9 @@ bool ownCloudTheme::moveToTrashDefaultValue() const // for the vanilla ownCloud client move-to-trash option is enabled by default return true; } + +bool ownCloudTheme::allowSystemConfigOverrides() const +{ + return true; +} } diff --git a/src/libsync/owncloudtheme.h b/src/libsync/owncloudtheme.h index 82f6e3a6591..801e5c9afbe 100644 --- a/src/libsync/owncloudtheme.h +++ b/src/libsync/owncloudtheme.h @@ -32,5 +32,6 @@ class ownCloudTheme : public Theme QIcon wizardHeaderLogo() const override; QIcon aboutIcon() const override; bool moveToTrashDefaultValue() const override; + bool allowSystemConfigOverrides() const override; }; } diff --git a/src/libsync/theme.cpp b/src/libsync/theme.cpp index 8b81c79c090..396a11216f5 100644 --- a/src/libsync/theme.cpp +++ b/src/libsync/theme.cpp @@ -562,6 +562,11 @@ bool Theme::moveToTrashDefaultValue() const return false; } +bool Theme::allowSystemConfigOverrides() const +{ + return false; +} + bool Theme::syncNewlyDiscoveredSpaces() const { return false; diff --git a/src/libsync/theme.h b/src/libsync/theme.h index 12fb7ff628d..429704da74d 100644 --- a/src/libsync/theme.h +++ b/src/libsync/theme.h @@ -481,6 +481,12 @@ class OWNCLOUDSYNC_EXPORT Theme : public QObject */ virtual bool moveToTrashDefaultValue() const; + /** + * @brief Allow the system configuration to override theme values. + * @default false + */ + virtual bool allowSystemConfigOverrides() const; + /** * @brief Automatically add sync connections for newly discovered Spaces. * @@ -529,4 +535,4 @@ class OWNCLOUDSYNC_EXPORT Theme : public QObject bool _mono = false; }; -} \ No newline at end of file +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 685efd9d7f7..3bbbe5a571f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -22,6 +22,7 @@ endif() owncloud_add_test(ExcludedFiles) owncloud_add_test(Utility) +owncloud_add_test(SystemConfig ../src/libsync/owncloudtheme.cpp) owncloud_add_test(SyncEngine) owncloud_add_test(SyncMove) diff --git a/test/testoauth.cpp b/test/testoauth.cpp index 77d9edf0697..30a8336eae4 100644 --- a/test/testoauth.cpp +++ b/test/testoauth.cpp @@ -13,6 +13,8 @@ #include "testutils/syncenginetestutils.h" #include "theme.h" +#include + using namespace std::chrono_literals; using namespace OCC; @@ -155,7 +157,8 @@ class OAuthTestCase : public QObject QObject::connect(&desktopServiceHook, &DesktopServiceHook::hooked, this, &OAuthTestCase::openBrowserHook); - auto out = std::make_unique(account, nullptr); + SystemConfig config; + auto out = std::make_unique(account, config.openIdConfig(), nullptr); QObject::connect(out.get(), &OAuth::result, this, &OAuthTestCase::oauthResult); return out; } diff --git a/test/testsystemconfig.cpp b/test/testsystemconfig.cpp new file mode 100644 index 00000000000..faf40abd6b4 --- /dev/null +++ b/test/testsystemconfig.cpp @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "testutils/testutils.h" + +#include "libsync/owncloudtheme.h" +#include "libsync/config/systemconfig.h" + +class TestSystemConfig : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testConfigPath() + { + auto t = OCC::ownCloudTheme(); + QCOMPARE(OCC::SystemConfig::configPath(QOperatingSystemVersion::Windows, t), QString("HKEY_LOCAL_MACHINE\\Software\\Policies\\ownCloud\\ownCloud")); + QCOMPARE(OCC::SystemConfig::configPath(QOperatingSystemVersion::MacOS, t), QString("/Library/Preferences/com.owncloud.desktopclient/ownCloud.ini")); + QCOMPARE(OCC::SystemConfig::configPath(QOperatingSystemVersion::Unknown, t), QString("/etc/ownCloud/ownCloud.ini")); + } +}; + +QTEST_GUILESS_MAIN(TestSystemConfig) +#include "testsystemconfig.moc"