The Graph API might be great in some regards, but in others leaves a lot to be desired. One such example is the permission granularity it offers, or in other words the fact that an app running with application permissions is able to perform actions against any and all resources of a given type (i.e. Mail.Read gives you access to every single mailbox within the tenant). To address such concerns, different teams within Microsoft took different approaches, for example the Exchange PG introduced the so-called application access policies. “Native” integration within the Exchange RBAC controls followed, but on the Graph side of things, no scoping mechanism has been introduced.
The lack of unified approach is of course not a great experience for any developer new to the Graph, but more concerning is the fact that to date, we still have no way to restrict access to many resource types. This is not to say that Microsoft is idling and ignoring this topic altogether. In fact, the SharePoint Online team just released a set of new permissions/scopes that bring much needed improvements. Which is what we will explore in this article. Let’s dive right in!
Refresher on Sites.Selected permission
In all fairness, the Graph API already had limited support for scoping SharePoint Online application permissions. In fact, we already covered this back in 2021, when Microsoft initially introduced the Sites.Selected scope. In a nutshell, this method allowed you to designate read-only or full access permissions on a per-site (site collection) basis, by means of stamping the corresponding application id and desired permission level as part of the /permissions resource. This, combined with the aforementioned Sites.Selected scope allows to restrict what the app can access.
In effect, an application configured with Sites.Selected permission will only get access to the set of sites for which we have explicitly added a /permissions entry. Any sites lacking such entry would deny access to the application. Unfortunately, this approach doesn’t work that well when you want to restrict the app from being able to access specific site(s), as you still have to stamp the /permissions entry on every other site. Nor does it help in scenarios where the application had been granted tenant-wide access via the all encompassing Sites.*.All permission, so you should still account for that!
At that time, the Sites.Selected permission was introduced only for the application permissions scenario. In other words, it did not support delegate permissions, i.e. the application running in the context of a given user. While we can argue that for such scenarios we can use other controls to restrict access, there is one corner case that still needed to be addressed – namely the scenario where you want to restrict this specific application to accessing data in specific sites only, even if the authorized user himself is not limited to just said site(s). Thus, we arrive at the first new addition, namely support for Sites.Selected permission for delegate access!
Delegate Sites.Selected
The process of granting Sites.Selected permissions for an application that leverages the delegate permissions model is the same as before. In a nutshell, you use the Sites.FullAccess.All scope to create a /permissions entry for the app on each of the sites in question and designate the desired access level. To illustrate this, let’s use the Graph explorer application for our “target” app and pick a random Group/Team site. First, we need to add the corresponding permission entry, in other words issue a POST query against the site’s /permissions endpoint, with the following payload:
POST https://graph.microsoft.com/beta/groups/12c96e98-45fd-4591-93e5-cca4fd91666b/sites/root/permissions { "roles": [ "read" ], "grantedToIdentities": [ { "application": { "id": "de8bc8b5-d9f9-48b1-a8ad-b748da725064", "displayName": "Graph Explorer" } } ] }
Once the permissions have been added, we need to also add the Sites.Selected (delegate) scope to the Graph Explorer’s service principal object itself, and clear any additional SPO-related consents while we are at it. As usual with any permission-related thing, it’s best to test the outcome in a fresh/private session. Then, we can verify the behavior of the app. With the Sites.Selected scope granted, we can use the Graph explorer tool to access the site in question, enumerate items and so on. However, a DELETE operation fails, as we only have read-only access.
Attempting to perform similar operations against a different site, one for which we did not configure a permissions resource matching the application id of the Graph explorer tool results in 403 Forbidden errors. One exception to this rule seems to be the case of enumerating drives for the group, which returns an empty 200 OK response instead. Either way, no metadata or content of any item stored within the group’s site is ever exposed, even though the user itself has more than sufficient permissions to get to it!
As with the application permissions scenario, it is important to remember that permission entry stamped on the parent site will be inherited by all its children sites. In other words, granting the Sites.Selected scope will result in the application having access to any and all item within the site, including in any child sites. In some cases, more granularity is needed!
Files.SelectedOperations.Selected joins the fray
Time to introduce the second new addition. At long last, Microsoft added support for scoping permissions on per-item basis. In other words, we now have support for the Files.SelectedOperations.Selected scope, as well as similar granular scopes for Lists (Lists.SelectedOperations.Selected) and ListItems (ListItems.SelectedOperations.Selected) objects. While the naming convention might differ a bit compared to Sites.Selected, but the scopes in fact work in a very similar manner!
In a nutshell, you will again need to stamp the /permissions resource on the selected file/list/listitem object with the desired access level. To do this, you will need the Sites.FullControl.All scope, or a combination of Sites.Selected scope for the parent site with a FullControl or Owner role, as detailed in the documentation. The payload is also very similar, with both the roles and application elements being mandatory. So for example, if we want to again use the delegate permissions scenario and grant Graph explorer access to specific file stored within a user’s OneDrive for Business, we can use a POST requests against the /items/{itemId}/permissions endpoint:
https://graph.microsoft.com/v1.0/sites/michev-my.sharepoint.com,fb2c7594-e473-451a-85e9-097d3c08307e,89d635a1-2c32-445d-8d61-0b3d22da0690/drives/b!lHUs-3PkGkWF6Ql9PAgwfqE11okyLF1EjWELPSLaBpAQmExIbx8PRrfLqklOFgx5/items/01BATTRZPYO6DOMHXJQJHZ54PMGTSU432A/permissions { "roles": [ "write" ], "grantedTo": { "application": { "id": "de8bc8b5-d9f9-48b1-a8ad-b748da725064" } } }
The method outlined above will also work for folder objects, in which case the application will be granted access to all (file) objects within the folder. Note that the displayName property is not a mandatory one, you can safely omit it. Successful execution of the request is signaled via “201 – Created” response. At this point we can query the /permissions endpoint on the selected resource to confirm the permission entry has been added or test access directly. For the sake of completeness, here are the allowed roles you can use:
- read – Read the metadata and contents of the resource.
- write – Read and modify the metadata and contents of the resource.
- owner – Represents the owner role.
- fullcontrol – Represents full control of the resource.
If at some point you decide that you no longer need the permission entry, you can remove it via a DELETE request:
DELETE https://graph.microsoft.com/beta/sites/m365x84802758-my.sharepoint.com,7172544f-6f33-4f27-8c46-066912e40075,81c55a86-5f06-4e07-ad2b-f178a5bba6a0/drives/b!T1RycTNvJ0-MRgZpEuQAdYZaxYEGXwdOrSvxeKW7pqDDJvSAaFZRTYwZvV-LZZjd/items/01J3JCXR2VXRODID5H2VDZDJ67EVCE3L6Y/permissions/aTowaS50fG1zLnNwLmV4dHw4OWVhNWM5NC03NzM2LTRlMjUtOTVhZC0zZmE5NWY2MmI2NmVAM2EyZDY0OTEtYTUzOS00ZTQ3LTg3NTctYmFiOGJkYThlN2Vi
Remember that adding the permission entry is just part of the process. Similar to the Sites scenario we examined above, your application will need to have a matching scope granted, either via the delegate or the application permissions model. For the Graph explorer example we used above, we must ensure that the corresponding Files.SelectedOperations.Selected scope has been granted. Without the scope, trying to access the file in question will result in 403 Forbidden error message, regardless of the permissions granted on the user itself. Once we add the scope, the resulting permissions will get us access to the file.
Having access to the desired file is one part of the story. Equally important one is ensuring no other items can be accessed by the same application/user combo. In the above scenario, trying to access any other file will result in 404 Not found error, again regardless of what permissions the user has on the file. This discrepancy in the error message is likely something that Microsoft added in order to prevent “enumeration” type attacks. Regardless, the important part is that the given application will NOT be able to access any additional files when using the Files.SelectedOperations.Selected scope. Only items stamped with a matching permission entry listing the application will be accessible!
Lists.SelectedOperations.Selected and ListItems.SelectedOperations.Selected
As mentioned above already, apart from the Files.SelectedOperations.Selected scope, Microsoft also introduced two similar scopes covering List and ListItem resources, Lists.SelectedOperations.Selected and ListItens.SelectedOperations.Selected, respectively. As they work in the same manner, we will not cover specific examples here. However, few important notes are due here.
First, and you might have already noticed this above, the permission entry we stamped is “agnostic” to the scopes granted on the application. While we used Files.SelectedOperations.Selected to test access, any of the other newly-introduced scopes is a viable option as well. You can repeat the same test with ListItems.SelectedOperations.Selected permissions and arrive at the same result. Effectively, you can think of the newly-introduced permissions as another scoping mechanism, allowing you to more granularly control access for the application. In “ascending” order we have the Files < ListItems < Lists < Sites scopes, each more “broader” than the former.
Here’s another way to describe this. As a file object within the Graph is technically a subset of the list item resource type, the ListItems.SelectedOperations.Selected and the Files.SelectedOperations.Selected scopes can be used interchangeably to work with files within document libraries. The latter is the “minimal” scope, and what you should be using if you want to adhere to the principle of least access in scenarios where you only need to grant access to a single file. Do remember that not every list item is a file though and plan your permissions accordingly!
On a similar note, be careful with application permissions. As there is no user element when using application permissions, this effectively breaks the permission inheritance on the given object (unless the permission is stamped on the site level).
Additional notes and summary
In addition to the scopes we detailed above, Microsoft has also introduced some additional ones for working with files. We have the Files.Read.Selected and Files.ReadWrite.Selected scopes, both only available as delegate permissions and marked as preview. Those should not be confused with the Files.SelectedOperations.Selected scope we detailed above! There is also the Files.ReadWrite.AppFolder scope, which seems to be specific to (consumer?) OneDrive and available in both application and delegate flavors.
In summary, Microsoft has expanded support for scoping Graph API permissions for working with SharePoint Online and OneDrive for Business objects. Not only this allows us to limit the scope of “all encompassing” application permissions such as Files.ReadWrite.All, but we can also use the same method for delegate permissions, addressing concerns with applications impersonating users to get access (*cough* Copilot *cough*). Furthermore, by providing scopes at different “depth”, we can go as “broad” or as “narrow” with our permission assignments as needed. Great stuff!
Additional details about the Selected permissions model can be found in the official documentation.
As it always is, after spending hours in troubleshooting and finally posting a question, one figures out how to do it.
The exact payload you have in this article works. I was confused as when you set the permission on a site collection level (and Sites.Selected permission), you need to use “grantedToIdentities” which is an array. With file/folder based permissions you use “grantedTo” object instead. Did followup article to explain this at https://blog.jussipalo.com/2024/11/how-to-use-filesselectedoperationsselec.html
Thanks Jussi. The risks of posting articles with /beta code… I’ll update the example.
I cannot set permissions. I can query permissions for a SPO folder just fine, but setting permissions gives Invalid Request error. Any idea what I’m doing wrong. I’m using Graph Explorer that has the following permissions: “Application.Read.All AppRoleAssignment.ReadWrite.All Directory.Read.All Files.ReadWrite.All OnlineMeetings.Read openid profile Sites.FullControl.All User.Read email”
I get permissions just fine using: https://graph.microsoft.com/v1.0/sites/xyz.sharepoint.com,8e099463-6a83-48c3-9c0e-59c3ca071054,15a706b9-af56-49dd-97ad-51325cee3b66/drives/b!A5QJjoNqw0icDlnDygcQVLkGpxVWr91Jl61RMlzuO2a6mn6WqINZRLEDcG_BOOKl/items/02QHANZNLH6DKOS3GO3FHKHKIC6AGODYML/permissions
but POST:ing permission payload to that same URL:
{
“roles”: [“read”],
“grantedToIdentities”: [
{
“application”: {
“id”: “30facd40-ec88-4c62-b5dc-8170a2ccaba1”,
“displayName”: “My-GenericAppRegistration”
}
}
]
}
returns:
{
“error”: {
“code”: “invalidRequest”,
“message”: “Invalid request”,
“innerError”: {
“date”: “2024-11-28T07:04:33”,
“request-id”: “f899b9ea-deb4-4d54-a76b-95157187ba7e”,
“client-request-id”: “4d6d397c-54a0-5feb-07db-c590ff4921d5”
}
}
}
I was able to set permissions like this for site level, it’s just this folder level that is not working…
I can’t make it work with app only permissions ! I have created an App with API permissions ListItems.SelectedOperations.Selected and Lists.SelectedOperations.Selected and have successfully POSTed write permissions for that app on a list and list item /permissions beta API endpoint. When I connect to Graph API with that app using its client ID and client secret and then GET the list or list item, I get “accessDenied” errors (HTTP/1.1 403 Forbidden). I can’t read or write. I’ve also tried on a driveItem.
Does it only work with delegated access ?
Are you able to set permissions for list items? It seems to me it’s not working at all.
Not directly, this part doesn’t seem to be working for now. But you can get the driveItem and follow the “regular” path to add/retrieve permissions.