Tag Archives: PowerShell

Automating SharePoint operations with Azure Functions

There are many scenarios for SharePoint or Teams automations, and in most cases you need to run some code on scheduled basis (e.g. every 5 minutes or every 24 hours etc.). This is where timer-triggered Azure Functions might help. In this article I will provide use cases, overview of the whole scenario and technical setup, and provide links to the detailed step-by-step guides for configuring different parts of the entire solution.

Possible scenarios

Possible scenarios (end-user-oriented):

  • Create site upon user request
  • Convert site to a HUB site upon user request
  • Set site search scope upon user request
  • Setup site metadata (site custom properties)
  • Request usage reports/analytics

Possible scenarios (admin-oriented):

  • Provide temporary access to the site (e.g. during troubleshooting)
  • Provide Sites.Selected permissions for the App to the Site
  • Disable custom scripts or ensure custom scripts are disabled
  • Enable custom scripts (e.g. during site migration)
  • Monitor licenses – available, running out etc.

Typical setup

Front-end

SharePoint site works as a front-end. You do not need to develop a separate web application, as It’s already there, with reach functionality, secured and free.

The site can have:
– one or more lists to accept intake requests
– Power Apps to customize forms
– Power Automate to implement (e.g. approval) workflows, send notifications etc.
– site pages as a solution documentation
– libraries to store documents provided as response to requests

You can provide org-wide access to the site if your intention is to allow all users to submit requests or secure the site if you want to accept requests only from a specific limited group of people.

Back-end

Timer-triggered Azure Function works as a back-end. The function can be scheduled to run based on job specific requirements (e.g. every 5 or 10 minutes, or daily or weekly etc.). The function can be written in PowerShell, C#, Python etc.

The function’s logic is to

  • read SharePoint list, iterate through items to get intake requests
  • validate request eligibility
  • perform action
  • share results (e.g. update intake form, send e-mail, save document to library etc.)

Configuration

There should not be an issue to setup a front-end. You’d just need a solid SharePoint and Power Platform skills.

For the back-end the solution stack would include the following tools/skills:
– Azure subscription to host solution
– Registered Apps to configure credentials and API access permissions
– Azure Function App to actually run the code
– Azure Key Vault to securely save credentials
– programming skills in language/platform of choice
– SharePoint API, Microsoft Graph API

Please refer to the separate article Configuring Azure Function App and Key Vault to work with Microsoft 365 SharePoint via Graph API for the basic setup.

Secure

Having basic setup in place, we’d improve solution security. Specifically, we’d address the following:

  • Azure Function network security
  • Key Vault network security
  • Storage Account network security
  • Key Vault purge protection
  • tbc…

TBC…

References

Update SharePoint Site Title: GUI vs PowerShell

If you need to update a SharePoint site title (site name) programmatically (e.g. with PowerShell), and if this site is a group-based site (e.g. Microsoft Teams team site or Viva Engage community site or…) – you should not update SharePoint site title, but you should update group display name instead. Here is why.

In Microsoft 365 there is no sync from SharePoint site title to a group name. When you are updating SharePoint site title with GUI – you can see that new site title becomes new group/team name as well. So you might think that if you update SharePoint site title – Microsoft synchronizes it to connected group name. That’s not true. Actually when you are updating a group-based (e.g. teams-connected) SharePoint site title with GUI – Microsoft updates group first, then syncs updated group display name to SharePoint site name (title).

Here is the proof:

That’s a network trace I got with browser dev tools when I renamed site (updated site title) with GUI. So you can see the first API call is to update group, then group properties are synced back to site.

When we are updating a standalone site title – we are not seeing these calls.

So, if you need to update group-based site title programmatically – you must update group instead.

# does not work for group-based (e.g. Teams) sites:
Set-PnPTenantSite -Identity ... -Title "New Site Title"

# instead, you'd update group display name 
Set-PnPMicrosoft365Group -Identity ... -DisplayName "New Display Name"
# and site title will be updated accordingly

References:

Using Microsoft.Graph PowerShell to Search in Microsoft 365

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.

Authentication

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 with Microsoft.Graph

# 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

Video tutorial:

Search through Microsoft 365 SharePoint from code

Below is how do I search Microsoft 365 content programmatically from PowerShell using MS Graph API, PowerShell PnP, Microsoft Graph module, MSAL library being authenticated as user or daemon application. Let me focus on SharePoint content here but you can use the same technique to search through other Microsoft 365 services. Also, I’ll be using PowerShell but same ideas should work for other platforms/languages – Python, C#, node.js etc.

First, we need to be authenticated

Here is how to authenticate to Microsoft 365 Graph API.

Second, we need to be authorized

To search on behalf of currently authenticated user we need delegated “Sites.Read.All” API permissions. I recommend you to add both Graph API and SharePoint API permissions as different libraries might use different API’s under the hood. Ensure you add delegated “Sites.Read.All” even if you already have “Sites.FullControl.All” as by some reason “Sites.FullControl.All” does not always work for search.

Here is how an app API permissions to search as current user should look like:

app API permissions to search as current user

Actually, for interactive authentication app ownership is not required, so we can surely use our own registered app, but also it is possible to use any other app registered in Azure and properly configured, e.g. Enterprise “PnP Management Shell” app id: “31359c7f-bd7e-475c-86db-fdb8c937548e”
How do I create and configure Azure App with delegated permissions to SharePoint for PowerShel usage

For unattended search – e.g. search on behalf of daemon app – we need application “Sites.Read.All” API permissions. Again, I suggest both Graph API and SharePoint API permissions added. Here is how an app API permissions to search as daemon app should look like:

Ensure you got admin consent for API permissions.

In case you have incorrect permissions in your app – Microsoft Graph will be kind enough to inform you exactly what you need. Example:

“Access to ChatMessage in Graph API requires the following permissions: Chat.Read or Chat.ReadWrite, ChannelMessage.Read.All. However, the application only has the following permissions granted: Sites.Read.All, User.Read”

Assuming we have configured apps – let us get started with

Microsoft Graph API

Microsoft Graph API allows search through all the Microsoft 365 content – including Exchange e-mail messages, Yammer (Viva Engage) and Teams chat messages and surely OneDrive and SharePoint content (please refer to the original doc).

Authenticate as current user to Search with Graph API

I use MSAL.PS PowerShell module to get token, then I build a headers variable

# Prerequisites
Get-Module MSAL.PS -ListAvailable | ft name, Version, Path 
# Install-Module MSAL.PS -Force -Scope CurrentUser -AcceptLicense
Import-Module MSAL.PS

# Interactive Authentication
$clientid = 'd82858e0-ed99-424f-a00f-cef64125e49c'
$TenantId = '7ddc7314-9f01-45d5-b012-71665bb1c544'
$token = Get-MsalToken -TenantId $TenantId -ClientId $clientid -Interactive
$headers = @{Authorization = "Bearer $($token.AccessToken)" }

Authenticate as service/daemon app

You’d need to update the script providing Tenant id, client (app) id and client (app) secret:

# App Authentication
$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" }
$headers

Search m365 SharePoint and OD content with Microsoft Graph API

In this sample I limited search scope to list items only ($entityTypes = “[‘listItem’]”). Check other entity types here.

Code:

# Search
$entityTypes = "['listItem']"
$apiUrl = "https://graph.microsoft.com/beta/search/query"
$query = "*"
# body for interactive search
$body = @"
{ 
  "requests": [
    {
      "entityTypes": $entityTypes,
      "query": {
        "queryString": "$query"
      }
    }
  ]
}
"@

$res = Invoke-RestMethod -Headers $Headers -Uri $apiUrl -Body $Body -Method Post -ContentType 'application/json'
$res.value[0].hitsContainers[0].hits

If you are getting error message “SearchRequest Invalid (Region is required when request with application permission.)”:

that’s OK, just modify your body to include region like this (“region”: “NAM” for North America or “GBR” or …). Also, I can modify body with from/size for paging (technique used to iterate through search results if there are many) and return just specific fields to decrease traffic and improve performance:

# 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

We’d not use region for interactive calls or we’ll get “Region is not supported when request with delegated permission.”.

Microsoft.Graph PowerShell module

There is a Microsoft.Graph PowerShell module provided by Microsoft which simplifies authentication and search operations.

Interactive authentication code sample:

# Prerequisites
Get-Module Microsoft.Graph.Authentication -ListAvailable 
Get-Module Microsoft.Graph.Search -ListAvailable 

# Interactive Authentication
$clientid = '31359c7f-bd7e-475c-86db-fdb8c937548e'
$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
$clientID = ""
$certThumbprint = ""
$TenantId = ""
Connect-MgGraph -ClientId $clientid -TenantId $TenantId -CertificateThumbprint $certThumbprint

Code sample for SharePoint search with Microsoft.Graph PowerShell module

As currently authenticated user

# Search in the current user context
$params = @{
	requests = @(
		@{
			entityTypes = @(
				"driveItem"
			)
			query = @{
				queryString = "lorem"
			}
			from = 0
			size = 25
			fields = @(
				"title"
				"description"
			)
		}
	)
}

$res = Invoke-MgQuerySearch -Body $params
$res.HitsContainers[0].Hits
   

Again, in case with app authentication – an additional parameter – region – is required:

# Search
$params = @{
	requests = @(
		@{
			entityTypes = @(
				"driveItem"
			)
			query = @{
				queryString = "lorem"
			}
			from = 0
			size = 25
			fields = @(
				"title"
				"description"
			)
                        region = "NAM"
		}
	)
}

$res = Invoke-MgQuerySearch -Body $params
$res.HitsContainers[0].Hits

It’s a good idea to explore returning object.

PnP.PowerShell module

PnP.PowerShell allows you to search through Microsoft 365 SharePoint content with PowerShell style – using command and options.

Let us authenticate interactively:

# Interactive Authentication
$clientid = 'd82858e0-ed99-424f-a00f-cef64125e49c'
$TenantId = '7ddc7314-9f01-45d5-b012-71665bb1c544'
$siteUrl = "https://s5dz3.sharepoint.com"
Connect-PnPOnline -ClientId $clientid -Tenant $TenantId -Url $siteUrl -Interactive

Authentication on behalf of service/daemon app would require certificate installed on the machine and configured in the app and look like:

# Application (daemon) Authentication
$clientID = ""
$certThumbprint = ""
$TenantId = ""
$siteUrl = "https://contoso.sharepoint.com"
Connect-PnPOnline -ClientId $clientid -Tenant $TenantId -Url $siteUrl -Thumbprint $certThumbprint

There are no differences in Microsoft 365 SharePoint Search with PowerShell code samples for interactive and daemon apps (no region parameter).

Examples:

# search
$query = "test*"
$res = Submit-PnPSearchQuery -Query $query 

# examples of query:
$query = "test*"
$query = "* contentclass:STS_ListItem_DocumentLibrary"
$query = "* author:Patti"
$query = "test* site:https://s5dz3.sharepoint.com/teams/sxc"

# examples of submitting request
$res = Submit-PnPSearchQuery -Query $query 
Submit-PnPSearchQuery -Query $query -All 
Submit-PnPSearchQuery -Query $query -MaxResults 5
Submit-PnPSearchQuery -Query $query -SortList @{"LastModifiedTime" = "ascending"} 

# exploring result object:
$res.ResultRows.Count
$res.ResultRows[0]
$res.ResultRows.Title
$res.ResultRows.OriginalPath
$res.ResultRows.LastModifiedTime

Video tutorials

Video tutorials (playlist) on how to authenticate to Microsoft 365 and Search through Microsoft 365 content from code

References

Massive Microsoft 365 groups update with PowerShell

What if you need to bulk update Microsoft 365 groups membership e.g. to add a group owner or member for tens of thousands m365 groups? Iterating through groups one-by-one is unproductive and could take days. Can we do it faster? Here is what I found.

In my case, it was Microsoft 365 ownerless groups policy implementation for large tenant… Skipping details – I needed to update ownership for 10,000 Microsoft 365 groups and I was looking for a best/fastest possible option maybe some kind of bulk update or with multiple threads. And I figured out that the fastest way is to use PnP.PowerShell that calls Microsoft Graph API but run it against list of groups with PowerShell parallel trick. Here is the sample PowerShell code:

$groups | ForEach-Object -Parallel {
    $owner = "newGroupOwnerUPN@contoso.com"
    Add-PnPMicrosoft365GroupOwner -Identity $_.Id -Users $owner
} -ThrottleLimit 50

That worked for me perfectly and it took ~8 seconds per 1,000 groups.

How to create an old document in SharePoint

Sometimes, mostly during PoC or testing policies like retention policy or lifecycle policy you would need some documents created and updated weeks, months or even years ago.

But if you create or upload a document in SharePoint library – it will be just a regular new document. So, how to get old documents in the new environment?

I see two options:

  1. Sync with OneDrive
    If you sync a library with your local folder (done Microsoft by OneDrive desktop app) and put some old document in your synced folder – the doc will be synchronized back to SharePoint library with Created and Modified properties preserved.
  2. Make the document older with PowerShell
    With “Set-PnPListItem” PowerShell command you can update not only such properties like Title, but also “Created By”, “Modified By” and even date and time document was created and modified via “Created” and “Modified”.
    Optionally you can play with document history with “-UpdateType” parameter.
    UpdateType possible values are:
    • Update: Sets field values and creates a new version if versioning is enabled for the list
    • SystemUpdate: Sets field values and does not create a new version. Any events on the list will trigger.
    • UpdateOverwriteVersion: Sets field values and does not create a new version. No events on the list will trigger

Manage Microsoft 365 groups membership with PowerShell and Graph API

As SharePoint or Teams admin you manage Microsoft 365 groups (create, update, delete, manage membership etc.) having your admin role activated. I prefer PowerShell 7 and Microsoft.Graph PowerShell module, and I need an Azure registered app with “Group.ReadWrite.All” Microsoft Graph API delegated permission.

Some findings:

If a user was not a group member or group owner – and the user is added to the group members – this user will get notification “You’ve joined the <Group Name> group” via e-mail that comes from a group e-mail address.

When a user is added to the group owners (or elevated to group owner if user was a group member) – user does not get notification.

When a user was a group owner and now you are adding this user to the group members – user does not get notification.

All the actions are logged into Microsoft 365 audit log under your personal Id.

Script samples:

# This script is just a sample to demonstrate basic technique on getting, updating groups membership and deletion m365 groups with PowerShell and MS Graph
#
# please do not run this script as is, but update it based on your needs

# authentication with personal Id
#  app must have as minimum "Group.ReadWrite.All" Microsoft Graph API delegated permission
#  user must have SharePoint admin (or Teams admin) roles activated
Connect-MgGraph -ClientId $clientid -TenantId $tenantId 
Get-MgContext | Select-Object Scopes -ExpandProperty Scopes

# sample data
$groups = @()
$groups += [PSCustomObject]@{GroupId = '443d22ae-683a-4fe4-8875-7bd78227a026' }
$groups += [PSCustomObject]@{GroupId = 'e5805388-c18c-48c0-b42d-6223cf8f3d82' }

# Get Groups
foreach ($group in $groups) {
    Get-MgGroup -GroupId $group.GroupId
}

# add members to the group
$groupId = '443d22ae-683a-4fe4-8875-7bd78227a026'
$userId = 'df74e0d3-d78c-495b-b47a-549437d93cf7' # Adele
New-MgGroupMember -GroupId $groupId -DirectoryObjectId $userId

# add Owner to the group
$groupId = '443d22ae-683a-4fe4-8875-7bd78227a026'
$userId = 'eacd52fb-5ae0-45ec-9d17-5ded9a0b9756' # Megan
New-MgGroupOwner -GroupId $groupId -DirectoryObjectId $userId

# Delete group
$groupId = '443d22ae-683a-4fe4-8875-7bd78227a026'
Remove-MgGroup -GroupId $groupId

References

Sites.Selected API permissions for SharePoint access

Sites.Selected permissions are required for the non-interactive applications to get access to a specific SharePoint site using Microsoft Graph API and/or SharePoint API.
(Since Microsoft announced EOL of SharePoint App-only service principals, Sites.Selected is the only option going forward). Below are

Brief overview of Sites.Selected

Historically, we utilized so called SharePoint app-only service principals to get unattended (daemon/service) access to one specific site programmatically. Initially in on-prem, later in SPO. SharePoint app-only service principals use ACS-based authentication and allow calls to SharePoint (REST) API and usage of SharePoint CSOM.

Then Microsoft started developing Graph API. You’d need to register your app in Azure to get App Id and App secret to authenticate to Microsoft Graph API. You’d also configure specific API permissions for this app to get access to services you need. Unfortunately, for a long time there were no options to get access to only one specific site with Graph API. Available API permissions allowed access to entire SharePoint only.

Then, in 2021 Microsoft introduced Graph API “Sites.Selected” application permissions. Hooray! The problem was dev had to have two service principals – new Sites.Selected to call Graph API and classic SP-App-only to call SharePoint API. Later, in 2022 Microsoft implemented SharePoint “Sites.Selected” API permissions… More on this

Long story short, below are the detailed steps to configure Sites.Selected for you unattended app access to SharePoint site.

Steps to get and configure Sites.Selected permissions

1. Register an application in Azure (Entra Id) via Azure portal GUI, PowerShell script or helpdesk/servicedesk request. E.g. with GUI you’d login to portal.azure.com,
the search for “App registrations” and select “+ New registration”:

With PowerShell you’d do it with e.g. Register-PnPEntraIDApp cmdlet.

If you are not allowed to register an Entra Id app due to permissions restrictions in your company – connect with your IT/admins, as there must me some way to request an app.

Once you get an application registration – you are this app owner now – you should be able to navigate to your app registration and configure it (see Step 2 and below).

2. Update the app “API permissions” – so both – MS Graph API Sites.Selected and SharePoint Sites.Selected application API permissions are configured:

Request tenant admin consent for your API permissions. Finally your app registration “API permissions” should look like:

3. App Secret or Certificate
Under Certificates and secrets – generate client secret, copy secret value to safe location.

Or you can obtain trusted (or create a self-signed) certificate, and upload it to your app registration. Certificates are considered as more secure option then secrets.

4. At the Overview page – grab your app client id and tenant id :

At this moment, having tenant id, app (client) id and client secret (or certificate) – you should be able to authenticate against Microsoft 365 tenant with app-only authentication path.

But! Having just Sites.Selected API permissions configured for app does not mean your app has access to any SharePoint site. Access for the app to a specific site is provided by SharePoint team via Graph API calls. That leads us to the next step.

5. Application access to SharePoint site
You need to request this from your SharePoint service admin (or if you are an admin – DIY), but access needs to be provided for the specific app to the specific site with specified permissions (Read-Only or Read/Write or Manage or Full Control)
Here is the Graph API
Here is PowerShell PNP cmdlet

Interesting that MS Graph advertises 3 possible roles – read, write and owner, but PNP team says you can select from 4 roles – Read, Write, Manage or FullControl.

Obviously, Read role allows an app to read site content;
Write role is similar to “Contributor” user permissions – it allows CRUD operations against list items (library documents and metadata), but does not allow create/update/delete lists/libraries (for this – you’d need Manage role).

Use Sites.Selected permissions

Once your SharePoint tenant/service admin confirmed that access has been provided – you can use app client id and client secret (or certificate) to work with SharePoint from your code using Graph API. There are some good tutorials published:

Generally, this Sites.Selected permissions allows you to make calls that are documented under “Files” and “Sites and Lists” Graph API documentation. I.e. get site details, get site lists/libraries, create lists and libraries, CRUD operations against list items, download/upload library documents – all within the specific site. Sites.Selected permissions does not allow search operations, anything related to group or team etc.

If you have concerns if permissions for your app were provided correctly or not – you can validate your app access to the target SharePoint site with simple PowerShell scripts: here is the sample code

Note: Sites.Selected API permissions allows you call Microsoft Graph API with client Id and client secret. Calling SharePoint API with client secret is not supported. You have to use client id and certificate to call SharePoint API having app with Sites.Selected permissions.

Call SharePoint API with client Id and client secret is possible only if ACS-based permissions are provided for the app to the site, which is not recommended due to announced retirement (see below).

Secure your credentials

You do not want to hard-code your client secret as you do not want your credentials be leaked. So you need to secure your secrets in production. Solutions for secrets are included in cloud providers offerings, you can also use GitHub environment variables. If you are hosting your application in Azure – consider using key vault to keep your secrets. You can configure managed identity for your application and provide access to the key vault for you application managed id.

Govern Sites.Selected permissions

(For SharePoint admins).

Existing admins API/cmdlets allows yo to provide Sites.Selected permissions for specific app to specific site, and to get Sites.Selected permissions provided to the specific site. But there is no API/cmdlet for the specific app to get all sites (with permissions) this app has access to. Meantime as SharePoint admin if you keep providing permissions upon users/devs requests – after some time you have no idea what app has access to what site with which level of access, especially in large organizations.

Surely you can (and should) pull reports on all registerd apps with access to SharePoint, but…

There is a solution developed by Joe Rodgers (Microsoft). This solution use SharePoint list as an inventory/storage and Power Automate flows to pull data from Entra Id and SharePoint and provides kind of dashboard so you can review details of all app registrations in the tenant with at SharePoint Online permission. Cool!

Note: you would not provide Sites.Selected permissions just upon user/developer request. You’d always get an approval from target site owner. Target site owner must understand that application will have permanent unattended access to entire SharePoint site with permissions specified (read or write or manage or full control).

Sites.Selected permissions provisioning automation

(for SharePoint admins)

Generally, to provide an Application with Sites.Selected API permissions configured access to a specific site, SharePoint admin would run a set of PowerShell commands (or C# program or…) to ensure the client id exists, API permissions are configured and consented, to get app owners, target site owners, to get existing app permissions etc. Finally, admin would provide permissions and validate that permissions were provided correctly. It does not take long…

But in medium and large environments number of requests could be significant enough to start thinking of automation. I do have a separate article and video on Sites.Selected permissions provisioning automation.

Classic ACS permissions vs Sites.Selected permissions

Note: ACS-based permissions are deprecated:
Your SharePoint admin doesn’t allow site owners to create/update ACS principal ⋆ Vladilen Microsoft 365 engineer

Though Sites.Selected is our choice going forward, old classic ACS-based App-only permissions have some advantages (unique features) Sites.Selected does not have, e.g. ability to provide permissions not to entire site, but to specific list or subsite only. You can get more details checking this article for Comparison between Azure Apps and Entra Id Sites.Selected API permissions vs SharePoint app-only spn and ACS-based permissions.

Update:
Microsoft announced decommissioning of ACS permissions. So using ACS for any new development is not recommended.

It may be acceptable to grant ACS permissions to existing custom applications or third-party or Microsoft apps/web apps (e.g. Alteryx, Azure Data Factory) – applications that only support a client ID and secret and use the SharePoint API under the hood – but only to avoid disruption to business processes and keeping in mind that ACS will expire soon, so these applications must be replaced/updated before 2026.

Update: Microsoft implemented granular (permissions to list, item or file) alongside with Sites.Selected permissions. Original implementations of Sites.Selected allowed access to entire site collection only. With new ‘Lists.SelectedOperations.Selected’, ‘ListItems.SelectedOperations.Selected’ and ‘Files.SelectedOperations.Selected’ permissions it is possible to provide application permissions to list, library or list item or particular document (reference).

References