I need help with Security Scoped Bookmarks

Hi Everyone,

I’m trying to get Security Scoped Bookmarks (SSBs) working after it was suggested by apple support that my app needs them to pass review on the app store. My app renames files, and for that it needs permission not only to the file, but the parent folder of the file, so my workflow is this:

-User selects file (this gives them read/write access)
-App checks for an existing SSB from a previous session and tries to gain write access to the parent folder
-App attempts to rename the file
-If the rename fails to due to a read/write error, prompt the user to select the folder
-Save a security scoped bookmark for the folder after it is selected
-Rename the file now that we have proper access
-If the user tries to rename files in that folder during a different session, the bookmark is loaded and the user does not need to select the folder again

So that seems like it should be pretty straightforward, but I have a problem: I am able to save and resolve bookmarks, but when I try to use them to access the file it doesn’t grant access.

Admittedly, I’m stumbling around in the dark here. Has anyone else tried something similar? Here is my code, I’m not sure where I’m going wrong.

PseudoCode that gives an idea of when I’m creating bookmarks, resolving them, and trying to use them:

  Dim dlg as SelectFolderDialog 'prompt for parent folder access
  dim b as boolean
  dim fBefore as folderitem 'a file selected by the user, so it has read/write access already
  dim fAfter as folderitem 'holds the new name
  fAfter = fBefore.parent.child("newname.jpg")
  if fAfter.exists = false then 'don't overwrite an existing file
    b= ssbStart(fBefore, true) 'looks for an existing bookmark for the parent folder if we created it in a previous session, resolves it, and starts accessing it
    'ssbStart always comes back false, even if the bookmark for this file's parent folder exists and resolves
    fBefore.Name = fAfter.Name 'tries to rename the file
    ssbStop(fBefore) 'we don't need access to the SSB anymore
    if fAfter.exists = false then 'the rename didn't work
      dlg = new SelectFolderDialog
      dlg.Title=getCaption("Please authorize access to this folder")
      dlg.ActionButtonCaption=getCaption("Authorize")
      dlg.InitialDirectory = fBefore.parent 'we need write access to the parent folder of the file
      ssbSelectFolder(dlg) 'prompts the user to auth the folder, saves the folder SSB for later access
      fBefore.Name = fAfter.Name 'the user selected the folder so now the rename will work
    end if
  end if

Actual function code below. SavePrefsData and GetPrefsData are not included but they store and retrieve strings in a SQL database.

[code]Sub ssbSaveBookmark(f as FolderItem)
'Creates a bookmark
#if TargetCocoa then
dim Bookmark as string
bookmark = CFBookmarkMBS.CreateBookmarkData(f, CFBookmarkMBS.kCreationWithSecurityScope)
savePrefsData(f.nativepath, Bookmark)
#endif
End Sub

Function ssbResolve(f as FolderItem, optional bParentDir as boolean = false) As FolderItem
'accepts a folderitem
'optionally uses the parent folder of the folderitem
'looks for a saved bookmark in preferences based on the file path
'if found, resolves the bookmark string into a folderitem and returns it
dim f2 as FolderItem
dim s as string
#if TargetCocoa then
if bParentDir = true then
'work with the parent Directory instead
if f.Directory = false then
f2 = f.Parent
else
f2 = f
end if
else
f2 = f
end if
dim Bookmark as string = getPrefsData(f2.NativePath, “”) 'returns a saved bookmark if we already created one for this file
dim isStale as Boolean
dim options as UInt32 = CFBookmarkMBS.kResolutionWithoutUIMask + CFBookmarkMBS.kResolutionWithoutMountingMask
if Bookmark <> “” then
dim file as FolderItem = CFBookmarkMBS.ResolveBookmarkData(Bookmark, options, nil, isStale) 'resolves Bookmark string to a file

  if isStale = true then
    ssbSaveBookmark(file) 'recreate the Bookmark since it's stale
  end if
  
  if file<>Nil then
    return file 'send back the resolved FolderItem with read/write access
  else
    dim e as CFErrorMBS = CFBookmarkMBS.LastError
    if e = nil then
      s =  "Failed to resolve." 'for debugging
    else
      s = e.Description 'for debugging
    end if
    return f 'bookmark did not resolve, send back the original folderitem
  end if
else
  return f 'no Bookmark string exists in prefs, send back the original folderitem
end if

#else
return f
#endif
End Function

Function ssbSelectFolder(dlg as SelectFolderDialog, optional form as Window) As FolderItem
'Displays the dialog passed and saves an SSB for the selected folder
dim f as FolderItem
if form <> nil then
f = dlg.ShowModalWithin(form)
else
f = dlg.ShowModal
end if
#if TargetCocoa then
if f <> nil then
ssbSaveBookmark(f)
end if
#endif
return f
End Function

Function ssbStart(f as FolderItem, optional bParentDir as boolean = false) As boolean
'called when ready to manipulate the file
#if TargetCocoa then
dim f2 as folderitem
f2 = ssbResolve(f, bParentDir)
return CFBookmarkMBS.StartAccessingSecurityScopedResource(f2)
#else
return true
#endif
End Function

Sub ssbStop(f as FolderItem, optional bParentDir as Boolean = false)
'called when access is no longer needed
#if TargetCocoa then
dim f2 as FolderItem
f2 = ssbResolve(f, bParentDir)
CFBookmarkMBS.StopAccessingSecurityScopedResource(f2)
#endif
End Sub

[/code]

One thing to check is that your sandboxed version isn’t trying to open bookmarks created by a non-sandboxed debug version. I ran into that before… The OS will copy the prefs from your non-sandboxed version to the sandbox if there is no prefs file in the sandbox. The OS can be very (un)helpful like that :-/

I’m using app-wrapper mini to sandbox the debug version, give it user-selected.read-write application specific bookmark entitlements.

Does it look like I’m doing it right: save the bookmark of the file after the user selects it, then resolve the bookmark string to a folderitem in a later session and call startAccessingSecurityScopedResource on the folderitem?

Sorry, I meant user-selected.read-write and application specific bookmarks. Do I need document specific as well?

I think my database might be messing it up, the bookmark I save is longer than what’s coming back. Maybe the DB is encoding it and ruining the bookmark which is why it isn’t working.

You’re clearly a masochist! SSBs are a PITA!

#1 I suspect it might have something do to with encoding, when saving and restoring. SSB data is a NSData object, which is closer to a MemoryBlock than a String.

#2 Someone else may be able to help here, as I the only time I’ve seen this is when the source file doesn’t exist and then you can’t recreate a bookmark!.

if isStale = true then ssbSaveBookmark(file) 'recreate the Bookmark since it's stale end if

#3 YOU MUST BALANCE EVERY START ACCESS WITH A END ACCESS. Double check your code, in fact triple check it or alternatively, you can do what I did, (with the suggestion from Thomas Templeman), which is to create a Xojo class, that holds the NSURLRef. Then in the class destructor, we end access.

So you code ends up something like below.

Dim token as SSBToken = startAccessing( currentMethodName ) ~ do stuff here
I track the method that created the token, so I can validate when it’s released and access is closed.

#4 In AWM, you can select just Application specific as you’re only storing the bookmarks within the application and not document files.

If I can think of anything else that might related to your situation, I’ll let you know.

I don’t see in this code how the CFURL or NSURL is being handled, but the URL object needs to be around until the access is closed.

The other thing I’ve recently found is holding onto the CFURL or NSURL can be a good thing and a bad thing.

Good!

  • It’s expensive to create the CF/NSURL from a bookmark.
  • It speed up repeated access to the item on the t’other end.

Bad

  • If the file/folder gets renamed, the CF/NSURL gets broke.
  • if the file/folder get moved, the URL gets broke.

So you’ll want to make sure that you release the CF/NSURL object when you no longer need it.

Sam, thanks for your responses.

I agree completely.

[quote=65780:@Sam Rowlands]You’re clearly a masochist! SSBs are a PITA!
#1 I suspect it might have something do to with encoding, when saving and restoring. SSB data is a NSData object, which is closer to a MemoryBlock than a String.
[/quote]

I think so too, how do you recommend I store the bookmarks?

[quote=65780:@Sam Rowlands]
#3 YOU MUST BALANCE EVERY START ACCESS WITH A END ACCESS.[/quote]

Right now I am doing:

Start Access
Rename File
Stop Access

Once I get it working I will probably need something more sophisticated and efficient, since I’m only working with parent folders I won’t need to do it for every file.

[quote=65780:@Sam Rowlands]
#4 In AWM, you can select just Application specific as you’re only storing the bookmarks within the application and not document files.[/quote]

Thanks, that’s what I thought.

I don’t think I ever get a CFURL object with this code. The MBS functions return a string when you create a bookmark, and a folderitem when you resolve the bookmark. I then pass that folderitem to StartAccessingSecurityScopedResource, which returns a boolean (always false for me which indicates a problem).

I just assumed that the folderitem returned when I resolve the bookmark would be writable once I called StartAccessing, but it’s not.

I’m happy using the MBS stuff if I can get it to work, but there aren’t any example projects for it which means all I have to go on is this page documenting the methods:

http://www.monkeybreadsoftware.net/alias-cfbookmarkmbs-method.shtml

So if there is some better method with declares I would be willing to give it a try.

In the original code that I posted I noticed I was resolving the bookmark without security scope. I’ve added CFBookmarkMBS.kResolutionWithSecurityScope to the options but alas, the fix didn’t make a difference.

I’m coming up with a much more simple bit of code to test all this. Sorry for that huge mess of code I posted originally.

Ok, here is a much more simple test that removes any encoding issues created by my database or by passing the code around to different methods. If you have MBS plugins it should work by dropping it into a pushbutton action. StartAccessingSecurityScopedResource still returns false for me.

[code]Sub Action()
Dim dlg as New SelectFolderDialog
dim s as string
dim f as FolderItem
dim b as boolean
dim fResolved as FolderItem
dim bookmark as string
dim isStale as Boolean
dim options as UInt32 = CFBookmarkMBS.kResolutionWithoutUIMask + CFBookmarkMBS.kResolutionWithoutMountingMask + CFBookmarkMBS.kResolutionWithSecurityScope

f = dlg.ShowModal
if f <> nil then
s = s + "Selected " + f.NativePath + EndOfLine
bookmark = CFBookmarkMBS.CreateBookmarkData(f, CFBookmarkMBS.kCreationWithSecurityScope)
s = s + "Bookmark length: " + cstr(len(bookmark)) + EndOfLine
fResolved = CFBookmarkMBS.ResolveBookmarkData(bookmark, options, nil, isStale) 'resolves Bookmark string to a file
if fResolved <> nil then
s = s + "Resolved to: " + fResolved.NativePath + EndOfLine
b = CFBookmarkMBS.StartAccessingSecurityScopedResource(f)
s = s + "SSB Start: " + CStr(b) + EndOfLine
CFBookmarkMBS.StopAccessingSecurityScopedResource(fResolved)
else
s = s + “Did not resolve”
end if
MsgBox s
end if
End Sub
[/code]

I did notice that the MBS StartAccessingSecurityScopedResource accepts either a URL or a FolderItem. I’m not sure where the URL would come from, though.

I tried passing the bookmark directly to StartAccessing and it also returned false.

Everything but StartAccessing seems to be working properly so now I think the problem must be that I’m just not sending it the right information.

I really wish I could edit the original post to take out that huge chunk of wandering code.

Try dropping CFBookmarkMBS.kResolutionWithoutUIMask and/or CFBookmarkMBS.kResolutionWithoutMountingMask. I’ve never had any luck with CFBookmarkMBS.kResolutionWithoutUIMask especially. I suspect it might be disallowed in sandbox, though that’s not mentioned anywhere in any of Apple’s docs.

Also, when storing the bookmarks I’ve always just used EncodeBase64 before saving and DecodeBase64 before resolving. Works well to avoid string encoding issues. I think CFBookmarkMBS should be using CFBinaryDataMBS objects rather than strings…? The API returns a CFDataRef which is not a string.

I decided to try a bunch of permutations for the available resolution options. No matter what I did, StartAccessing returned false.

dim optionsResolve() as UInt32 optionsResolve.append CFBookmarkMBS.kResolutionWithSecurityScope + CFBookmarkMBS.kResolutionWithoutUIMask + CFBookmarkMBS.kResolutionWithoutMountingMask optionsResolve.append CFBookmarkMBS.kResolutionWithSecurityScope + CFBookmarkMBS.kResolutionWithoutUIMask optionsResolve.append CFBookmarkMBS.kResolutionWithSecurityScope + CFBookmarkMBS.kResolutionWithoutMountingMask optionsResolve.append CFBookmarkMBS.kResolutionWithSecurityScope

The same goes for the creation options.

Of course I just noticed a bug in that test code, I was trying to StartAccess on f instead of fResolved. Stupid mistakes certainly aren’t helping. :slight_smile:

So here is where I’m at with a pushbutton and a textarea:

[code]Sub Action()
Dim dlg as New SelectFolderDialog
dim s as string
dim f, fResolved as FolderItem
dim b as boolean
dim i, j as integer
dim bookmark as string
dim isStale as Boolean
dim optionsCreate() as UInt32
optionsCreate.Append CFBookmarkMBS.kCreationWithSecurityScope

dim optionsResolve() as UInt32
optionsResolve.append CFBookmarkMBS.kResolutionWithSecurityScope
optionsResolve.append CFBookmarkMBS.kResolutionWithSecurityScope + CFBookmarkMBS.kResolutionWithoutUIMask
optionsResolve.append CFBookmarkMBS.kResolutionWithSecurityScope + CFBookmarkMBS.kResolutionWithoutMountingMask
optionsResolve.append CFBookmarkMBS.kResolutionWithSecurityScope + CFBookmarkMBS.kResolutionWithoutUIMask + CFBookmarkMBS.kResolutionWithoutMountingMask

output “CLEAR”

f = dlg.ShowModal
if f <> nil then
output "Selected " + f.NativePath
for i = 0 to ubound(optionsCreate)
output "Creation Option " + cstr(i) + " " + cstr(optionsCreate(i))
bookmark = CFBookmarkMBS.CreateBookmarkData(f, optionsCreate(i))
output "Bookmark length: " + cstr(len(bookmark))
for j = 0 to UBound(optionsResolve)
output "Resolution Option " + cstr(j) + " " + cstr(optionsResolve(j))
if bookmark <> “” then
fResolved = CFBookmarkMBS.ResolveBookmarkData(bookmark, optionsResolve(j), nil, isStale)
if fResolved <> nil then
output "Resolved to: " + fResolved.NativePath
b = CFBookmarkMBS.StartAccessingSecurityScopedResource(fResolved)
output "SSB Start: " + CStr(b)
CFBookmarkMBS.StopAccessingSecurityScopedResource(fResolved)
else
output “Did not resolve”
end if
else
output “Was not created”
end if
next
next
end if
End Sub

Sub output(s as string)
if s = “CLEAR” then
TextArea1.text = “”
else
TextArea1.Text = textarea1.Text + s + EndOfLine
end if
End Sub

[/code]

The output of that function is:

Selected /Users/Mike/Desktop
Creation Option 0 2048
Bookmark length: 620
Resolution Option 0 1024
Resolved to: /Users/Mike/Desktop
SSB Start: False
Resolution Option 1 1280
Resolved to: /Users/Mike/Desktop
SSB Start: False
Resolution Option 2 1536
Resolved to: /Users/Mike/Desktop
SSB Start: False
Resolution Option 3 1792
Resolved to: /Users/Mike/Desktop
SSB Start: False

I corrected my test code that can be dropped into a pushbutton and sent it to Christian at MBS to see if he can provide some insight.

[code]Sub Action()
Dim dlg as New SelectFolderDialog
dim s as string
dim f as FolderItem
dim b as boolean
dim fResolved as FolderItem
dim bookmark as string
dim isStale as Boolean
dim options as UInt32 = CFBookmarkMBS.kResolutionWithoutUIMask

f = dlg.ShowModal
if f <> nil then
s = s + "Selected " + f.NativePath + EndOfLine
bookmark = CFBookmarkMBS.CreateBookmarkData(f, CFBookmarkMBS.kCreationWithSecurityScope)
s = s + "Bookmark length: " + cstr(len(bookmark)) + EndOfLine
fResolved = CFBookmarkMBS.
ResolveBookmarkData(bookmark, options, nil, isStale) 'resolves Bookmark string to a file
if fResolved <> nil then
s = s + "Resolved to: " + fResolved.NativePath + EndOfLine
b = CFBookmarkMBS.StartAccessingSecurityScopedResource(fResolved)
s = s + "SSB Start: " + CStr(b) + EndOfLine
CFBookmarkMBS.StopAccessingSecurityScopedResource(fResolved)
else
s = s + “Did not resolve”
end if
MsgBox s
end if
End Sub[/code]

In the code above, the forums put a line break after fResolved = CFBookmarkMBS. that should not be there. Of course it says I don’t have permission to edit it. :slight_smile: