Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
1de9cef
fix(macOS): Enforce valid app group identifier.
i2h3 Oct 29, 2025
5d882e2
chore(macOS): Updated Xcode build settings.
i2h3 Oct 29, 2025
5f2dd67
fix(macOS): Updated signing settings for all targets.
i2h3 Oct 29, 2025
22eec82
feat(macOS): Enabled app sandbox and network client entitlements for …
i2h3 Nov 4, 2025
1a5082a
fix(macOS): Relocated socket files into sandboxed containers.
i2h3 Oct 29, 2025
202cb4e
fix(macOS): Added container migration manifest.
i2h3 Nov 5, 2025
d7a6d7c
chore(macOS): Removed obsolete values from Info.plist
i2h3 Nov 5, 2025
645b628
chore(macOS): Outsourced some build settings from Xcode project into …
i2h3 Nov 5, 2025
6afb39a
chore(macOS): Updated bundle identifier in Xcode project to Nextcloud.
i2h3 Nov 5, 2025
c947db9
chore(macOS): Creating build settings file on demand from template.
i2h3 Nov 5, 2025
7bd2315
chore(macOS): Added default values to build settings file.
i2h3 Nov 5, 2025
9298122
fix(macOS): NextcloudDesktopClientSocketKit signing.
i2h3 Nov 7, 2025
c2e01d4
fix(macOS): Added missing user selected read-write entitlement to mai…
i2h3 Nov 7, 2025
b6cfd6d
fix(macOS): Writable check on debug archive destination directory.
i2h3 Nov 7, 2025
11be220
fix(macOS): Accessing target of debug archive as a security scoped URL.
i2h3 Nov 10, 2025
09005a0
fix(file-provider): App group container lookup API change in Nextclou…
i2h3 Nov 11, 2025
52212ab
fix(macOS): File provider path assembly for debug archive.
i2h3 Nov 11, 2025
6ebdab4
fix(macOS): Debug archive creation in a sandbox.
i2h3 Nov 11, 2025
291156d
fix(macOS): Development team group identifier prefix.
i2h3 Nov 11, 2025
eed5950
fix(macOS): Defined "FileProviderUIExt" as target dependency to "desk…
i2h3 Nov 11, 2025
c4846ce
fix: Define default DEVELOPMENT_TEAM for main app.
i2h3 Nov 11, 2025
1679b82
fix: Restored NCFPKAppGroupIdentifier in main app Info.plist
i2h3 Nov 11, 2025
28421ec
fix: Fixed too long socket paths (max 104 characters).
i2h3 Nov 11, 2025
f499658
chore(macOS): Made whole "src" instead of just "gui" folder accessibl…
i2h3 Nov 12, 2025
ff45457
fix(macOS): Synchronization folder selection.
i2h3 Nov 12, 2025
af2e689
fix(file-provider): Consolidated support and log directories.
i2h3 Nov 12, 2025
5aca5a6
chore(file-provider): Updated to NextcloudFileProviderKit 4.0.0
i2h3 Dec 4, 2025
79aa712
fix(macOS): Signing identity.
i2h3 Dec 4, 2025
aeee811
fix(file-provider): File provider domains reset on app sandbox migrat…
i2h3 Dec 5, 2025
d5b5a85
chore(macOS): Xcode removed an orphaned reference from the project file.
i2h3 Dec 5, 2025
7e68148
fix(file-provider): Synchronous file provider domain removals.
i2h3 Dec 5, 2025
9c1e865
chore(macOS): Removed obsolete compatibility switches for no longer s…
i2h3 Dec 8, 2025
7897d15
chore(file-provider): Logging improvements.
i2h3 Dec 8, 2025
f2ba1b0
chore(file-provider): Renamed fileproviderdomainmanager_mac.mm to fil…
i2h3 Dec 8, 2025
87d7694
chore(file-provider): Simplified FileProviderDomainManager.
i2h3 Dec 8, 2025
a1462d7
fix(file-provider): Made addFileProviderDomain and removeFileProvider…
i2h3 Dec 8, 2025
ed7d303
fix(file-provider): Made findExistingFileProviderDomains synchronous.
i2h3 Dec 8, 2025
e5b4d37
chore(file-provider): Consolidated account to domain mapping.
i2h3 Dec 9, 2025
52e5d4a
FIXUP: Improved logging.
i2h3 Dec 10, 2025
866d709
FIXUP: Removing domain completely.
i2h3 Dec 10, 2025
41f37a2
FIXUP: Crash due to dangling pointer.
i2h3 Dec 10, 2025
4108791
chore(file-provider): Improved logging.
i2h3 Dec 10, 2025
525cb9c
fix(file-provider): Authentication calls via XPC by file provider dom…
i2h3 Dec 10, 2025
296567b
fix(file-provider): Actually enable or disable file provider domain i…
i2h3 Dec 10, 2025
7751571
chore(ci): macOS build and test action now uses latest macOS and late…
i2h3 Dec 11, 2025
1255a0f
fix(file-provider): Resolved circular object initializer dependency.
i2h3 Dec 11, 2025
9ae167a
fix(CI): Disabling code signing for macOS by default.
i2h3 Dec 12, 2025
6dcc1fd
chore(CI): Removed obsolete environment variables from CI.
i2h3 Dec 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions .github/workflows/macos-build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,20 @@ jobs:
build:
name: Build nextcloud-client
timeout-minutes: 60
runs-on: macos-15
runs-on: macos-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 1

- name: List Xcode installations
run: sudo ls -1 /Applications | grep "Xcode"

- name: Select Xcode 16.3
run: sudo xcode-select -s /Applications/Xcode_16.3.app/Contents/Developer

- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable

- name: Show Swift version
run: swift --version

- name: Set up Python ${{ inputs.PYTHON_VERSION }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
Expand Down Expand Up @@ -74,7 +76,7 @@ jobs:
name: Run tests
needs: build
timeout-minutes: 60
runs-on: macos-15
runs-on: macos-latest
steps:
- name: Restore cached Craft directories containing the built client
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
Expand Down
4 changes: 0 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,6 @@ if(OWNCLOUD_5XX_NO_BLACKLIST)
add_definitions(-DOWNCLOUD_5XX_NO_BLACKLIST=1)
endif()

if(APPLE)
set( SOCKETAPI_TEAM_IDENTIFIER_PREFIX "" CACHE STRING "SocketApi prefix (including a following dot) that must match the codesign key's TeamIdentifier/Organizational Unit" )
endif()

if(BUILD_CLIENT)
OPTION(GUI_TESTING "Build with gui introspection features of socket api" OFF)

Expand Down
1 change: 1 addition & 0 deletions NEXTCLOUD.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ set( APPLICATION_ICON_SET "SVG" )
set( APPLICATION_SERVER_URL "" CACHE STRING "URL for the server to use. If entered, the UI field will be pre-filled with it" )
set( APPLICATION_SERVER_URL_ENFORCE ON ) # If set and APPLICATION_SERVER_URL is defined, the server can only connect to the pre-defined URL
set( APPLICATION_REV_DOMAIN "com.nextcloud.desktopclient" )
set( DEVELOPMENT_TEAM "NKUJUXUJ3B" CACHE STRING "Apple Development Team ID for code signing" )
Copy link
Collaborator

Choose a reason for hiding this comment

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

is it just a random string or something specific ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It is the unique and stable identifier of the development team registered with Apple which is expected to sign the build later on. Like apps have bundle identifiers (com.nextcloud.desktopclient), their development teams have it, too (NKUJUXUJ3B for Nextcloud GmbH). This is relevant for branded builds in particular.

set( APPLICATION_VIRTUALFILE_SUFFIX "nextcloud" CACHE STRING "Virtual file suffix (not including the .)")
set( APPLICATION_OCSP_STAPLING_ENABLED OFF )
set( APPLICATION_FORBID_BAD_SSL OFF )
Expand Down
1 change: 1 addition & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ SPDX-License-Identifier = "GPL-2.0-or-later"

[[annotations]]
path = [
"admin/osx/container-migration.plist.cmake",
"admin/osx/TransifexStringCatalogSanitizer/Package.swift",
"admin/osx/TransifexStringCatalogSanitizer/Package.resolved",
"admin/osx/TransifexStringCatalogSanitizer/README.md",
Expand Down
5 changes: 5 additions & 0 deletions admin/osx/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ else()
set(DEBUG_ENTITLEMENTS "")
endif()

# Sandbox migration description
configure_file(container-migration.plist.cmake ${CMAKE_CURRENT_BINARY_DIR}/container-migration.plist @ONLY)
install(FILES ${CMAKE_BINARY_DIR}/admin/osx/container-migration.plist
DESTINATION ${OWNCLOUD_OSX_BUNDLE}/Contents/Resources)

configure_file(create_mac.sh.cmake ${CMAKE_CURRENT_BINARY_DIR}/create_mac.sh)
configure_file(macosx.entitlements.cmake ${CMAKE_CURRENT_BINARY_DIR}/macosx.entitlements)
configure_file(macosx.pkgproj.cmake ${CMAKE_CURRENT_BINARY_DIR}/macosx.pkgproj)
Expand Down
10 changes: 10 additions & 0 deletions admin/osx/container-migration.plist.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Move</key>
<array>
<string>${Library}/Preferences/@APPLICATION_NAME@</string>
</array>
</dict>
</plist>
8 changes: 7 additions & 1 deletion admin/osx/macosx.entitlements.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>@SOCKETAPI_TEAM_IDENTIFIER_PREFIX@@APPLICATION_REV_DOMAIN@</string>
<string>@DEVELOPMENT_TEAM@.@APPLICATION_REV_DOMAIN@</string>
</array>
@DEBUG_ENTITLEMENTS@
</dict>
Expand Down
2 changes: 1 addition & 1 deletion cmake/modules/MacOSXBundleInfo.plist.in
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,6 @@
</dict>
</array>
<key>NCFPKAppGroupIdentifier</key>
<string>@SOCKETAPI_TEAM_IDENTIFIER_PREFIX@@APPLICATION_REV_DOMAIN@</string>
<string>@DEVELOPMENT_TEAM@.@APPLICATION_REV_DOMAIN@</string>
</dict>
</plist>
2 changes: 1 addition & 1 deletion config.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#cmakedefine WITH_QTKEYCHAIN 1
#cmakedefine BUILD_FILE_PROVIDER_MODULE "@BUILD_FILE_PROVIDER_MODULE@"
#cmakedefine WITH_PROVIDERS "@WITH_PROVIDERS@"
#define SOCKETAPI_TEAM_IDENTIFIER_PREFIX "@SOCKETAPI_TEAM_IDENTIFIER_PREFIX@"
#cmakedefine DEVELOPMENT_TEAM "@DEVELOPMENT_TEAM@"

#cmakedefine THEME_CLASS @THEME_CLASS@
#cmakedefine THEME_INCLUDE @THEME_INCLUDE@
Expand Down
231 changes: 231 additions & 0 deletions doc/macOS-Sandbox-Qt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: GPL-2.0-or-later
-->

# macOS App Sandbox Support for Qt Applications

## Overview

This document explains how to make the Nextcloud Desktop Client work properly with macOS App Sandbox when using Qt. The key issue is that Qt's `QFileDialog` returns security-scoped URLs that require explicit access management in sandboxed applications.

## The Problem

When running a sandboxed macOS application with the `com.apple.security.files.user-selected.read-write` entitlement, file operations on user-selected files (via `QFileDialog`) will fail unless you explicitly:

1. Call `startAccessingSecurityScopedResource()` on the URL before accessing the file
2. Call `stopAccessingSecurityScopedResource()` when done

This is **required by macOS sandbox security**, but Qt does not handle this automatically. The underlying issue is that:

- `QFileDialog::getSaveFileUrl()` returns a `QUrl` that represents a security-scoped bookmark
- Without calling `startAccessingSecurityScopedResource()`, the sandboxed app has no permission to access the file
- Even though you have the entitlement, you must explicitly claim access for each user-selected file

## The Solution

### 1. Security-Scoped Access Wrapper

We created a RAII wrapper class `MacSandboxSecurityScopedAccess` (in `utility_mac_sandbox.h/mm`) that:

- Automatically calls `startAccessingSecurityScopedResource()` in the constructor
- Automatically calls `stopAccessingSecurityScopedResource()` in the destructor
- Uses unique_ptr for exception safety
- Provides `isValid()` to check if access was successfully obtained

### 2. Usage Pattern

```cpp
#ifdef Q_OS_MACOS
#include "utility_mac_sandbox.h"
#endif

void MyClass::saveFile()
{
const auto fileUrl = QFileDialog::getSaveFileUrl(
this,
tr("Save File"),
QUrl::fromLocalFile(QDir::homePath()),
tr("Text Files (*.txt)")
);

if (fileUrl.isEmpty()) {
return;
}

#ifdef Q_OS_MACOS
// Acquire security-scoped access for the user-selected file
auto scopedAccess = Utility::MacSandboxSecurityScopedAccess::create(fileUrl);

if (!scopedAccess->isValid()) {
// Handle error - access could not be obtained
QMessageBox::critical(this, tr("Error"), tr("Could not access file"));
return;
}
// scopedAccess will automatically release when it goes out of scope
#endif

// Now you can safely access the file
QFile file(fileUrl.toLocalFile());
if (file.open(QIODevice::WriteOnly)) {
// Write to file...
}
}
```

### 3. Required Entitlements

In `admin/osx/macosx.entitlements.cmake`, ensure you have:

```xml
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
```

## Key Requirements for Qt + macOS Sandbox

### 1. Use QFileDialog URL-based Methods

Always use the URL-based variants of QFileDialog methods:
- ✅ `QFileDialog::getSaveFileUrl()`
- ✅ `QFileDialog::getOpenFileUrl()`
- ✅ `QFileDialog::getOpenFileUrls()`
- ❌ `QFileDialog::getSaveFileName()` - returns QString, not security-scoped
- ❌ `QFileDialog::getOpenFileName()` - returns QString, not security-scoped

### 2. Wrap File Access with Security Scoping

```cpp
#ifdef Q_OS_MACOS
auto scopedAccess = Utility::MacSandboxSecurityScopedAccess::create(fileUrl);
if (!scopedAccess->isValid()) {
// Handle error
return;
}
#endif
// Access file here
// scopedAccess releases automatically when going out of scope
```

### 3. Handle Scope Lifetime Correctly

The security-scoped access must remain valid for the entire duration of file access:

```cpp
// ✅ CORRECT - scopedAccess lives until after file operations
#ifdef Q_OS_MACOS
auto scopedAccess = Utility::MacSandboxSecurityScopedAccess::create(fileUrl);
if (!scopedAccess->isValid()) {
return;
}
#endif

QFile file(fileUrl.toLocalFile());
file.open(QIODevice::WriteOnly);
file.write(data);
file.close();
// scopedAccess destructor called here

// ❌ WRONG - scopedAccess destroyed before file operations
#ifdef Q_OS_MACOS
{
auto scopedAccess = Utility::MacSandboxSecurityScopedAccess::create(fileUrl);
if (!scopedAccess->isValid()) {
return;
}
} // scopedAccess destroyed here!
#endif

QFile file(fileUrl.toLocalFile()); // This will fail!
file.open(QIODevice::WriteOnly); // No longer have access
```

### 4. Consider All File Operations

This applies to ANY file operation on user-selected files:
- Reading files
- Writing files
- Creating archives/zip files
- Copying files
- Moving files
- Checking file existence/permissions

## Common Pitfalls

### 1. Using QString-based paths instead of QUrl

```cpp
// ❌ WRONG - loses security-scoped bookmark
QString path = QFileDialog::getSaveFileName(...);

// ✅ CORRECT - preserves security-scoped bookmark
QUrl url = QFileDialog::getSaveFileUrl(...);
```

### 2. Converting QUrl too early

```cpp
// ❌ WRONG - converts to string before starting access
QUrl url = QFileDialog::getSaveFileUrl(...);
QString path = url.toLocalFile(); // Loses security scope!
#ifdef Q_OS_MACOS
auto access = Utility::MacSandboxSecurityScopedAccess::create(QUrl::fromLocalFile(path)); // Won't work
#endif

// ✅ CORRECT - start access before conversion
QUrl url = QFileDialog::getSaveFileUrl(...);
#ifdef Q_OS_MACOS
auto access = Utility::MacSandboxSecurityScopedAccess::create(url); // Works!
#endif
QString path = url.toLocalFile();
```

### 3. Forgetting to check isValid()

```cpp
// ❌ RISKY - doesn't check if access was obtained
auto scopedAccess = Utility::MacSandboxSecurityScopedAccess::create(fileUrl);
QFile file(fileUrl.toLocalFile()); // Might fail silently

// ✅ CORRECT - always check validity
auto scopedAccess = Utility::MacSandboxSecurityScopedAccess::create(fileUrl);
if (!scopedAccess->isValid()) {
// Show error to user
return;
}
QFile file(fileUrl.toLocalFile()); // Now safe to use
```

## Testing Sandbox Behavior

To test if your app properly handles sandbox restrictions:

1. **Build with proper entitlements**: Ensure the app is codesigned with the entitlements file
2. **Test file operations**: Try to save/open files in various locations
3. **Check Console.app**: Look for sandbox violation messages like:
```
Sandbox: MyApp(12345) deny(1) file-write-create /Users/...
```
4. **Test without access calls**: Temporarily remove the security-scoped access calls to verify they're needed

## References

- [Apple Documentation: App Sandbox](https://developer.apple.com/documentation/security/app_sandbox)
- [Apple Documentation: Security-Scoped Bookmarks](https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso)
- [Qt Documentation: QFileDialog](https://doc.qt.io/qt-6/qfiledialog.html)

## Files Modified

- `src/common/utility_mac_sandbox.h` - Header for security-scoped access wrapper
- `src/common/utility_mac_sandbox.mm` - Implementation using Objective-C++
- `src/common/common.cmake` - Added new files to build system
- `src/gui/generalsettings.cpp` - Fixed debug archive creation to use security-scoped access

## Future Work

Consider auditing all uses of `QFileDialog` in the codebase to ensure they:
1. Use URL-based methods (`getSaveFileUrl`, `getOpenFileUrl`, etc.)
2. Properly acquire security-scoped access on macOS
3. Handle access errors gracefully
3 changes: 0 additions & 3 deletions shell_integration/MacOSX/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ if(APPLE)
"OC_OEM_SHARE_ICNS=${OC_OEM_SHARE_ICNS}"
"OC_APPLICATION_NAME=${APPLICATION_NAME}"
"OC_APPLICATION_REV_DOMAIN=${APPLICATION_REV_DOMAIN}"
"OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX=${SOCKETAPI_TEAM_IDENTIFIER_PREFIX}"
COMMENT building Mac Overlay icons
VERBATIM)

Expand All @@ -34,7 +33,6 @@ if(APPLE)
"OC_APPLICATION_VENDOR=${APPLICATION_VENDOR}"
"OC_APPLICATION_NAME=${APPLICATION_NAME}"
"OC_APPLICATION_REV_DOMAIN=${APPLICATION_REV_DOMAIN}"
"OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX=${SOCKETAPI_TEAM_IDENTIFIER_PREFIX}"
COMMENT building macOS File Provider extension
VERBATIM)

Expand All @@ -46,7 +44,6 @@ if(APPLE)
"OC_APPLICATION_VENDOR=${APPLICATION_VENDOR}"
"OC_APPLICATION_NAME=${APPLICATION_NAME}"
"OC_APPLICATION_REV_DOMAIN=${APPLICATION_REV_DOMAIN}"
"OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX=${SOCKETAPI_TEAM_IDENTIFIER_PREFIX}"
DEPENDS mac_fileproviderplugin
COMMENT building macOS File Provider UI extension
VERBATIM)
Expand Down
3 changes: 0 additions & 3 deletions shell_integration/MacOSX/NextcloudIntegration/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,3 @@
# SPDX-License-Identifier: GPL-2.0-or-later

DerivedData

# exception
!NextcloudDev/Build.xcconfig.template
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>$(OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX)$(OC_APPLICATION_REV_DOMAIN)</string>
<string>$(DEVELOPMENT_TEAM).$(OC_APPLICATION_REV_DOMAIN)</string>
</array>
<key>com.apple.security.network.client</key>
<true/>
Expand Down
Loading
Loading