Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0c4cc55
fix(macOS): Enforce valid app group identifier.
i2h3 Oct 29, 2025
78d9055
chore(macOS): Updated Xcode build settings.
i2h3 Oct 29, 2025
072aa4e
fix(macOS): Updated signing settings for all targets.
i2h3 Oct 29, 2025
577ff56
feat(macOS): Enabled app sandbox and network client entitlements for …
i2h3 Nov 4, 2025
25c66b3
fix(macOS): Relocated socket files into sandboxed containers.
i2h3 Oct 29, 2025
b889b24
fix(macOS): Added container migration manifest.
i2h3 Nov 5, 2025
b5e986d
chore(macOS): Removed obsolete values from Info.plist
i2h3 Nov 5, 2025
032dd67
chore(macOS): Outsourced some build settings from Xcode project into …
i2h3 Nov 5, 2025
402b429
chore(macOS): Updated bundle identifier in Xcode project to Nextcloud.
i2h3 Nov 5, 2025
e1344e0
chore(macOS): Creating build settings file on demand from template.
i2h3 Nov 5, 2025
b898958
chore(macOS): Added default values to build settings file.
i2h3 Nov 5, 2025
06ad59f
fix(macOS): NextcloudDesktopClientSocketKit signing.
i2h3 Nov 7, 2025
3b3c31d
fix(macOS): Added missing user selected read-write entitlement to mai…
i2h3 Nov 7, 2025
19c9d1c
fix(macOS): Writable check on debug archive destination directory.
i2h3 Nov 7, 2025
2c986fe
fix(macOS): Accessing target of debug archive as a security scoped URL.
i2h3 Nov 10, 2025
856454c
fix(file-provider): App group container lookup API change in Nextclou…
i2h3 Nov 11, 2025
012a9bc
fix(macOS): File provider path assembly for debug archive.
i2h3 Nov 11, 2025
376a40c
fix(macOS): Debug archive creation in a sandbox.
i2h3 Nov 11, 2025
b3b71ea
fix(macOS): Development team group identifier prefix.
i2h3 Nov 11, 2025
c37b386
Work in progress: Direct and local NextcloudFileProviderKit reference.
i2h3 Nov 11, 2025
b5ba15a
fix(macOS): Defined "FileProviderUIExt" as target dependency to "desk…
i2h3 Nov 11, 2025
83af300
fix: Define default DEVELOPMENT_TEAM for main app.
i2h3 Nov 11, 2025
fb21dc8
fix: Restored NCFPKAppGroupIdentifier in main app Info.plist
i2h3 Nov 11, 2025
c8906c6
fix: Fixed too long socket paths (max 104 characters).
i2h3 Nov 11, 2025
d72db42
chore(macOS): Made whole "src" instead of just "gui" folder accessibl…
i2h3 Nov 12, 2025
b2ef402
fix(macOS): Synchronization folder selection.
i2h3 Nov 12, 2025
1534ff2
fix(file-provider): Consolidated support and log directories.
i2h3 Nov 12, 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
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" )
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,21 @@ import OSLog
///
let ncKit: NextcloudKit

let appGroupIdentifier = Bundle.main.object(forInfoDictionaryKey: "SocketApiPrefix") as? String
var ncAccount: Account?
var dbManager: FilesDatabaseManager?
var changeObserver: RemoteChangeObserver?
var ignoredFiles: IgnoredFilesMatcher?
lazy var ncKitBackground = NKBackground(nkCommonInstance: ncKit.nkCommonInstance)

lazy var socketClient: LocalSocketClient? = {
guard let containerUrl = pathForAppGroupContainer() else {
guard let containerUrl = FileManager.default.applicationGroupContainer() else {
logger.fault("Won't start socket client, no container URL available!")
return nil;
}

let socketPath = containerUrl.appendingPathComponent(
".fileprovidersocket", conformingTo: .archive)
let socketPath = containerUrl.appendingPathComponent("fps", conformingTo: .archive)
let lineProcessor = FileProviderSocketLineProcessor(delegate: self, log: log)

return LocalSocketClient(socketPath: socketPath.path, lineProcessor: lineProcessor)
}()

Expand Down
Loading
Loading