Tag Archives: 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.

Token – SharePoint API compatibility matrix

If I get token with (Graph, MSAL, PnP) and use this token for (Graph API, SharePoint CSOM API, SharePoint REST API) matrix.

An App used in this tests has Sites.FullControl.All MS Graph API and SharePoint API permissions, as well as FullControl ACS based permissions to SharePoint (AppInv.aspx).

Token/APIMS Graph
/v1.0/sites
SharePoint CSOM
PnP.PowerShell
Get-PnPSite
Get-PnPTenantSite
SharePoint REST API
PnP.PowerShell
Invoke-PnPSPRestMethod
Invoke-RestMethod
MS Graph
/oauth2/v2.0/token
secret
OK(401) UnauthorizedAudienceUriValidationFailedException
MSAL.PS
Get-MsalToken
with secret
OK(401) UnauthorizedAudienceUriValidationFailedException
MSAL.PS
Get-MsalToken
with certificate
OK(401) UnauthorizedAudienceUriValidationFailedException
PnP.PowerShell
Get-PnPAccessToken
with Certificate
OKOK
OK
OK
AudienceUriValidationFailedException
PnP.PowerShell
Get-PnPGraphAccessToken
with Certificate
OKOK
OK
OK
AudienceUriValidationFailedException
PnP.PowerShell
Get-PnPAppAuthAccessToken
with Certificate or secret
InvalidAuthenticationTokenOK
OK
OK
OK
PnP.PowerShell
Request-PnPAccessToken
with Certificate
InvalidAuthenticationTokenOK
OK
OK
AudienceUriValidationFailedException
PnP.PowerShell
Request-PnPAccessToken
with Secret
InvalidAuthenticationTokenOK
OK
OK
OK
AudienceUriValidationFailedException = Exception of type ‘Microsoft.IdentityModel.Tokens.AudienceUriValidationFailedException’ was thrown

Testing Sites.Selected SharePoint and MS Graph API

Sites.Selected MS Graph API permissions were introduced by Microsoft in March 2021. One year later, in 2022 they added SharePoint Sites.Selected API permissions.

Azure registered app with SharePoint and MS Graph API Sites.Selected permissions

Why is this so important? Because MS Graph API for SharePoint is still limited and cannot cover all possible needs. I’d estimate: 90% of applications use SharePoint CSOM, so developers have to use AppInv.aspx to provide permissions for their applications to SharePoint API.

But from this moment – having SharePoint API permissions in MS Graph – in theory – we can fully rely on permissions provided in Azure and – in theory – this should allow us disable SharePoint-Apps only principal:

Set-SPOTenant -DisableCustomAppAuthentication $true

My math professor taught me: “before trying to find a solution – ensure the solution exists.” So let us test:

Are we really able to work with a specific SharePoint site using MS Graph and SharePoint API Sites.Selected permissions provided via Microsoft Azure?

What will happen with our new/legacy applications if we disable SharePoint app-only SPNs (DisableCustomAppAuthentication)?

I’m getting controversial test results… maybe PnP.PowerShell 1.10 is not fully support SharePoint Sites.Selected API.

Tech Wizard (Sukhija Vikas) on March 20, 2022 in the article “SharePoint and Graph API APP only permissions for Selected Sites” suggests using pre-release (AllowPrerelease).

So please ignore the following for a while.

Meantime I’ll test providing SharePoint Sites.Selected API permissions via Graph API call.

(wip) Test set #1: Certificate vs Secret

DisableCustomAppAuthentication: $false (SP-app-only spns are enabled).
All applications have “write” access provided to a specific site only.
Connecting with Connect-PnPOnline and then test access with Get-PnPSite

App / Get-PnPSiteSecretCertificate
ACS based (Azure+AppInv)OKThe remote server returned an error: (401) Unauthorized.
MS Graph API Sites.SelectedThe remote server returned an error: (403) Forbidden.The remote server returned an error: (401) Unauthorized.
SharePoint API Sites.SelectedOKOK
MS Graph API + SharePoint API Sites.SelectedAccess is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))OK
App with no permissionsThe remote server returned an error: (403) ForbiddenThe remote server returned an error: (401) Unauthorized

(wip) Test set #2: Sites.Selected SharePoint vs MS Graph (secret)

  • DisableCustomAppAuthentication = $false
    (SP-app-only spns are enabled).
  • All applications have “write” access provided to a specific site only.
  • Using Client Secret (not a certificate)
  • Using PnP.PowerShell
Action/ViaSharePoint + MS Graph
Sites.Selected
“secret”
SharePoint
Sites.Selected
“secret”
MS Graph
Sites.Selected
“secret”
Connect-PnPOnlineWARNING: Connecting with Client Secret uses legacy authentication and provides limited functionality. We can for instance not execute requests towards the Microsoft Graph, which limits cmdlets related to Microsoft Teams, Microsoft Planner, Microsoft Flow and Microsoft 365 Groups.WARNING: Connecting with Client Secret uses legacy authentication and provides limited functionality. We can for instance not execute requests towards the Microsoft Graph, which limits cmdlets related to Microsoft Teams, Microsoft Planner, Microsoft Flow and Microsoft 365 Groups.WARNING: Connecting with Client Secret uses legacy authentication and provides limited functionality. We can for instance not execute requests towards the Microsoft Graph, which limits cmdlets related to Microsoft Teams, Microsoft Planner, Microsoft Flow and Microsoft 365 Groups.
Get-PnPSiteOKOKThe remote server returned an error: (403) Forbidden.
Get-PnPListOKOK
Get-PnPListItemOKOK
Set-PnPSiteAttempted to perform an unauthorized operation.
Set-PnPListAttempted to perform an unauthorized operation.
Set-PnPListItemOKOK
New-PnPListAccess is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))
Add-PnPListItemOK

(wip) Test set #3: Read vs Write vs FullControl

DisableCustomAppAuthentication = $false
(SP-app-only spns are enabled).
All applications have Sites.Selected SharePoint and MS Graph API permissions.
Using Client Secret (not a certificate)
Using PnP.PowerShell

ReadWriteFullControl
Connect-PnPOnlineWARNING: Connecting with Client Secret uses legacy authentication and provides limited functionality. We can for instance not execute requests towards the Microsoft Graph, which limits cmdlets related to Microsoft Teams, Microsoft Planner, Microsoft Flow and Microsoft 365 Groups.WARNING: Connecting with Client Secret uses legacy authentication and provides limited functionality. We can for instance not execute requests towards the Microsoft Graph, which limits cmdlets related to Microsoft Teams, Microsoft Planner, Microsoft Flow and Microsoft 365 Groups.WARNING: Connecting with Client Secret uses legacy authentication and provides limited functionality. We can for instance not execute requests towards the Microsoft Graph, which limits cmdlets related to Microsoft Teams, Microsoft Planner, Microsoft Flow and Microsoft 365 Groups.
Get-PnPSiteAccess is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))
Get-PnPListAccess is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))
Get-PnPListItem
Set-PnPSite
Set-PnPList
Set-PnPListItem
New-PnPList
Add-PnPListItem

(wip) Test set #5: Certificate vs Secret

C#, SharePoint CSOM, PnP.Framework

Findings

PnP.PowerShell Get-, Grant-, Set- and Revoke-PnPAzureADAppSitePermission cmdlets require Azure App with MS Graph Sites.FullControl.All app permissions (otherwise it says “Access denied”) and authentication via certificate (otherwise it says “This cmdlet does not work with a ACS based connection towards SharePoint.”)

The same actions – managing permissions for the client app to the specific site collections – could be done via Microsoft Graph Sites Permissions API using just secret-based authentication.

If an azure app does not have Sites.Selected API permissions configured – “Grant-PnPAzureADAppSitePermission” works as expected – no error messages – the output is normal – as if Sites.Selected API permissions were configured in the app. The same for Get-, -Set and Revoke-. Permissions provided for the app to the site are not effective though: Connect-PnPOnline works well, but all other commands – starting from Get-PnPSite – returns “The remote server returned an error: (403) Forbidden.”

If an app have no permissions to SharePoint – “Connect-PnPOnline” works ok, but “Get-PnPSite” return an error: “The remote server returned an error: (403) Forbidden.”

Set-PnPAzureADAppSitePermission gives an error message “code”:”generalException”,”message”:”General exception while processing”
if the site is not specified.

AppInv is not working?

Error: Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))

References

Testing environment

  • Microsoft 365 E5 Dev environment
  • PowerShell 7.2.2
  • PnP.PowerShell 1.10
  • “write” permissions to the specific sites for client apps were assigned via PnP.PowerShell

Providing Permissions to a Site for Sites.Selected App

How to provide permissions for an Azure registered application with MS Graph SharePoint Sites.Selected API permissions to a specific site via calling Microsoft Graph API from PowerShell.

We need an “admin” application – Azure registered application with with Sites.FullControl.All MS Graph API permissions. This method can use secret, so we need Client Id and Client Secret for this “admin” app.

We also need a Client Id and Application Display Name for an Azure application with Sites.Selected MS Graph and/or SharePoint API permissions provided.

And we need our “target” site Url.

With PowerShell scripts you can:

  1. Get Microsoft Graph Access Token with an “admin” app
  2. Get client (target) site Id
  3. Get current app permissions provided to client site
  4. Add read or write permissions for the client app to the client site
  5. Revoke one specific permission from site
  6. Revoke all app permissions provided to site

– please refer to the GitHub Repo Sites.Selected

References

Connecting to SharePoint Online programmatically: Secret vs Certificate

Update: Sites.Selected API MS Graph permissions was introduced by Microsoft in 2021. It was a good move towards site-level development, but still developers were limited with only what MS Graph API provides for SharePoint dev.
So devs had to use AppInv.aspx at site level to provide ACS permissions to their apps to be able to use SharePoint CSOM and REST APIs.
Recently Microsoft introduced Sites.Selected SharePoint API permissions for registered Azure Apps! So now devs should be fully happy without ACS-based permissions.

Scenario

You have an application that needs access to Microsoft 365 SharePoint Online site/list/documents. Application is running without interaction with users – e.g. unattended, as daemon job.

There are two options you can authenticate to Microsoft 365 – with the secret or with the certificate. Authenticating with certificate is considered more secure.

Questions

  • What happens if SharePoint-Apps only principal is disabled
    (i.e. ‘set-spotenant -DisableCustomAppAuthentication $true’ )?
  • Why I’m getting 401 error when authenticating to SPO?
  • Why I’m getting 403 error when authenticating to SPO with secret?
  • What permissions to I need to work with SPO?

Findings

Note: we will use PowerShell 7.2 and PnP.PowerShell 1.9 to illustrate it.

Disabled SharePoint-Apps only principal

If SharePoint-Apps only principal is disabled in your tenant
(i.e. ‘Get-PnPTenant | select DisableCustomAppAuthentication’ returns $true ), then the only way you work with SPO from code is:

  • an App registered in Azure
  • API permissions provided via Azure (MS Graph, SharePoint)
  • Certificate is used

In all other cases (even your Connect-PnPOnline command complete successfully) – you will be getting error 401 (unauthorized) when trying Get-PnPTenant or Get-PnPTenantSite or Get-PnPSite

Enabled SharePoint-Apps only principal

If SharePoint-Apps only principals are enabled in your tenant
(i.e. ‘Get-PnPTenant | select DisableCustomAppAuthentication’ returns $false ), then you have three options to work with SPO from code:

  • Azure App with a secret (Client Id + Client Secret) and permissions to SharePoint provided via SharePoint (AppInv.aspx) to access SharePoint REST API
  • Azure App with a certificate (Client Id + Certificate) and permissions provided via Azure to access SharePoint REST API
  • Azure App with a certificate or secret (Client Id + Secret or Certificate) and permissions provided via Azure to access SharePoint via Microsoft Graph API

Get list of new m365 SharePoint sites or teams with PowerShell

There are some scenarios you need to quickly get only newly created SharePoint sites, but traditional methods do not work as there is no “filter” options and it’s too long to iterate through all sites. Here is how to us Microsoft Graph API to get it.

Scenario

Let say you administer Teams, OneDrive and SharePoint Online in a Microsoft 365 tenant. You have a pretty big environment – ~10k or more sites and you want to quickly find just new SharePoint sites or teams (e.g. sites created recently – during last hour/day/week/month). This might be required for ad-hoc reports and for automation scenarios – like applying required configurations or assign some property value to all newly created sites.

With GUI it’s done easily: SharePoint Admin Center -> Active Sites -> sort based on “Date Created” – done.

With PowerShell – not so simple.
“Get-PnPTenantSite” cmdlet returns a site object but the object does not have “Created” field. It’s a web property. But to get a web object – you have to connect separately to each site and get root web object to check when the web was created. For small environments it is possible, for large environments it can take days… And still not nice.
“Get-PnPTenantSite” with “-Filter” option would help, but “…Currently, you can filter by these properties: Owner, Template, LockState, Url.”

Get-SPOSite – similar experience.

Teams + Exchange modules can help a little:

Get-Team | select GroupId | % { Get-UnifiedGroup $_.GroupId | select DisplayName,WhenCreated } | sort WhenCreated

but… 1) it’ll give you group-based sites only 2) it is not easy to automate 3) this might take long for large environments. I know much better solution:

Solution

Microsoft Graph API helps. It returns result in seconds and you can sort or filter results based on created date . Below are two methods: Option 1 is based on Search and filtering and Option 2 is based on Sites Search and sorting. So there are some pros and cons for each method.

Option #1: Microsoft Graph Search API.

Entry point: https://graph.microsoft.com/v1.0/search/query

Microsoft Graph Search API allows KQL in queries. So we can form a query with something like “created>=1/1/2021” and use entity type = ‘[“site”]’. Search should return only sites created after Jan 01, 2021.

Check PowerShell script sample here: Get-NewSites.ps1
https://github.com/VladilenK/PowerShell/blob/main/reports/SharePoint/Get-NewSites.ps1

If you are getting more than 500 results – think of paging.

Option #2: Microsoft Graph Sites API

Entry point: https://graph.microsoft.com/v1.0/sites

This option is also based on Microsoft Graph API, but sites entry point, which allows search too and sort results by property “createdDateTime”. So we will just search for everything and select how many results we need based on createdDateTime property.

Check PowerShell script sample here: Get-NewSites.ps1
https://github.com/VladilenK/PowerShell/blob/main/reports/SharePoint/Get-NewSites.ps1

References

Video tutorial:

Retrieve SharePoint Online system page html content programmatically (PowerShell)

How can I get HTML content of a SharePoint online page from code, e.g. PowerShell?

Invoke-WebRequest returns “Sign in to your account” page, not a real page, even with -Token option.

Thanks to Denis Molodtsov, the solution is found. It turns out the “Invoke-PnPSPRestMethod” PnP cmdlet works not only against /api endpoints, but also against site pages and system pages.

But (as per my experience) it works only with PnP.PowerShell and with -UseWebLogin authentication option and with -raw parameter.

Connect-PnPOnline -url $siteUrl -UseWebLogin
Invoke-PnPSPRestMethod -url /_layouts/15/viewlsts.aspx -Raw

Other combination of authentication options ( -interactive, -clientId, -Token, -SPOManagementShell, -PnPManagementShell ) – worked well, but only for /_api endpoints, and gave me “401 UNAUTHORIZED” against system/site pages.
Unattended authentication (with clientId, clientSecret and certificate) – same.

Legacy PnP module SharePointPnPPowerShellOnline did not work at all: “EXCEPTION,PnP.PowerShell.Commands.Admin.InvokeSPRestMethod”.

I tested it with
– SharePointPnPPowerShellOnline v 3.29.2101.0 (under Windows PowerShell 5.1) and
– PnP.PowerShell 1.8.0. (both Windows PowerShell 5.1 and .net core PowerShell 7.1.5)