File::hasWriteAccess error with Sandboxed app on macOS

In a standalone app, the following code reports false for desktop.hasWriteAccess() when the Projucer setting for Use App Sandbox is enabled:

    auto desktop = File::getSpecialLocation (File::userDesktopDirectory);
    DBG ("Desktop [" + desktop.getFullPathName()  +  "] hasWriteAccess() ? " + String (desktop.hasWriteAccess() ? "true" : "false"));

(It reports true if Use App Sandbox is disabled.)

I understand that getting a Sandboxed app’s entitlements working correctly also requires the app to be codesigned. So, after building the app in Xcode, I’m running codesign from the Terminal to sign it, and verifying the results using both the Taccy utility app and command line tools.

As far as I can tell, the app is codesigned and has the necessary entitlements at that point, but yet it is still not granted access to the desktop (or anywhere else for that matter).

(And to be clear, this isn’t just a case where File.hasWriteAccess is returning an incorrect result, but also that attempts to write to a FileOutputStream for a file in that location also fails.)

This is on macOS Monterey, 12.6.2, with JUCE 7.0.5.

Which sandbox entitlements are you requesting? I don’t see any entitlement (other than the deprecated com.apple.security.files.all) that would automatically allow access to the Desktop directory. You might be able to do it using the entitlement for user-selected files, but I think you’d need to get the user to pick the Desktop from a file chooser, and then potentially bookmark the resulting URL so that it can be accessed across application restarts without prompting each time.

Right, that’s a good point to clarify. My example was a simplified one – in the actual code in the app, I’m using a FileChooser dialog. The Desktop location is just what I use for the FileChooser’s initialFileOrDirectory parameter.

And in the entitlements, I am including com.apple.security.files.user-selected.read-write

And File::hasWriteAccess returns false for any location I pick with the FileChooser, e.g. the home ~/user folder.

The actual code is more like this:

FileChooser fileChooser ("Please select where to save the zip file...",
                     File::getSpecialLocation (File::userDesktopDirectory).getChildFile ("Userdata.zip"),
                     "*.zip",
                     true /* useOSNativeDialogBox */);

if (fileChooser.browseForFileToSave (true))
{
    File chosenFile = fileChooser.getResult();

    if (! chosenFile.hasWriteAccess())
        DBG ("Sorry, you do not have write access to: " + chosenFile.getFullPathName());

    // ...else write file to disk using FileOutputStream
}

For files that don’t exist, hasWriteAccess() will check whether the parent directory is writable. However, the sandboxed app is only allowed to write to the selected file, it’s not allowed to write arbitrarily to ~/Desktop. Without the hasWriteAccess check I’m able to write to user-selected files from a sandboxed app, so I suspect that the problem you’re seeing stems from this check.

This might be a case where it’s better to ask forgiveness than permission: try to open the file for writing, and display an error at that point if the file couldn’t be opened.

Thanks for explaining that @reuk. I guess I hadn’t quite parsed it that finely, but when you break it down like that it makes sense why the hasWriteAccess call would return false in a sandboxed app.

Indeed, if I remove the hasWriteAccess check, and instead check if FileOutputStream::failedToOpen(), then that seems to work for the error checking.

I also tried an alternate implementation using the TemporaryFile class, but that didn’t work. The FileOutputStream failed to open for that. That makes sense, since the user-selected file in that case was named “Userdata.zip”, and TemporaryFile wants to write to a file named something different (e.g. “Userdata_tempf11112e1.zip”).

Or another way to solve it in a sandboxed app would be to first call chosenFile.create(), then do the hasWriteAccess check, although presumably that check would be redundant at that point.

And I suppose it’s possible that somehow you could be able to successfully use File::create, but that the subsequent check if FileOutputStream::failedToOpen() would return true (although I can’t think of what circumstance that would happen in), and then you’d be on the hook for deleting the blank file you just created with File::create. So that’s probably not the best approach.