How to Trigger Permission Dialogue for Accessing the User's Desktop in macOS?

In my app I need to have access to the users desktop, and I would like to implement the standard dialogue for the user to give permission for this access at launch. I do not want to use the NSOpenPanel() for the user to select the desktop, as I dont think that is an elegant solution.

However I am having issues implementing this.

I use the following code to be granted access to the Desktop URL:

let accessGranted = desktopURL.startAccessingSecurityScopedResource()

However no dialogue box appears and the call returns false

I have also included "Desktop Usage Description" in my plist.

Here is my code

@State var message:String = "Good Luck!"
var body: some View {
VStack {
Button("Get Desktop files") {
accessDesktopWithPermission()
}
Text(message)
}
.padding()
}
//: –—–—–—–—–—–—–—–—–—–—–—–—–—–— ://
func accessDesktopWithPermission(){
guard let desktopURL = getDesktopURL() else{
return
}
let accessGranted = desktopURL.startAccessingSecurityScopedResource()
if accessGranted{
if let content = try? FileManager.default.contentsOfDirectory(at: desktopURL, includingPropertiesForKeys: nil ){
message = "Found \(content.count) on Desktop"
}
else{
message = "issue loading file from desktop"
}
}
else{
message = "Access denied to:\(desktopURL )"
}
}

obviously I have setup something incorrectly so I have also attached my code if anyone is interested to take a look.

[https://www.openscreen.co/DesktopAccess.zip)

Answered by DTS Engineer in 827746022

I use the following code to be granted access to the Desktop URL:

let accessGranted = desktopURL.startAccessingSecurityScopedResource()

I think you've misunderstood what "startAccessingSecurityScopedResource" actually does. The documentation describes this in more detail, but that API is used when you want to start using a security scoped URL. It does NOT provide direct access and will only work when you already have a security scoped URL.

That leads to here:

I have also included "Desktop Usage Description" in my plist.

That key (and similar ones) aren't actually relevant to a sandboxed app. If you look at the documentation for NSDesktopFolderUsageDescription, it described two kinds of access.

  1. The user granted the user access to that location (for example, through an open panel).

  2. The app directly accessed a location without any user involvement/consent.

It then says:

"The first time your app tries to access a file in the user’s Desktop folder without implied user consent, the system prompts the user for permission to access the folder’s contents."

In other words, the system will present that dialog when your app tries to go through the second access type above.

However, the key point for a sandboxed app is that it then later says:

"App Sandbox enforces stricter limits on Desktop folder access, so that policy may supersede this one if your app enables sandboxing. See App Sandbox for more information."

What isn't clear from that languages is that for most user directories (including "Desktop"), the sandbox's policy is very simple. It ONLY allow access the user has granted (#1 above). That's why the system never presents your dialog- the sandbox denied access to "Desktop" before the system would have presented the dialog. Similarly, if you turn off the App Sandbox it will immediately start showing the permission dialog.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

I use the following code to be granted access to the Desktop URL:

let accessGranted = desktopURL.startAccessingSecurityScopedResource()

I think you've misunderstood what "startAccessingSecurityScopedResource" actually does. The documentation describes this in more detail, but that API is used when you want to start using a security scoped URL. It does NOT provide direct access and will only work when you already have a security scoped URL.

That leads to here:

I have also included "Desktop Usage Description" in my plist.

That key (and similar ones) aren't actually relevant to a sandboxed app. If you look at the documentation for NSDesktopFolderUsageDescription, it described two kinds of access.

  1. The user granted the user access to that location (for example, through an open panel).

  2. The app directly accessed a location without any user involvement/consent.

It then says:

"The first time your app tries to access a file in the user’s Desktop folder without implied user consent, the system prompts the user for permission to access the folder’s contents."

In other words, the system will present that dialog when your app tries to go through the second access type above.

However, the key point for a sandboxed app is that it then later says:

"App Sandbox enforces stricter limits on Desktop folder access, so that policy may supersede this one if your app enables sandboxing. See App Sandbox for more information."

What isn't clear from that languages is that for most user directories (including "Desktop"), the sandbox's policy is very simple. It ONLY allow access the user has granted (#1 above). That's why the system never presents your dialog- the sandbox denied access to "Desktop" before the system would have presented the dialog. Similarly, if you turn off the App Sandbox it will immediately start showing the permission dialog.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

In My build settings, for Signing I set "Enable App Sandbox" to No, and I still didn't get any difference in behaviour. I even delete the sandbox from the "Signing & Capabilities" and still the same behaviour. Can you update the app that I had attached and make it respond as I want?

In My build settings, for Signing I set "Enable App Sandbox" to No, and I still didn't get any difference in behaviour. I even delete the sandbox from the "Signing & Capabilities" and still the same behaviour.

Did you look closely at what happened without the App Sandbox? You may be allowed to view the contents without any warning, as we may not trigger the dialog until you actually access something.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

I used the following code:

func getDesktopConent(){
let fileManager = FileManager.default
let desktopURL = fileManager.urls(for: .desktopDirectory, in: .userDomainMask).first!
guard let resolovedDesktopURL = resolveFinderAlias(at: desktopURL) else{
message = "Could not resolve desktop content"
return
}
if let content = try? FileManager.default.contentsOfDirectory(at: resolovedDesktopURL, includingPropertiesForKeys: nil ){
message = "Found \(content.count) on Desktop"
}
else{
message = "Could not access desktop"
}
}

and get the message "Could not access desktop"

The code snippet below will behave in one of two ways:

func getDesktopContent(){
let fileManager = FileManager.default
let desktopURL = fileManager.urls(for: .desktopDirectory, in: .userDomainMask).first!
if let content = try? FileManager.default.contentsOfDirectory(at: desktopURL, includingPropertiesForKeys: nil ){
message = "Found \(content.count) on Desktop"
}
else{
message = "Could not access desktop"
}
}
  1. In a sandboxed app it will always fail, as sandboxed apps are not allowed to "blindly" access directories like this.

  2. In a non-sandboxed app, the system "may" trigger an authorization dialog to the user showing the text of "NSDesktopFolderUsageDescription" and failing the request if the user denies the app access.

I use the word "may" here because:

  • There are a broad variety of edge cases which would cause the system NOT to present the dialog, but allow access.

  • There are other edge cases which would cause the system NOT to present the dialog, but deny access.

  • There are (probably) edge cases where the system would present the dialog, the user would approve access... and the app would still be denied access.

What I'm describing is the baseline behavior (for example, when this code is first run in a new app and a base system install), not an attempt to cover every possible edge case.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

No, this does not work.

SO, let me try and list out some of the edge cases that effect this:

There are a broad variety of edge cases which would cause the system NOT to present the dialog, but allow access.

The main case here is that the system thinks it already gave you access. Having Full Disk Access is one example, but there can also be cases where a file your app interacted with at some earlier point in a session gave you access.

There are other edge cases which would cause the system NOT to present the dialog, but deny access.

The reverse of this is also common, that is the system denying access because it believes it already denied access. However, what can make this issue tricky is that the system definition of "your app" can be stickier than you might expect. For example, simply toggling the app sandbox on/off doesn't always work because the system thinks it already denied the sandboxed version and thinks of both apps as "the same".

One way to address this is to use the command found in the documentation:

 tccutil reset SystemPolicyDesktopFolder <bundleID>

The resets the environment, starting everything from "scratch".

However, you can also force the system to treat your app as "new" by doing the following:

  1. Clean your build folder to make sure Xcode generate an entirely new build.

  2. Change your apps bundle ID, since this is the primary identifier tcc relies on.

  3. Make "some" change to your apps source that makes it "different" than the previous build. The change doesn't have to be meaningful (for example, you change one character in any print statement) but it does have to effect the final compiled product (so, for example, modifying a comment won't work). The issue here is that parts of the system also use the build UUID as an identifier and you want to ensure you're got a different build UUID.

There are (probably) edge cases where the system would present the dialog, the user would approve access... and the app would still be denied access.

This is the most open ended case, but the basic issue is that whether or not you ACTUALLY have access is a multistep process with MANY different failure points. The tcc check occurs relatively "early" in the cycle, which means the dialog can be presented and your access fail because a later stage rejected you.

In addition, the dialog itself also creates a much "longer" time cycle than would other wise occur which allows race conditions that would otherwise be quite difficult. For example (and this is an artificial example) the dialog introduces enough delay that that you could change the permission on ~/Desktop/ after the dialog was presented but before the call returned, something that woud otherwise be quite difficult to do.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

How to Trigger Permission Dialogue for Accessing the User's Desktop in macOS?
 
 
Q