Skip to content

feature: managed settings. - #10589

Open
camilasan wants to merge 9 commits into
masterfrom
feature/5497/mdm
Open

camilasan wants to merge 9 commits into
masterfrom
feature/5497/mdm

Conversation

@camilasan

@camilasan camilasan commented Aug 12, 2026

Copy link
Copy Markdown
Member

Resolves

#5497

This PR is branched off #9191 (the first 4 commits).

Summary

Lets an administrator set defaults and enforce client settings across their users, through the Nextcloud server (Enterprise) and through device policy (Windows, macOS, Linux). Settings resolve through one enforcement aware path with a clear precedence, and enforced controls are disabled in the UI with a "Managed by ..." label.

How it works

  • ConfigFile::getConfig resolves each setting across a priority ordered stack: device enforced, server enforced, user, server default, device default, built in default.
  • Sources are device policy (registry, managed preferences, OS defaults), the server (delivered as a support app capability, gated on Enterprise, sanitized and cached), and the user config.
  • A schema declares each key with its default, type and whether it can be enforced.

What can be managed

  • Updates: autoUpdateCheck, skipUpdateCheck
  • External storage prompt: confirmExternalStorage
  • Big folder limits: useNewBigFolderSizeLimit, newBigFolderSizeLimit, notifyExistingFoldersOverLimit, stopSyncingExistingFoldersOverLimit
  • Proxy: proxyType, proxyHost, proxyPort
  • Virtual files: virtualFilesMode

Key rules

  • The server may only default update and proxy keys, never enforce them. Only device policy can enforce those.
  • Server values are sanitized and range checked; invalid values are dropped.
  • Without an Enterprise subscription the server channel is inert; device policy still works.

Tests and docs

test/testmanagedsettings.cpp covers the resolver, sanitize, schema, proxy merge and virtual files. src/libsync/settings/README.md documents the design.

Server side

Delivery depends on the support app change (separate repo and PR) exposing support.desktopClient in the capabilities, gated on an enterprise subscription.

Checklist

AI (if applicable)

@camilasan camilasan added this to the 35.0.0 milestone Aug 12, 2026
@camilasan camilasan changed the title Feature/5497/mdm feature: managed settings. Aug 12, 2026
@camilasan
camilasan force-pushed the feature/5497/mdm branch 8 times, most recently from 9f1a6e7 to ede09eb Compare September 9, 2026 22:43
@camilasan
camilasan marked this pull request as ready for review September 9, 2026 22:44
@camilasan
camilasan force-pushed the feature/5497/mdm branch 2 times, most recently from 7f0f940 to 0e837d9 Compare September 9, 2026 23:14
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Artifact containing the Flatpak bundle: com.nextcloud.desktopclient.nextcloud.flatpak.zip

Digest: sha256:4f914ddd9b6e9074817916bf419b3c11df93beee5363c77fdcac3ab88fb92796

To test this change/fix you can download the above artifact file, unzip it, and install the bundle with:
flatpak install --user com.nextcloud.desktopclient.nextcloud.flatpak

Please make sure to quit your existing Nextcloud app and backup your data.

@github-actions

Copy link
Copy Markdown
Contributor

Artifact containing the AppImage: nextcloud-appimage-pr-10589.zip

Digest: sha256:8b6789b2587675a38dacdf35b7ef4829af6f087c7a63ecd23f847dcf9d0febe9

To test this change/fix you can download the above artifact file, unzip it, and run it.

Please make sure to quit your existing Nextcloud app and backup your data.

@nilsding
nilsding changed the base branch from master to enh/migration September 10, 2026 06:43
@Rello

Rello commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

1. Existing in-place legacy configurations are no longer imported

Priority: P1
Location: src/libsync/settings/migration.cpp:123

Migration::legacyData() initializes legacyData to nullptr. If the already-open themed configuration contains child keys, legacy-location discovery is skipped, but the function still returns the null legacyData.

The previous implementation continued with the already-open QSettings object, so existing in-place legacy account migration can now fail.

Suggested direction: Return the initially opened settings when its legacy group is non-empty, and add a regression test covering that configuration layout.


2. Account restoration erases the offline managed-settings cache

Priority: P1
Location: src/gui/accountmanager.cpp:944

Restored accounts do not hydrate _serverManagedSettings from the persisted aggregate. Nevertheless, addAccountState() immediately calls updateServerManagedSettings() before fresh capabilities arrive.

The merge therefore sees empty per-account maps and persists an empty aggregate over serverManagedSettings. This defeats the intended offline cache on every startup.

Suggested direction: Preserve the cached aggregate until fresh capabilities have successfully arrived, or persist and restore managed settings per account before merging them.


3. One enforced proxy field promotes unrelated defaults above user settings

Priority: P1
Locations:

proxyType, proxyHost, and proxyPort are resolved independently, but ManagedProxySettings::isEnforced becomes true when any field is enforced. Both consumers then replace every field marked as managed.

For example, if device policy enforces only proxyPort while the server provides a default proxyHost, the aggregate isEnforced flag causes the server-default host to overwrite an explicit user host. This contradicts the declared user > server default precedence.

Suggested direction: Preserve source and enforcement metadata per field and decide whether to override each field independently, or resolve the proxy configuration atomically from one source.


4. Failed type conversion is still returned as an enforced value

Priority: P1
Location: src/libsync/settings/managedsettings.cpp:17

The resolver selects the highest-priority source before converting its value to the schema type. It ignores the return value of QVariant::convert() and returns the result as present and potentially enforced even when conversion fails.

Qt leaves a failed conversion as a cleared/null value of the target type. Consumers can consequently observe false, 0, or an empty value while the UI reports that value as enforced. The malformed source also masks valid lower-priority values.

Suggested direction: Convert and validate each candidate before it participates in priority selection. If conversion fails, reject that candidate and continue with the next source.


5. Persisted server settings bypass the allowlist and enforcement policy

Priority: P1
Location: src/libsync/settings/managedconfig.cpp:57

Live capability data is passed through sanitizeServerManagedSettings(), but ManagedConfig::parse() reconstructs cached defaults and enforced maps directly from the configuration file. buildServerSources() subsequently trusts those maps.

A stale, future, or edited cache can therefore restore unknown keys or server-enforced update/proxy values that the live-capability path explicitly rejects.

The cache also records schemaVersion without checking whether the version is supported.

Suggested direction: Apply schema-version validation and sanitizeServerManagedSettings() after reading the cache and before writing it. It should not be possible to construct trusted server sources from unsanitized maps.


6. The update settings UI disables the wrong control

Priority: P2
Location: src/gui/infosettings.cpp:132

When autoUpdateCheck is enforced, the code disables updateButton, which is the manual “Check now” action. It does not disable autoCheckForUpdatesCheckBox.

The automatic-update switch therefore remains visually editable but has no connected handler. Meanwhile, the unrelated manual update action is disabled and can later be re-enabled by updater-state handling.

Suggested direction: Disable the automatic-update switch and its label when autoUpdateCheck is enforced. Keep manual update checking governed by updater availability and state.


7. The folder-limit checkbox checks enforcement for the wrong key

Priority: P2
Location: src/gui/advancedsettings.cpp:344

newFolderLimitCheckBox controls useNewBigFolderSizeLimit, but it is disabled based on enforcement of newBigFolderSizeLimit.

As a result:

  • Enforcing useNewBigFolderSizeLimit leaves its checkbox editable even though the write will be rejected.
  • Enforcing only the numeric threshold unnecessarily disables the independent enable/disable checkbox.

Suggested direction: Bind the checkbox to useNewBigFolderSizeLimit enforcement and the spin box to newBigFolderSizeLimit enforcement. Add tests for independent and combined enforcement.


8. The migration test gives FakeQNAM two independent owners

Priority: P2
Location: test/testmigration.cpp:119

setupStandardConfig() owns FakeQNAM through a local QScopedPointer. It then passes the same raw pointer through FakeCredentials to Account::setCredentials(), which stores it in another owning QSharedPointer.

The local QScopedPointer deletes the manager when the helper returns, leaving the retained account with a dangling pointer and a second deleter.

Suggested direction: Give the fake network manager exactly one owner and ensure its lifetime extends through AccountManager::shutdown().

@nilsding nilsding left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for building a solid foundation to manage settings in a centralised way 🎉

Some smaller comments from my end, other than that we're good to go

Comment thread src/gui/wizard/accountwizardcontroller.cpp
Comment thread src/gui/folderwizard.cpp Outdated
Comment thread src/libsync/settings/settingsources.cpp
Comment thread src/libsync/configfile.cpp
Comment thread src/libsync/settings/README.md Outdated
@camilasan
camilasan force-pushed the enh/migration branch 2 times, most recently from f8ea365 to f1238e3 Compare September 10, 2026 20:13
Base automatically changed from enh/migration to master September 10, 2026 21:12
@camilasan
camilasan force-pushed the feature/5497/mdm branch 2 times, most recently from 5600fde to e4d153f Compare September 10, 2026 22:19

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Clang-Tidy found issue(s) with the introduced code (1/1)

Comment thread test/testmanagedsettings.cpp Outdated
Comment thread test/testmanagedsettings.cpp Outdated
Comment thread test/testmanagedsettings.cpp Outdated
Comment thread test/testmanagedsettings.cpp Outdated
Comment thread test/testmanagedsettings.cpp Outdated
Comment thread test/testmanagedsettings.cpp Outdated
@camilasan
camilasan force-pushed the feature/5497/mdm branch 4 times, most recently from d422fcf to e7b39b0 Compare September 11, 2026 16:48

@claucambra claucambra left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial comments

Comment thread src/gui/settingspanelstyle.cpp
Comment thread src/libsync/settings/README.md Outdated
Comment thread src/libsync/settings/README.md Outdated
Comment thread src/libsync/settings/README.md Outdated
Comment thread src/libsync/settings/README.md Outdated
Comment thread src/libsync/settings/README.md Outdated
Comment thread src/libsync/settings/README.md Outdated
Comment thread src/libsync/settings/README.md Outdated
Comment thread src/libsync/settings/README.md Outdated
Comment on lines +139 to +206
## Sources and the resolver

A source is a small adapter over one place a value can come from. It holds no
values; it reads them live and tags them with a provenance, an enforcement state and
a priority:

```cpp
class SettingSource {
public:
virtual std::optional<QVariant> read(const QString &key, const QString &group) const = 0;
virtual SettingSourceType type() const = 0; // PlatformPolicy, ServerDefault, ...
virtual EnforcementState enforcement() const = 0; // Enforced or NotEnforced
virtual int priority() const = 0; // who wins on a conflict
};
```

buildDeviceSources and buildServerSources are factory functions that construct these
adapters for the current environment. The device set depends on the platform and
carries the priorities:

```cpp
// buildDeviceSources, Windows
HKCU\Software\Policies\<vendor>\<app> PlatformPolicy, Enforced, 210
HKLM\Software\Policies\<vendor>\<app> PlatformPolicy, Enforced, 200
HKLM\Software\<vendor>\<app> PlatformDefault, NotEnforced, 20
// macOS: MacForcedPreferenceSource(200) + /Library/Preferences/<domain>.plist (20)
// Linux: <sysconfdir>/<app>/policies.conf (enforced 200), <sysconfdir>/<app>/<app>.conf (20)
```

```cpp
// buildServerSources, from the cached server settings
if (!sanitized.enforced.isEmpty()) {
sources.push_back(std::make_unique<ServerSettingsSource>(
sanitized.enforced, SettingSourceType::ServerEnforced, EnforcementState::Enforced, 100));
}
if (!sanitized.defaults.isEmpty()) {
sources.push_back(std::make_unique<ServerSettingsSource>(
sanitized.defaults, SettingSourceType::ServerDefault, EnforcementState::NotEnforced, 30));
}
```

getConfig builds the whole stack fresh on each call, so resolution reflects the
current registry, plist and cfg (which is why a device policy change is seen after
the dialog reopens):

```cpp
ManagedSettings resolver;
for (auto &deviceSource : buildDeviceSources()) {
resolver.addSource(std::move(deviceSource));
}
resolver.addSource(std::make_unique<UserConfigSource>(configFile(), groupName)); // 50
resolver.addSource(std::make_unique<UserConfigSource>(configFile(), QString(), 49)); // legacy top level
for (auto &serverSource : buildServerSources(serverManagedSettings())) {
resolver.addSource(std::move(serverSource));
}
return resolver.resolve(definition);
```

resolve then asks every source and keeps the highest priority one that has a value,
subject to the enforceable gate:

```cpp
if (source->priority() > settingPriority) {
settingSource = source.get();
settingValue = *value;
settingPriority = source->priority();
}
```

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this is delving too deep into implementation details. I think these comments should be in the code and not in a broader README; it seems to me a README should instead more broadly describe the bird's eye view of what is happening

Comment thread src/libsync/settings/README.md Outdated
Add the managed settings model, source hierarchy, schema, server cache,
and configuration gateway. Cover precedence, enforcement metadata, platform
sources, and resolver behavior with tests.

Assisted-by: GitHub Copilot:2026-09
Signed-off-by: Camila Ayres <hello@camilasan.com>
Keep update and proxy settings defaultable by the server while reserving
enforcement for device policy. Route update access through the managed resolver
and preserve the security boundary.

Assisted-by: GitHub Copilot:2026-09
Signed-off-by: Camila Ayres <hello@camilasan.com>
Add folder limit definitions, server value validation, managed resolution,
and enforcement aware controls for advanced settings.

Assisted-by: GitHub Copilot:2026-09
Signed-off-by: Camila Ayres <hello@camilasan.com>
Validate virtual files modes and apply managed defaults and enforcement when
creating folders through the setup and folder wizards.

Assisted-by: GitHub Copilot:2026-09
Signed-off-by: Camila Ayres <hello@camilasan.com>
…ing more clear.

Clarify managed settings names and present enforcement state in update,
proxy, folder limit, and virtual files controls.

Assisted-by: GitHub Copilot:2026-09
Signed-off-by: Camila Ayres <hello@camilasan.com>
Assisted-by: GitHub Copilot:2026-09
Assisted-by: Claude Code:claude-opus-4-8
Signed-off-by: Camila Ayres <hello@camilasan.com>
Move it to docs/settings folder.
Renamed it to managed-settings.md.

Signed-off-by: Camila Ayres <hello@camilasan.com>
Signed-off-by: Camila Ayres <hello@camilasan.com>
A policy cannot enforce a setting the client did not declare.
Update the documentation with the new default.

Assisted-by: Claude Code:claude-opus-4-8
Signed-off-by: Camila Ayres <hello@camilasan.com>
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
38 New Code Smells (required ≤ 0)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants