Sounds intriguing…
I’m going to dive deeper…
References
- Microsoft: Microsoft 365 Copilot APIs overview (documentation)
- Microsoft’s Paolo Pialorsi: Microsoft 365 Copilot Retrieval API (Video)
Sounds intriguing…
I’m going to dive deeper…
Microsoft says “Initially, Sites.Selected existed to restrict an application’s access to a single site collection. Now, lists, list items, folders, and files are also supported, and all Selected scopes now support delegated and application modes.”. This article deep-dives into providing and using SelectedOperations.Selected granular permissions to SharePoint:
Lists.SelectedOperations.Selected | Provides application access to a specific list |
ListItems.SelectedOperations.Selected | Provides application access to one or more list items, files, or folders |
Files.SelectedOperations.Selected | Provides application access to to one or more files or library folders |
Set of SelectedOperations permissions is exactly what Microsoft promised a few years ago. And this is great, as we really need granular access to SharePoint sites. I’ve been supporting enterprise SharePoint for more than 10 years now, and I know that it was always a concern when application require access to a list/library or a folder or even document, but admins have to provide access to entire site.
Especially, I believe, this feature becomes more in demand because of Copilot for Microsoft 365. As for now – it’s mostly developers and data analytics who needs unattended application access to SharePoint, but what if regular users powered with m365 Copilot license start creating autonomous agents?
So below is my lab setup, PowerShell scripts and guides with screenshots on how to provide granular (not to entire site but to library/list or folder, or even just one document or list item) permissions to SharePoint and how to use provided permissions.
First, we need an Admin App – an app we will use to provide permissions.
The only requirement – this app should have Microsoft.Graph Sites.FullControl.All API permissions consented:
For this lab/demo setup, I have created a team under Microsoft Teams (so it’s a group-based Teams-connected SharePoint site), then test list and test library:
There must be an App Registration for client application – application that will have access to Test-List-01 and Test-Lib-01 only. This app registration should have Microsoft Graph “Lists.SelectedOperations.Selected” API permissions consented:
and I will use Python to access SharePoint programmatically.
At this moment (we have a client app and secret, and “” API permissions, but did not provide for this app access to specific sites or libraries) – we should be able to authenticate to Microsoft 365, but not able to get any kind of data (we can get token, but other call to Graph API would return 403 error – Error: b'{“error”:{“code”:”accessDenied”,”message”:”Request Doesn\’t have the required Permission scopes to access a site.”,):
PowerShell script to provide selectedoperations.selected access for an app to a specific list would be as below. Here we use plain calls to MS Graph API. Full script for your refence is available at GitHub, but here is the essential part:
$apiUrl = "https://graph.microsoft.com/beta/sites/$targetSiteId/lists/$targetSiteListId/permissions"
$apiUrl
$params = @{
roles = @(
"read"
)
grantedTo = @{
application = @{
id = $clientAppClientId
}
}
}
$body = $params | ConvertTo-Json
$response = Invoke-RestMethod -Headers $Headers -Uri $apiUrl -Method Post -Body $body -ContentType "application/json"
I use Python console app as a client application. Link to the code at the GitHub is shared below under References, but the core part of the Python code is (I do not use any Microsoft or other libraries here, just plain requests to Microsoft Graph for authentication and for data):
import requests
import json
from secrets import clientSc, clientId, tenantId, siteId, listId
# specify client id, client secret and tenant id
# clientId = ""
# clientSc = ""
# tenantId = ""
apiUri = "https://login.microsoftonline.com/" + tenantId + "/oauth2/v2.0/token"
body = {
"client_id" : clientId,
"client_secret" : clientSc,
"scope" : "https://graph.microsoft.com/.default",
"grant_type" : "client_credentials"
}
try:
response = requests.post(apiUri, data=body)
token = json.loads(response.content)["access_token"]
except:
print("Error: ", json.loads(response.content)["error_description"])
exit()
print("Got token: ", token[0:10], "...")
headers={'Authorization': 'Bearer {0}'.format(token)}
# Get specific site list
print("Geting specific site list")
# graph_url = 'https://graph.microsoft.com/v1.0/sites/' + siteId + '/lists/' + listId
graph_url = 'https://graph.microsoft.com/beta/sites/' + siteId + '/lists/' + listId
graphResponse = requests.get( graph_url, headers=headers )
print(" Response status code: ", graphResponse.status_code)
if graphResponse.status_code == 200:
list = json.loads(graphResponse.content)
print(" List display name: ", list["displayName"])
Note.
I’m not sure if it’s a bug or my incorrect setup, but I noticed that if I provide access for the app to the list – app can read site.
TBC…
Python code samples published by Microsoft at the Microsoft Graph API reference pages use GraphServiceClient module. But you also can use just requests module and call Microsoft graph API directly, using requests.post or requests.get methods. Here I’m sharing my Python code samples.
https://github.com/VladilenK/m365-with-Python/tree/main/Graph-API-Plain
In 2021 Microsoft implemented “Sites.Selected” Graph API permissions to allow application access (without a signed in user) to specific sites (entire site only). In 2024 Microsoft implemented granular access – to specific list/libraries, as well as to specific documents/files and list items. Now name convention is *.SelectedOperations.Selected.
Permissions come in two flavors – delegated and application:
Below is how I authenticate and call Microsoft Graph API to work with SharePoint from Python application.
no MSAL or Azure libraries used:
import requests
import json
from secrets import clientSc
clientId = "7e60c372-ec15-4069-a4df-0ab47912da46"
# clientSc = "<imported>"
tenantId = "7ddc7314-9f01-45d5-b012-71665bb1c544"
apiUri = "https://login.microsoftonline.com/" + tenantId + "/oauth2/v2.0/token"
body = {
"client_id" : clientId,
"client_secret" : clientSc,
"scope" : "https://graph.microsoft.com/.default",
"grant_type" : "client_credentials"
}
response = requests.post(apiUri, data=body)
token = json.loads(response.content)["access_token"]
graph_url = 'https://graph.microsoft.com/v1.0/sites/root'
site = requests.get(
graph_url,
headers={'Authorization': 'Bearer {0}'.format(token)}
)
print(site.content)
print(json.loads(site.content)["webUrl"])
secrets is a Python file where I assign client secret to variable clientSc (so my secret is not shared on github). This is ok for demo purposes but generally, you should not hard-code secrets but keep secrets somewhere safe (Vault).
Using MSAL library to get bearer token:
https://github.com/VladilenK/m365-with-Python/tree/main/Graph-API-MSAL
There is a Microsoft.Graph PowerShell module provided by Microsoft which simplifies usage of Microsoft Graph API. Below is how to authenticate to MS Graph and how to search within SharePoint and Teams Microsoft 365 content using Microsoft.Graph PowerShell module.
Interactive authentication code sample:
# Prerequisites
Get-Module Microsoft.Graph.Authentication -ListAvailable
Get-Module Microsoft.Graph.Search -ListAvailable
# Interactive Authentication
$clientid = 'd82858e0-ed99-424f-a00f-cef64125e49c'
$TenantId = '7ddc7314-9f01-45d5-b012-71665bb1c544'
Connect-MgGraph -ClientId $clientid -TenantId $TenantId
For daemon app authentication we need a certificate configured in Azure App and installed on the user machine. Daemon app authentication code sample (please specify your tenant id, app (client) id and certificate thumbprint:
# App Authentication
$TenantId = ""
$clientID = ""
$certThumbprint = ""
Connect-MgGraph -ClientId $clientid -TenantId $TenantId -CertificateThumbprint $certThumbprint
# Search
$params = @{
requests = @(
@{
entityTypes = @(
"driveItem"
)
query = @{
queryString = "test*"
}
from = 0
size = 50
fields = @(
"title"
"description"
)
region = "NAM"
}
)
}
$res = Invoke-MgQuerySearch -Body $params
$res.HitsContainers[0].Hits
Note: when you are calling MS Graph Search API authenticated as user – you need to remove “region” parameter.
Code samples: https://github.com/VladilenK/m365-PowerShell/tree/main/KBA/Search
Search Microsoft 365 content programmatically: all articles index
Microsoft Graph API allows you to work with all the Microsoft 365 content – including search through Exchange e-mail messages, Yammer (Viva Engage) and Teams chat messages and surely OneDrive and SharePoint content (please refer to the original doc). Let me focus on searching in SharePoint Online and OD here but you can use the same technique to search through other Microsoft 365 services. I will use PowerShell but same ideas should work for other platforms/languages – Python, C#, node.js etc.
Assuming we have a registered Azure app configured correctly, including Secrets/Certificates blade and API permissions provided – we should be ready to authenticate and call Graph API unattended – on behalf of application itself.
Let us authenticate as a service/daemon app with client id and client secret:
# Authenticate to M365 as an unattended application
# specify your app id. app secret, tenant id:
$clientID = ""
$clientSc = ""
$TenantId = ""
# Construct URI and body needed for authentication
$uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
$body = @{
client_id = $clientID
client_secret = $clientSc
scope = "https://graph.microsoft.com/.default"
grant_type = "client_credentials"
}
# Get OAuth 2.0 Token
$tokenRequest = Invoke-WebRequest -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing
$token = ($tokenRequest.Content | ConvertFrom-Json).access_token
$headers = @{Authorization = "Bearer $token" }
Below is how I search Microsoft 365 content programmatically from PowerShell using MS Graph API being authenticates as user.
# Search
$entityTypes = "['driveItem','listItem','list','drive','site']"
$entityTypes = "['driveItem','listItem']"
$query = "LastModifiedTimeForRetention<2021-01-01"
$apiUrl = "https://graph.microsoft.com/beta/search/query"
$query = "test*"
$body = @"
{
"requests": [
{
"entityTypes": $entityTypes,
"query": {
"queryString": "$query"
},
"from" : 0,
"size" : 5,
"fields": ["WebUrl","lastModifiedBy","name" ],
"region": "NAM"
}
]
}
"@
$res = Invoke-RestMethod -Headers $Headers -Uri $apiUrl -Body $Body -Method Post -ContentType 'application/json'
$res.value[0].searchTerms
$res.value[0].hitsContainers[0].hits
$res.value[0].hitsContainers[0].hits.Count
$res.value[0].hitsContainers[0].moreResultsAvailable
Notice we use “region” – it is required to search with Graph API under application credentials. Otherwise you will get an error message “SearchRequest Invalid (Region is required when request with application permission.)”:
Parameter “fields” allows you to request only fields you need to be returned. As returning object will be smaller your request will perform faster.
There might be a big number of objects found in m365 upon your request. Graph will not always return to you all the results. AFAIK currently the limit is 500, so if there are more than 500 objects found – only first 500 will be returned. You can specify how many objects you need to be returned per call with “size” parameter.
You can check value of $res.value[0].hitsContainers[0].moreResultsAvailable property and if it’s True – that means there are more results. The value above and parameters “from” and “size” would allow you to organize a loop so you can call search API many times to return all results.
Search in Yammer (Viva Engage) is special. In Viva Engage there are files and there are posts (messages).
So to search through files (pictures, documents) shared via Viva Engage – you can use Graph API SharePoint search, as all Viva Engage files (documents, pictures etc.) are stored in SharePoint site every community got at creation.
To search through Yammer messages (Viva Engage posts, questions, answers, discussions etc.) you have to use classic Yammer API (see details on Yammer API usage), because currently (Mar 2025) Microsoft Graph API does not support search in Viva Engage (Yammer) messages, even with beta.
Microsoft Graph API allows you to work with all the Microsoft 365 content – including search through Exchange e-mail messages, Yammer (Viva Engage) and Teams chat messages and surely OneDrive and SharePoint content (please refer to the MS’s original doc). After we got a registered Azure app configured correctly, including Authentication and API permissions provided (more on this) – we should be ready to authenticate and call Graph API on behalf of a user.
Let me focus on searching in SharePoint Online and OD here but you can use the same technique to search through other Microsoft 365 services. I will use PowerShell but same ideas should work for other platforms/languages – Python, C#, node.js etc.
Let us authenticate first. We’d need a MSAL.PS module for that.
# Ensure we have MSAL.PS module installed
Get-Module MSAL.PS -ListAvailable | ft name, Version, Path
# Install-Module MSAL.PS -Force -Scope CurrentUser -AcceptLicense
Import-Module MSAL.PS
# Authenticate to Microsoft Interactively
$clientid = 'd82858e0-ed99-424f-a00f-cef64125e49c' # your client id
$TenantId = '7ddc7314-9f01-45d5-b012-71665bb1c544' # your tenant id
$token = Get-MsalToken -TenantId $TenantId -ClientId $clientid -Interactive
$headers = @{Authorization = "Bearer $($token.AccessToken)" }
Below is how I search Microsoft 365 content programmatically from PowerShell using MS Graph API being authenticates as user:
# Search
# MS Graph Search API url (beta or v1.0):
$apiUrl = "https://graph.microsoft.com/beta/search/query"
# specify where to search - entity types
$entityTypes = "['driveItem','listItem','list','drive','site']"
$entityTypes = "['driveItem','listItem']"
# query
$query = "test*"
# build a simple request body
$body = @"
{
"requests": [
{
"entityTypes": $entityTypes,
"query": {
"queryString": "$query"
}
}
]
}
"@
# call Graph API:
$res = Invoke-RestMethod -Headers $Headers -Uri $apiUrl -Body $Body -Method Post -ContentType 'application/json'
# explore returned object
$res.value[0].searchTerms
$res.value[0].hitsContainers[0].hits
$res.value[0].hitsContainers[0].hits.Count
$res.value[0].hitsContainers[0].moreResultsAvailable
I used “beta” search API to research or make demos, but in production code youd stick with “v1.0”.
You can scope search down using entity types – ‘driveItem’,’listItem’,’list’,’drive’,’site’. “driveitem” here represents document library.
In query you can use KQL.
Always check if more results available with “$res.value[0].hitsContainers[0].moreResultsAvailable”. If there are more results and you need them – consider looping using paging technique.
Search in Yammer – different story. You can take a look at this article to search through Viva Engage (Yammer) messages.