I'm developing a sandboxed app for Mac OS X 10.7 and I'm trying to implement file saving in a way similar to NSDocument:
Rewrite the file's new contents to a temporary file
Overwrite the original file with the temporary file
The issue I'm having is that the sandbox is denying step 2. I see the following line in Console:
sandboxd: XXXX deny file-write-create /Volumes/Home/sbooth/Test Files/Test
I already have this file open for reading and writing, and I have the File System Read/Write Access entitlement enabled. I know NSDocument does this with no special entitlements so I'm trying to figure out what I've missed.
Here is how I'm doing things now (this portion of the app is in C++, not Objective-C/C++):
FSRef tempFileFSRef;
if(noErr != FSPathMakeRef((const UInt8 *)tempFileName, &tempFileFSRef, NULL))
; // Handle it
CFURLRef destinationDirURL = CFURLCreateCopyDeletingLastPathComponent(kCFAllocatorDefault, mURL);
FSRef destinationDirFSRef;
if(!CFURLGetFSRef(destinationDirURL, &destinationDirFSRef))
; // Handle it
CFRelease(destinationDirURL), destinationDirURL = NULL;
CFStringRef destinationName = CFURLCopyLastPathComponent(mURL);
FSRef target;
OSStatus result = FSCopyObjectSync(&tempFileFSRef, &destinationDirFSRef, destinationName, &target, kFSFileOperationOverwrite | kFSFileOperationSkipSourcePermissionErrors);
if(noErr != result)
; // Handle it
The code works correctly if I disable sandboxing.
Edit: Additional information was requested by Femi. I open the file using C stdio:
FILE *f = fopen(reinterpret_cast<const char *>(buf), "r");
and it closed using fclose before the temp file is created.
My entitlements are:
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.assets.music.read-write</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
It's also worth noting that Apple says in their App Sandbox Design Guide that:
If you are managing documents using any technology other than the
NSDocument class, you must convert to using this class. The NSDocument
class automatically works with Powerbox. NSDocument also provides
support for keeping documents within your sandbox if the user moves
them using Finder.
The sandbox only allows you to read and write from your application's container. If you have the read/write entitlement enabled, then you only have access to the files that are opened via the OpenSavePanel (or some method like dragging an icon to the dock where the user has specifically opened the file). This will severely limit how the application works with files on the file system.
So, as long as your application has permission to write over the document in question, you can write your temporary file to your applications container and then retrieve the temp file contents to then save to the original location. However, this assumes that you app maintains permissions to the original file throughout the process.
So, my suggestion is to make sure any writing you do is done in the application container and then verify that you app still has permission to write to the real file location before calling the save function.
I recently wrote a similar question where my app lost access to files that were open because another (non-sandboxed) app saved to the same file. The sandbox then gave that app access to my doc and removed permissions from the app.
Related
I am developing a Mac app that makes use of App Scripting. The is Sandboxed and thus needs the proper entitlements in order to get permissions to send AppleScript events to other apps. I have gotten this working properly for apps (like Mail and spotify) which specify access-group identifiers like this:
<access-group identifier="com.apple.mail.compose" access="rw"/>
<access-group identifier="com.spotify.playback"/>
However, a few other of the Apple made apps (like Xcode) specify their identifiers like this:
<access-group identifier="*"/>
I have tried to configure my entitlement file like this:
<key>com.apple.security.scripting-targets</key>
<dict>
<key>com.apple.dt.Xcode</key>
<array>
<string>*</string>
</array>
</dict>
, but when doing this it does not work and I get this error message in the console:
AppleEvents/sandbox: Returning errAEPrivilegeError/-10004 and denying dispatch of event xcod/buld from process '-------'/0x0-0x1a05a04, pid=82514, because it is not entitled to send an AppleEvent to this process.
Does anyone know how to properly configure this?
Actually I found the answer myself. In the sdef Man pages it states:
An identifier of "*" means the element is usable by any application,
without an explicit entitlement.
This means that you do not have to add the identifiers in your entitlements file and it should work regardless. The reason why I got the error was because I accidentally used commands that were not included in those particular access-groups.
After installation of our software in /Applications, I wanted our end users to be able to see the serial number they entered in our about box. Since the software is activated for ALL USERS, I figured I need to write that information to a common preference file location. However Apple's docs say that CFPreferencesAppSynchronize can't be used for kCFPreferencesAnyUser:
"Note that you can only save preferences for “Any User” if you have
root privileges"
So of course the code below doesn't work (although CFPreferencesAppSynchronize returns true):
void WritePrefTest(void)
{
// Write it out
CFStringRef textKey = CFSTR("myTextKey");
CFStringRef applicationID = CFSTR("com.foo.bar");
CFStringRef textValue = CFSTR("text that should be written");
CFPreferencesSetValue(textKey, textValue, applicationID, kCFPreferencesAnyUser, kCFPreferencesCurrentHost);
Boolean wasSuccessful = CFPreferencesAppSynchronize(kCFPreferencesAnyUser);
assert(wasSuccessful);
// read it back in
CFStringRef prefText = (CFStringRef)CFPreferencesCopyAppValue(textKey, applicationID);
NSLog(#"The text from com.foo.bar == %#", (__bridge NSString *)prefText);
}
Since activation and entry of the serial number takes place OUTSIDE the installer, should I 1) request elevating privileges (never done that before), 2) create a plist in some other folder (/Library/Application Support) -- assuming I'm allowed to write there -- or 3) write it into some other macos-accepted folder?
Ah hah!
While CFPreferencesAppSynchronize() will NOT CREATE a new file in /Library/Preferences using the kCFPreferencesAnyUser parameter, it WILL WRITE to an existing file, IF that file's permissions are already set to allow writing by an admin user (and that user is logged in). Since our installation requires admin privileges, when the user runs the Apple Installer, the elevated privileges of the Apple Installer can create that file in /Library/Preferences and give it admin write privileges, so that later during product activation the user's serial number can be added for all users to access.
Note that this plist file is NOT the plist containing user-specific preferences, nor would we expect the end-user running an already-installed app be writing to that file (which would fail if they weren't an admin user).
So this gets us around the issue (though I'm certain more experienced developers may have some thoughts to add).
My OS X app (currently not sandboxed) accesses files contained inside a directory set by the user (one chooses the path with a NSOpenPanel and a reference to this path is kept throughout execution). The list of files is generated via NSDirectoryEnumerator and I then read from and write to those files using AVAsset and taglib (in C++ with a bridging header) respectively.
As expected, enabling Sandboxing in Xcode rendered the app useless, the list of files given by NSDirectoryEnumerator is empty and even if it weren't, I would not be able to read from and write to the files. What are the steps I need to take to make my app sandbox-compliant?
Does my app need to be document based? Can my app really be "document-based" since I don't really have proper documents (as in: I don't have a window per file, it doesn't seem to comply to the standard document-based app model)? My app is basically just a table view with files references as rows.
Another important point: can I still use taglib to write to my files if my app is document-based ? I need to pass taglib the path to my file as a string pointer in order for it to work.
Thanks a lot, this topic is quite confusing at the moment.
You don't have to convert your app to be document-based to gain access to user selected files and security scoped bookmarks.
I can think of 2 reasons why your current code does not work in a sandboxed environment:
You don't have the "User Selected File Access" capability set (Xcode > target > Capabilities > App Sandbox > File Access)
You are using the path/NSString based API of the directory enumerator instead of the URL NSURL based one.
A vanilla Xcode project with Sandboxing enabled and the User selected files capabilities set, should enumerate any path obtained via NSOpenPanel:
NSOpenPanel* panel =[NSOpenPanel openPanel];
panel.canChooseDirectories = YES;
[panel beginSheetModalForWindow:self.view.window completionHandler:^(NSInteger result) {
NSFileManager *fileManager = [[NSFileManager alloc] init];
NSURL *directoryURL = panel.URL;
NSDirectoryEnumerator *enumerator = [fileManager
enumeratorAtURL:directoryURL
includingPropertiesForKeys:nil
options:0
errorHandler:nil];
for (NSURL *url in enumerator) {
NSLog(#"url:%#", url);
}
}];
If you want to store the ability to access specific folders from the sandbox across app launch/quit cycles, you will need to store a security scoped bookmark.
This post contains information persisting user selected file/directory access via app scoped bookmark:
Trouble creating Security-Scoped Bookmark
It sounds like the current functionality will convert to Sandboxing just fine.
The user selects the directory via NSOpenPanel (which will invoke something called Powerbox in the Sandboxed environment).
This directory is now writable, as the user has explicitly selected it.
You can even maintain write access to this directory by creating a Security Scoped Bookmark and storing it away between sessions.
This has got nothing at all to do with being Document based; that is an internal design that is unrelated to Sandboxing.
I've got an OS X App where I request app-scoped security bookmarks from the user using an NSOpenPanel - this works great.
Now I want to delete the file as well - this works for ALL files except for those stored in system locations, e.g. /private/var/log. Even though the user granted me a (not stale) security bookmark.
Is there any entitlement that allows me to delete user-selected files from those locations?
Just for reference, the following entitlements are set:
<key>com.apple.security.temporary-exception.files.home-relative-path.read-write</key>
<array>
<string>/.Trash</string>
</array>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.files.bookmarks.app-scope</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
Cheers!
Most of the files in system locations belong to user root and no one else has write permission. In order to delete such files, they need to have the appropriate permissions set. You can check this using terminal:
cd /private/var/log
ls -la
Just because your app has permission by sandbox doesn't mean it has permission by filesystem to write and remove. Sorry to say.
I believe all you need is access to the file's (parent) directory, as it's the directory that is modified when a file is deleted.
I assume you can do this using the same permission-granting mechanism you currently use.
Of course the user themselves don't have read/write access to all the files in the system, so that will limit the ability to delete system files. If you wanted to delete those then you need to implement privilege escalation.
I need to open a NSSavePanel with the users Library folder as destination folder. Normally I would do this by entering ~/Library/ in [NSSavePanel beginSheetForDirectory].
This works fine as long as the application is not sandboxed. For sandboxed applications this will result in the NSSavePanel trying to access a folder inside the applications document "box".
I cannot refer to /Users/username/Library/ as I do not know the users username at runtime. So how do I link to this path in cocoa?
I'm not sure how sandboxing fits in with this, but you can find the user's library directory using:
NSArray* paths = NSSearchPathForDirectoriesInDomains( NSLibraryDirectory, NSUserDomainMask, YES );
Not sure if this will work on a sandboxed application but this is how I do it right now. This will return /User/TheirUserName
-(NSString *)homeDirectory
{
return NSHomeDirectory();
}
It depends what you are trying to achieve.
If the behavior is required by your application, then you can request a temporary exception entitlement when submitting the application to the Mac App Store. But sooner or later, you will have to find a solution to remove this exception.
If you want to access data that were previously stored in the ~/Library/ folder, you can define a migration strategy to move back the data into the sandbox.