Tag Archives: PowerShell

Sites.Selected API permissions for SharePoint access

Sites.Selected permissions are required to get access to only a specific SharePoint sites 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. We used to get SharePoint app-only service principals via appregnew.aspx and provide permissions via appinv.aspx page. SharePoint app-only principals allow calls to SharePoint (REST) API and usage of SharePoint CSOM via ACS-based authentication.

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 (SharePoint, Exchange, Teams etc.). Unfortunately, for a long time there were no options to get access to only one specific site with Graph API. Available API permissions (e.g. Sites.Read.All) allowed access to all SharePoint sites in tenant.

Then, in 2021 Microsoft introduced Graph API “Sites.Selected” application permissions. Hooray! But the problem was devs 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 added SharePoint “Sites.Selected” API permissions – so both APIs are available for a single App Id (more App-Only and Sites.Selected history details…).

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,
then “Entra Id” -> “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 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 and you are this app owner – you should be able to navigate to your app registration in Azure Portal and configure it (see Step 2 and below).

2. Update the app “API permissions” – you’d need both – MS Graph API Sites.Selected and SharePoint Sites.Selected application API permissions configured:

Adding Sites.Selected API permissions via Azure Portal

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

How your app registration "API permissions" should look like for Sites.Selected - Graph and SharePoint APIs

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

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

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 should not be able to access any site.

Having just Sites.Selected API permissions configured for app does not mean your app has access to any SharePoint site automatically. Access for the app to a specific site is provided by SharePoint team via another Graph API call. 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)
  • Manage role allows create/update/delete lists/libraries
  • FullControl is full control

At this moment you should be able to access SharePoint sites. How? See below:

Use Sites.Selected permissions

Once your SharePoint tenant/service admin confirmed that access has been provided for you app to specific SharePoint site/sites – you can use app client id and client secret (or certificate) to work with SharePoint from your code using MS Graph API or SharePoint API. Exact technique depends on language/platform you use, but 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 are facing issues – first of all you’d try to isolate the issue – is it permissions to blame or something else. To ensure permissions for your app were provided correctly – you can validate your app access to the target SharePoint site with simple PowerShell scripts: here is the sample code

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

Calls to SharePoint API with client Id and client secret are possible only if ACS-based permissions are provided for the app to the site (with appinv.aspx), which is strictly not recommended due to announced App-Only (ACS) retirement – see updates in the end of the article.

Secure your credentials

You should not hard-code your 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 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. (Actually, if your app is running in Azure – it is possible to provide permissions for your app directly – via managed identity. This is considered as even more secure setup – no app registration and no key vault is needed as there is no credentials to save, but that’s a separate story: Connecting to Microsoft 365 and Graph API from the Azure Function App via Managed Identity).

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 what 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 think 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 ACS permissions (app-only principals) retirement in 2026. So using ACS for any new development is not recommended.

It may be acceptable to grant ACS permissions to existing legacy 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.

Here is How to prepare your tenant for Azure ACS retirement

Update: Microsoft is implementing 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

Connecting to Microsoft 365 SharePoint and Graph API from Azure Function App

Let say you need to run some code against Microsoft 365 on a scheduled basis. For this, you can use Azure Function App and timer-triggered Functions. From the code you’d call Microsoft Graph API and/or SharePoint API, so you’d need your Function somehow to get credentials on the fly from the KV and use it to call APIs. For this – you’d have a key vault where credentials will be be stored and retrieved by functions when needed. How does KV know if it’s ok to share secrets with the function app? Below I’ll share step-by-step how to create, configure and secure an Azure Function with system-assigned managed identity, and Azure Key Vault.

Update: you can make it even more secure – you can assign permissions directly to function, eliminating credentials and key vault. I created a KBA: Connecting to Microsoft 365 and Graph API from the Azure Function App via Managed Identity

Below is a “classic” approach with client id, client secret and a key vault.

First, we’ll create a resource group, e.g. “Azure-Func-Secured”.

Next, we’ll create a function app:

We’ll avoid the default “Flex Consumption” service plan (as it is Linux-only and does not support dependencies) and select “Consumption” a hosting option for now:

Runtime stack we’ll be using is PowerShell Core 7.4, but you can choose your own (options are – Python, .Net (C#), Node.js, Java). Let us leave other configuration settings by default (e.g. Enable Public access: On) for now, we’ll fix (secure) it later.

Ok, the function app has been created. Notice that app service plan, application insights and storage account, as well as some other services were created automatically under the same resource group:

Now we can create one or more functions under this function app, also we’d need to create a key vault and a registered app. Let us start with function and VS Code would be our environment of choice.

Let us start Visual Studio Code in a separate folder. Ensure you have “Azure Functions”, “Azure Resources” and PowerShell extensions by Microsoft. You’d sign-in to Azure:

and create a function project:

You’d choose “Timer triggered” function (as we need the function running on schedule), the function name would be MyScheduledFunct01 and we’d leave the default schedule as “0 */5 * * * *” – so our function will be triggered every 5 minutes.

Let us deploy the function right away and see if it works. For this, we can use “deploy” icon under “WORKSPACE” panel:

or “Deploy to function app” option from the function app context pop-up menu under the Azure “RESOURCES” panel:

After successful deployment give it some time, then check function invocations and ensure function is triggered an running:

Next, we’d update “requirements.psd1” to include Azure Key Vault and PnP PowerShell modules as it takes some time for the function app to pull in and install dependencies. Requirements.psd1:

# This file enables modules to be automatically managed by the Functions service.
# See https://aka.ms/functionsmanageddependency for additional information.
#
@{
    # For latest supported version, go to 'https://www.powershellgallery.com/packages/Az'. Uncomment the next line and replace the MAJOR_VERSION, e.g., 'Az' = '5.*'
    # 'Az' = 'MAJOR_VERSION.*'
    'Az.KeyVault' = '6.*'
    'PnP.PowerShell' = '2.*'
}

And we’d update function itself to monitor if dependencies are installed, than we’d deploy the function again so time would work for us. MyScheduledFunction/run.ps1:

# Input bindings are passed in via param block.
param($Timer)
$currentUTCtime = (Get-Date).ToUniversalTime()
if ($Timer.IsPastDue) {
    Write-Host "PowerShell timer is running late!"
}
Write-Host "PowerShell timer trigger function ran! TIME: $currentUTCtime"
#############################################################################################
Write-Host "Check modules installed:"
Import-Module PnP.PowerShell 
Import-Module Az.KeyVault
Get-Module PnP.PowerShell
Get-Module Az.KeyVault
Write-Host "Check command available:"
Get-Command -Name Connect-PnPOnline -Module PnP.PowerShell

At first, we might see warning like “The first managed dependency download is in progress, function execution will continue when it’s done. Depending on the content of requirements.psd1, this can take a few minutes. Subsequent function executions will not block and updates will be performed in the background.”. So we’d just wait.

After some time the function will be able to use required modules. Here is the invocation output example:

App Registration
To get unattended access to SharePoint (or Teams or Exchange etc.) as a service (daemon) application – we need credentials. It is done via “App Registration” under Entra Id (Azure AD) – here is the detailed step-by-step guide on registering apps in Azure.

Finally, we’d have a service principal with permissions we need:

We do not hard-code secrets, so let us create a key vault to keep secrets safe:

We’d leave all other configuration option by default, including “Azure role-based access control”, Allow access from: All Networks etc.

Here is the trick: even if I created this key vault and have an admin-level access, I’m not able to work with secrets values. So I need to provide additional access to myself. Being at the key vault, navigate to “Access control (IAM)” and add role assignment…

We’d select “Key Vault Secrets Officer”, next, select members… and select own name:

From this moment you should be able to CRUD secrets with values.

Now, let us generate a secret under App registration and copy secret value:

Then navigate to the Key Vault -> Object/Secrets -> Generate/Import – and save the secret value there:

Now you can get this secret… but the function cannot reach the secret… Here is the proof. Let us update the function code with:

Write-Host "Get secret from the key vault:"
$vaultName = "azure-func-secrets"
$secretName = "azure-func-secured-01"
$kvSecret = Get-AzKeyVaultSecret -VaultName $vaultName -Name $secretName -AsPlainText
Write-Host "Secret:" $kvSecret.Substring(0,3)

and check the function output. You’d see something like (which is not very descriptive):

So, how’d we allow key vault access for the function app? It’s as simple as that:
– first, we’d need some identity assigned to function app
– second, we’d provide access to the key vault to this identity

Managed Identity: assign an identity to the function.
For this, you’d go to function Settings/Identity, and under System Assigned, you’d switch status to On and Save settings.

Then, you’d go to your key vault, Access Control (IAM) and add role assignment. But this time, you’d select the role “Key Vault Secrets User” (more about roles), and “Assign access to” Managed Identity, and select members – select your function app identity (notice, identity is assigned to function app, so all functions under the app will be able to use the identity):

Now, we’d check the next function invocation detail, and voila:

You can see that azure function was able on the fly to pull secret from the key vault, so now we should be able to use these credentials to access SharePoint.

As you know, having client id and client secret would allow you to call MS Graph API. But calling SharePoint API would require authentication with a certificate. “PnP.PowerShell” module use both APIs under the hood, so we’d need a certificate to connect to tenant and work with SharePoint using “PnP.PowerShell” module. Please refer to this article on how to run Connect-PnPOnline with Certificate stored in the Key Vault.

Securing Azure Function

Out-of-the-box Azure Function App is created in a not very secure manner. Here is whan we’d need to do to secure Azure Functions:

Finally, it’d be more secure not to have a key vault, but provide permissions to the managed identity of the Azure Function App.

References:

SPO Site LastContentModifiedDate vs LastItemModifiedDate vs LastItemUserModifiedDate vs Graph LastModifiedDateTime

How do we know when the SharePoint site was last updated?

We have multiple “when the site was modified last time” properties – e.g. some we can retrieve with SharePoint CSOM:

  • Site LastContentModifiedDate
  • Web LastItemModifiedDate
  • Web LastItemUserModifiedDate

Also we can get

  • MS Graph site object with LastModifiedDateTime property
  • get usage reports via Microsoft Graph (activity reports), and
  • use “Last activity” field via Admin Center GUI

On the other hand – we can view and modify site in multiple ways – visit site home page, open and/or update document/list item, change site/library settings, configure site permissions, assign site sensitivity label, setup site property and so on.

Question: which site “last modified” or “last activity” properties reflect what events/actions?

This might be important if we think of retention policies, or any kind of clean-up processes… Let say, we are getting report on abandoned sites (inactive sites), but we are also assigning sites sensitivity labels, or we are updating site custom properties (e.g. for adaptive scopes), we have an ownerless groups policy working etc.

What if we assign site sensitivity label to an old inactive (5 years old) site – would it affect retention policy since site was updated this way?

Results

So i did some tests and based on detailed results below, it seems like

  • Web LastItemModifiedDate is triggered when user just visited site (but property LastItemUserModifiedDate is not triggered)
  • If a document or list Item updated by user or app – all properties are triggered
  • MS Graph site property LastModifiedDateTime, root web property LastItemModifiedDate and Site LastContentModifiedDate – same values
  • If site custom property is updated – it does not affect any site “last modified” property
  • The same for sensitivity label updated by app – it does not affect any site “last modified” property
  • The same for Microsoft ownerless groups policy – when user accept or decline group membership – no site “last modified” properties are changed (the same is true for Microsoft 365 group last modified date/time property).

Please refer to the table below

Detailed test results

Test results if the event triggers property update:

EventLast Content Modified DateLast Item Modified DateLast Item User Modified DateGraph Last Modified DateTimeGUI Last activity
Page viewed by userYesYesNoYes
Home Page viewed by user
Site Page viewed by user
Document or list item updated by userYesYesYesYes
Document or list item updated by appYesYesYesYes
Site config settings updated by user
Site config settings updated by app
Site custom property updated by appNoNoNoNo
Site Sensitivity label updated by user via SharePointYesNoNoNo
Site/Group Sensitivity label updated by user via Teams
Site/Group Sensitivity label updated by user via AzureNoNoNoNo
Site Sensitivity label updated by appNoNoNoNo
Site collection admin updated by userYesYesNoYes
Site collection admin updated by appYesYesNoYes
SharePoint group membership updated by userYesYesNoYes
Standalone Site connected to a group by userYesYesYesYes
Add Microsoft Teams to Site by UserYesYesYesYes
Update m365 group membership via M365 admin console by adminYesYesNoYes
Update m365 group membership via Azure by admin
Update m365 group membership via Teams by userNoNoNoYes
Update m365 group membership via App
Accept group ownership invitation sent by ownerless groups policyNoNoNoNo
Decline group ownership invitation sent by ownerless groups policyNoNoNoNo

PowerShell scripts for Microsoft 365 SharePoint

After many years working with SharePoint I wrote a lot of PowerShell scripts that help me support, troubleshoot, administer and secure SharePoint. So I’m sharing my scripts with you.

It’s here: https://github.com/VladilenK/Manage-m365-with-PowerShell

Get all SharePoint and Teams sites owners report with PowerShell

This PowerShell script pulls all tenant sites and all sites owners. The script require app authentication with Sites.FullControl.All and Directory.Read.All permissions.
PnP.PowerShell for PowerShell 7 is used.

The script generates two reports

  • Owners report: one user per line, include: Site Url, Title, Owner e-mail, name and type
  • Sites report: one site per line, include: Site Url, Title, list of owners e-mails

Here is the script:


$connAdmin = Connect-PnPOnline -ReturnConnection -Tenant $tenantId  -Url $adminUrl -ClientId $clientid -Thumbprint $certThumbprint
$allTenantSites = Get-PnPTenantSite -Connection $connAdmin | Sort-Object Url
$allTenantSites.count

$sitesReport = @()
$ownersReport = @()
foreach ($tenantSite in $allTenantSites) {
    Write-Host $tenantSite.Url
    $connSite = Connect-PnPOnline -ReturnConnection -Tenant $tenantId  -Url $tenantSite.Url -ClientId $clientid -Thumbprint $certThumbprint
    $site = Get-PnPSite -Connection $connSite -Includes RootWeb, GroupId, Owner
    $siteOwnerEmail = ''
    $siteOwnersReport = @()
    if ($site.GroupId.Guid -eq '00000000-0000-0000-0000-000000000000') {
        $siteAdmins = Get-PnPSiteCollectionAdmin -Connection $connSite | ? { $_.PrincipalType -eq 'User' }
        $ownerType = 'Site Collection Administrator'
        $isGroupSite = $false
    }
    else {
        $siteAdmins = Get-PnPAzureADGroupOwner -Connection $connAdmin -Identity $site.GroupId.Guid
        $ownerType = 'Group Owner'
        $isGroupSite = $true
    }
    foreach ($siteAdmin in $siteAdmins) {
        if (!$siteAdmin.UserPrincipalName) {
            Get-PnPProperty -Connection $connAdmin -ClientObject $siteAdmin -Property UserPrincipalName | Out-Null
        }
        $aadUser = Get-PnPAzureADUser -Connection $connAdmin -Identity $siteAdmin.UserPrincipalName
        if ($aadUser.AccountEnabled) {
            $siteOwnerEmail += $aadUser.Mail + '; '
        }
        $siteOwnersReport += [PSCustomObject]@{
            SiteUrl     = $site.Url
            SiteTitle   = $site.RootWeb.Title
            IsGroupSite = $isGroupSite
            OwnerEmail  = $aadUser.Mail
            OwnerName   = $aadUser.DisplayName
            OwnerType   = $ownerType
            Enabled     = $aadUser.AccountEnabled
        }
    }
    $ownersReport += $siteOwnersReport
    $sitesReport += [PSCustomObject]@{
        SiteUrl     = $site.Url
        SiteTitle   = $site.RootWeb.Title
        IsGroupSite = $isGroupSite
        OwnerEmail  = $siteOwnerEmail
    }
}

$ownersReport.count
$sitesReport.count

Source code: https://github.com/VladilenK/Manage-m365-with-PowerShell

Microsoft 365 SharePoint: prevent throttling with RateLimit headers

Bert Jansen (Microsoft) revealed some details on throttling when you access Microsoft 365 programmatically – via Microsoft Graph or CSOM and guided developers on how to regulate request traffic for optimized throughput using RateLimit headers (Here).

Demystifying SharePoint throttling

Throttling is necessary to ensure that no single user or application consumes too many resources compromising the stability of the entire system, which is used by many clients.

Throttling happens at

  • User (there are user request limits. Microsoft counts all requests linked to user
  • Application (Delegated or Application permissions)
    • Resource units per app per minute
    • Resource units per app per day
  • Farm – Spike protection

Very common reason for throttling – when an Application (Delegated or Application permissions) reaches “Resource units per app per minute” threshold.

Usually you catch HTTP errors 429 or 503, wait for some time (respect Retry-after header) and try again.

SharePoint provides various APIs. Different APIs have different costs depending on the complexity of the API, but Microsoft favor Graph API over SharePoint REST/CSOM. The cost of APIs is normalized by SharePoint and expressed by resource units. Application’s limits are also defined using resource units.

Quota depends on tenant size.

Resource unit limits for an application in a tenant (please refer to the Microsoft article)

Predefined costs for Microsoft Graph calls:

Assuming 2 resource units per request is a safe bet.

Links

Update Large Number of SharePoint Sites with PowerShell Parallel

WIP

Here I’m trying to figure out – how much PowerShell Parallel option is beneficial and how to avoid throttling…

Let us test, how long would it take to create a SharePoint site, if we use regular (sequential) loop or parallelism (I’m creation a sample set of 50 SharePoint Sites in a row):

Regular
(Sequential)
seconds per site
Parallel,
100 sites in batch
seconds per site
Parallel,
500 sites in batch
seconds per site
Regular (Sequential)3.0
Parallel,  ThrottleLimit = 21.600.91
Parallel,  ThrottleLimit = 50.69
Parallel,  ThrottleLimit = 100.2 – 0.3
Parallel,  ThrottleLimit = 200.17

Interesting, but I did not get even one (throttling or any other) error during creation 500 sites.

Get sites details

Now let us test, how long it takes to get sites details with Get-PnPTenantSite (I use a sample set of 500 sites):

Test typeRegular
(Sequential),
seconds per site
Parallel
sample = 100 sites,
seconds per site
Parallel
sample = 200 sites,
seconds per site
Parallel
sample = 500 sites,
seconds per site
Regular (Sequential)0.65
Parallel,  ThrottleLimit = 20.400.330.31
Parallel,  ThrottleLimit = 50.170.140.36 (errors)
Parallel,  ThrottleLimit = 100.11 (errors)0.11 (errors)0.34 (errors)
Parallel,  ThrottleLimit = 200.12 errors+0.07 errors+0.52 (errors)

(errors) means there were small number of errors during test… e.g.

SharePoint Site Template, Look Book and PnP Provisioning Engine

Aug 2024 Update: you cannot apply site template from Look Book. You must use PowerShell to apply a Look Book template to your site.

Here is the article: Applying PnP Templates to SharePoint Sites

Some templates can be applied by regular users (site admins) and some templates would require SharePoint tenant admin permissions. But now it’s only via PowerShell. You can get an idea how templates look like at

PnP provisioning engine is something that us used under the hood.

If you are interested in automation of provisioning templates – please let me know in comments below or via site feedback.

===============================

So the information below is obsolete and I will keep it just for the sake of history of SharePoint:

SharePoint Look Book

SharePoint Look Book – a site with a collection of modern SharePoint site templates. You can browse through dozens of good-looking templates… but how do you apply chosen template to your site?

Gotcha #1

There is a button “Add to your tenant>” and it says “You must be a tenant administrator to deploy this template.” Really? No… but
Actually, SharePoint Administrator role is required to apply template from lookbook.
So yes, tenant-level admin role but just SharePoint service admin role.
Site admin role is not enough…

Gotcha #2

Next, when you try to get template by clicking “Add to your tenant>” button, it actually offers you to create a new site. But it also says “…can use existing URL”. Really? No.
When you type existing site Url into the “Relative URL to be used for the site” field –
You can get “Can’t add this template. The provided site is already in use and the current template cannot be provisioned onto an already existing site. Please provide a different URL” message:

Or, if you managed to enter existing Url, you might get: “Unfortunately your site provisioning at least partially failed!”:


References: