File/Folder access/scoping for background only apps

We create plug-ins for Adobe Creative Cloud and have run into an issue with respect to file/folder permissions. First, all of our libraries, code is code-signed and notarized as per Apple requirements but distribute outside of the Mac App store.

We install a Photoshop plug-in and its mainly a UI which then executes a background app containing the business logic to read/write files. The background app runs as a separate process and is not in the Photoshop sandbox space so it doesn't inherit Photoshop permissions/scoping rules. Our plug-in communicates with the background process via ports etc.

When a user chooses a file to process from lets say the Desktop, generally macOS first pops up a message that says ABCD background app is trying to access files from the Desktop do you grant it permission etc...This is also true for network mounted volumes or downloads folder. This message generally appears properly when everything is under an account with admin rights.

However, when our tool is installed from a Standard Account, the macOS messages asking for confirmation to access the Desktop or Documents or Downloads folder doesn't appear and access to the file/folders is denied. Thus our background only process errors out. Looking at the Security and Privacy->Files and Folders the button to enable access is in the Off position. If we turn these on Manually, everything works.

But this is a really poor user experience and sometimes our users think our software is not working.

Does anybody have any idea how to allow for the file/folder permissions to be registered/granted in such a case? Should we try to register these as Full Disk Access? Any ideas and/or solutions are welcome.

There, that's better...

I'm unaware of any difference between admin and standard users with respect to these kinds of permissions. Your description of the behaviour when installed in a standard account is how I would expect these apps to function normally. I think the most likely explanation is that most users (and developers and testers) use admin accounts and they haven't made a habit of resetting these permissions to test onboarding and initial setup. Full Disk Access is sometimes a solution. But that isn't a meaningful improvement over the default process. The user still has to manually go into System Settings. And in some cases, when people try to run these tasks as root, it can make these problems even more difficult. The root cause is attempting to use separate backgrounds tools. Why not just put all of the logic into the UI? Or take that further and make your app more stand-alone, with the plug-in being less important? I'm afraid there aren't any ways to technically work around the problem. The user has control. So rather than fight the system, look for different ways to engage with the user. Since your app isn't distributed via the Mac App Store, you have lots of options.

First off, ruling an issue out here:

However, when our tool is installed from a Standard Account, the macOS messages asking for confirmation to access the Desktop or Documents or Downloads folder don’t appear and access to the file/folders is denied.

Does your app include all of the relevant tcc strings (NSDesktopFolderUsageDescription, etc.)? Also, one thing to be careful of here is that there are a bunch of heuristics in this system that control how/when these are presented, which can make knowing "why" something is working a particular way difficult to determine. When you're looking closely at an issue like this, I'd always suggest working on a totally "clean" machine*, not your normal development machine.

*VMs are handy for this, as you can get the system to a fully configured state, then duplicate the VM image so you can always start over from the same exact starting point.

We install a Photoshop plug-in and it’s mainly a UI which then executes a background app containing the business logic to read/write files. The background app runs as a separate process and is not in the Photoshop sandbox space so it doesn't inherit Photoshop permissions/scoping rules.

Full disclosure, I have no idea how Photoshop's plugin system works these days. Is your Photoshop plugin written as a library that Photoshop loads and executes or is it an independent process/app that Photoshop runs?

Getting into details:

executes a background app containing the business logic to read/write files.

How are you doing this? Is the background app a launch agent or something else? Also, what exactly "is" your background app? Is it a true (presumably faceless) "app" that runs NSApplication, has a bundle ID, etc.?

Our plug-in communicates with the background process via ports etc.

How are you actually communicating? XPC or something else? The "official" way to transfer file access is what's described in "Share file access between processes with URL bookmarks”. However, while I know that will work over XPC, I'm not sure it will work through other communication pipes. Note that if you use this approach, make sure you read it carefully and don't pass anything into the options parameter. Somewhat confusingly, you DON'T want to create a "security-scoped bookmark", because that would actually restrict access (details depending on the bookmark type). What you're creating has implicit scope attached, which means the access of the creating process is extended to the receiving process.

There are other options for transferring access, but I want to understand what you're actually doing in more depth before we get into that.

Finally, on this point:

Should we try to register these as Full Disk Access?

I would recommend against this. It's a lot more access than you really want, there isn't any way to "know" if it's on or not, and the confusion around that makes it very difficult to know why something actually failed. Ultimately, that means you end up with a narrower "I can't access these files and I don't know why" problem, which is exactly where you started.

Does anybody have any idea how to allow for the file/folder permissions to be registered/granted in such a case?

SO, my big recommendation here is that you START by doing the interface work of:

  1. Create UI that tells the user that you can't open the file/object and asks them to give you access through an open panel.

  2. (optional) Create UI that allows the user to select directories you "should" have access to, which your background app then saves access to using bookmarks.

The idea here is that this is the "final defense" that ensures your app ALWAYS works. This final defense is important because:

  • macOS is sufficiently complicated that it's difficult to cover EVERY single possible case. The UI above means that missing an edge case doesn't break your app.

  • End user configurations are EXTREMELY difficult to predict or control. This is actually what makes #2 helpful- it lets people with unexpected/odd configurations fix any issue "themselves" without you needing to be involved.

  • Even if something works today, it's very hard to guarantee it will work in the future, either because the system changes or we introduce bugs. Note that the benefit of the open panel isn't that it's immune from bugs*, it's that it's SO critical that it's a pretty safe bet that we'll make fixing it a top priority.

*Over a long enough time scale, everything breaks.

That last point is really critical here. I'm sure we can figure out and fix whatever is happening here. I'm also fairly sure that this will happen again in some other context no matter what you do. Forcing the user to manually select files they "shouldn't" have to is annoying, but that's still better than failing without any solution.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

First of all, thank you for the answers. We went back and figured exactly what was occurring and the condition. Everything we did when I posted my original message was on a newly setup M4 Pro Mac with 15.5 as we like to test things continuously.

To investigate further we setup a VM with the same conditions Admin user/Standard user. On the VM everything was working under the Standard user account wherein the System did pop up message saying "Do you wish to grant ABCD" access to the Desktop or Downloads etc folder. We grant access and our plug-ins work as expected.

We went back to the actual M4 Pro machine and understood exactly what was occurring. The Admin user was Physically logged into the system though the GUI was not in use. But the Standard User for testing purposes was logging in via Screen Sharing to the M4 Pro Mac.

So the System was not popping up "Do you want to provide access to the Desktop etc folder" etc. in this condition to the Standard User. Where did the Message/Window to confirm access to the folder vanish to?

The moment the Admin User was physically logged off the Mac, everything started working for the Standard User even under Screen Sharing.

This makes us wonder if this is an edge case macOS bug?

So the System was not popping up "Do you want to provide access to the Desktop etc folder" etc. in this condition to the Standard User. Where did the Message/Window to confirm access to the folder vanish to?

There are more questions on this below, but I suspect what's going on here is you have two plugin instances (because Photoshop is running as a separate process in the two different login sessions) which are both talking to the same background process. I don't know how you ended up in that state, but it's impossible for the same process to "work" in different login sessions.

The moment the Admin User was physically logged off the Mac, everything started working for the Standard User even under Screen Sharing.

This makes us wonder if this is an edge case macOS bug?

No, I don't think this is a macOS bug. Questions I need answered here are these:

How are you doing this? Is the background app a launch agent or something else? Also, what exactly "is" your background app? Is it a true (presumably faceless) "app" that runs NSApplication, has a bundle ID, etc.?

Also, how do your background app and your plugin "find" each other so they can start communicating?

The reason this is important is that what you're describing is what happens when app component ends up (incorrectly) "crossing" between users’ sessions. Instead of each user having their own instance of your background app/process, both users end up talking to the same process. The first user works because the component is partially "entangled" with that user session and other users fail because the system is deeply confused by the entire state.

A few other things you can try:

  • Flip the users, so the standard user runs first, then the admin. I suspect the admin user will start failing, because what matters is the session that first launched the background app, NOT the account type.

  • Try the same thing with multiple admin users.

My guess is that you'll find that the second user always fails, but let me know if something else happens.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

We didnt have 2 sessions of our plug-in/Photoshop being invoked. The admin user was just physically logged in for other purposes. So, only 1 instance was running of Photoshop/our plug-in/background process always under the Standard user account.

To answer your questions the following is the code we use for finding/launching is below -

[[NSWorkspace sharedWorkspace] openApplicationAtURL:app_path  configuration: configuration completionHandler:^(NSRunningApplication* app, NSError* error)   if (error) {              NSArray *directory_path_arr = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSLocalDomainMask, YES);            NSString *filePath = nil;                                            if(directory_path_arr)                              {                                  NSString *directory_path = [directory_path_arr objectAtIndex:0];                                  NSString *filePath = [directory_path stringByAppendingPathComponent: [NSString stringWithUTF8String:(const char *)gAppSupportFolderName]];                                                                    if(filePath)                                     [[NSWorkspace sharedWorkspace] launchApplication:filePath];                             }                             NSLog(@"Failed to run the app: %@", error.localizedDescription);                         }                    }];

Configuration is set to [configuration setPromptsUserIfNeeded: NO]

We use TCP/IP sockets to communicate between the plugi-n and our background process.

First, jumping back to the testing side, have you specifically confirmed in your VM that you see the same problem when you login an admin user, followed by standard user. I want to be clear on this point because with any macOS investigation it's CRITICAL to differentiate between:

  1. A "general" issue that's happening on a broad set of machines configured in a specific, controlled way.

  2. A "specific" issue that's happening on a particular machine.

The difference here is critical as it completely changes how you focus the investigation. In the first case, the focus is on finding out "what makes your app different" (because something about it is confusing the machine), while the second case is about "what makes this machine different".

We didnt have 2 sessions of our plug-in/Photoshop being invoked. The admin user was just physically logged in for other purposes. So, only 1 instance was running of Photoshop/our plug-in/background process always under the Standard user account.

Huh. First off, how exactly does your plug-in run? That is:

  1. Loaded directly into the photoshop process.
  2. Run in a secondary helper process that is NOT "yours".
  3. Run as it's own process that you "fully" control.

Next, what is the user ID of whatever processes are involved here? Is it the standard user, the admin user, or something else?

To answer your questions the following is the code we use for finding/launching is below

A few questions about the code here:

  • Which code path is actually succeeding when this failure happens, openApplicationAtURL and launchApplication?

  • If openApplicationAtURL fails, what error did it return?

  • You're passing in different paths two the two cases, but what are those paths and, in the launchApplication case, is that path "reasonable"?

  • When you're in the failure configuration, what happens if you manually launch your helper app before you start the test? Does "openApplicationAtURL" find it (as I'd expect it to)? And does the problem still happen?

One warning here:

We use TCP/IP sockets to communicate between the plugin and our background process.

The big issue here is that it opens the door to miscommunication issues that would otherwise not occur. If two instances of your helper process are running, how do you know you're connecting to the correct one?

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

What we did was go back to the original M4 Pro macOS machine we just configured and went through the process of repeating the steps which allowed us to discover the condition in the first place.

Our plug-in is completely loaded into Photoshop process. The faceless background app is launched by our Photoshop plug-in.

We poured through all logs and the faceless background app we invoke runs without any errors. Nothing is reported anywhere so the first call openApplicationAtURL succeeds.

If we manually launch the background process the result is the same as our original findings.

We don’t allow our background app to run more than one instance under the same user.

OK, so this is the important (and quite strange) point we need to focus on:

If we manually launch the background process, the result is the same as our original findings.

Assuming my understanding is correct, this would seem to mean that Photoshop itself (and your plugin) are basically irrelevant to the process, but I think that's something that's worth specifically confirming. So, the simplest test here is to hard-code a path into your background app, then have it attempt to open that path shortly after launch. If that reproduces the problem, then the next step would be to strip your background app down into a minimal test case, then file a bug and post the bug number back here.

If that doesn't reproduce the problem, then the next step is to take a closer look at Photoshop’s interaction with that file, as I suspect the problem has something to do with how Photoshop is interacting with that file. One other test along those lines is this:

  • Create another file in the same location as the file you'll be modifying with Photoshop. Make sure nothing at all has that file open after you create it.

  • When the app passes the target file to your background app, try having it open that "extra" file at the same time as the target.

If the target file can be opened and/or triggers the dialog, then I think what's actually going on here is that something is blocking the access at a lower level (before the TCC dialog would be triggered).

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

File/Folder access/scoping for background only apps
 
 
Q