Category Archives: Security

Securing Azure Function App for Full Compliance

WIP: Work In Progress

There are several assertion regarding how an Azure Function App should be configured according to security best practices. Think of each one as a requirement or policy Azure Function App should be compliant with. Azure Function App setup usually includes other Azure services – like Storage account, Key Vault, Networking etc. Some of the requirements contradict others. Blindly following remediation steps might break function app. Let us think of it holistically and propose an ideal Azure Function App configuration.

Requirements (policies) are:

  • function apps should only be accessible over https
  • function apps should have client certificates (incoming client certificates) enabled
  • storage account public access should be disallowed
  • versioning for Azure Storage should be enabled
  • storage account should use private link
  • storage accounts should restrict network access using virtual network rules
  • storage accounts should prevent shared key access
  • managed identity should be enabled on function apps
  • azure key vault should have purge protection enabled
  • azure key vaults should use private link
  • azure key vault should use RBAC permission model
  • firewall should be enabled on key vault

Step-by-step guide securing Azure Function App

Create an OotB Function App

Let us first build a working function app created “as is”, with all settings left by default, so we could apply best practices later. First, we’d need a new resource groups, then we’d create a function with “App Service” (aka Dedicated) hosting plan:

“App Service” (Dedicated) hosting plan is required as it is the only one that will allow us to satisfy all security best practices under Windows. Alternatives might be (tbc) Container or Flex Consumption – both Linux-based.

I will use PowerShell – so I have selected Code (not container) under Windows and PowerShell 7.4. runtime stack. All the other settings are by default – new auto-created storage, “Enable public access” – On, “Enable virtual network integration” – Off, new auto-created application insights etc.

You can use whatever runtime stack you are comfortable with – C#, Python etc. As this is what we need to ensure function is working before and after we applied all the security best practices. Actual steps to secure the function app are not depending on runtime stack chosen.

Create a simple Function and deploy it to Function App

We`d need a fully working function that access SharePoint. Ideally, I’d create a key vault and save credentials in the key vault. I’d provide access to the key vault for my function app so my code would pull credentials when needed and access Microsoft 365 (you can refer to “Connect to SharePoint via Graph API from Azure Function App” for the detailed step-by-step process), but since we are going to get rid of key vaults and use Managed Identities – I’ll save app secret in the environment variable for now.

Import modules

So, I created a simple OotB PowerShell timer-triggered function and deployed it to My function app. Then I updated requirements.psd1:

# requirements.psd1
@{
    'PnP.PowerShell' = '3.*'
}

and I updated function code itself (run.ps1)

# run.ps1
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 "##############################################"
Write-Host "Check modules installed:"
Import-Module PnP.PowerShell 
Get-Module PnP.PowerShell
Write-Host "Check command available:"
Get-Command -Name Connect-PnPOnline -Module PnP.PowerShell | select Version, Name, ModuleName
Write-Host "##############################################"

Check function invocations, ensure function is running correctly – PnP.PowerShell module is imported and ready to use.

Credentials to access SharePoint

As I mentioned, we will temporary be using environment variables to keep secrets – so we ensure the function does have access to SharePoint. For this we need

  1. register a new application under Entra Id and configure it to access SharePoint
    here is the guide: “Authentication to Microsoft Graph: Azure Registered Apps Certificates and Secrets
  2. put secret in an environment variable
    you can use vscode local.settings.json or update environment variables manually under Azure Function App

VSCode local.settings.json:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME_VERSION": "7.4",
    "FUNCTIONS_WORKER_RUNTIME": "powershell",
    "ORGNAME": "contoso",
    "TENANT_ID": "your-tenant-id",
    "CLIENT_ID": "your-client-id",
    "TMP_SECRET": "This is a secret",
    "ADMIN_URL": "https://admin.microsoft.com",
    "SITE_URL": "https://contoso.sharepoint.com"
  }
}

Finally you should have something like this under environment variables of you Function App:

environment variables under Azure Function App

Access SharePoint from function

Now let us update the function code with actual connections to SharePoint tenant and to Specific site.
I will try to connect to SharePoint via PowerShell module PnP.PowerShell and via plain web-requests calls to Microsoft Graph API.

Add the following (PnP part) to your function code run.ps1:

$orgName = $env:ORGNAME
$tenantId = $env:TENANT_ID
$clientID = $env:CLIENT_ID
$clientSc = $env:TMP_SECRET
$adminUrl = $env:ADMIN_URL
$siteUrl = $env:SITE_URL

#############################################################################################
Write-Host "Let us connect to SharePoint via PnP:"
$connectionToTenant = Connect-PnPOnline -Url $adminUrl -ClientId $clientID -ClientSecret $clientSc -ReturnConnection
Write-Host "Connected to admin site:" $connectionToTenant.Url
$tenantSite = Get-PnPTenantSite -Url $siteUrl -Connection $connectionToTenant
Write-Host "Tenant site title:" $tenantSite.Title
$connectionToSite = Connect-PnPOnline -Url $siteUrl -ClientId $clientID -ClientSecret $clientSc -ReturnConnection
Write-Host "Connected to regular site:" $connectionToSite.Url
$site = Get-PnPSite -Connection $connectionToSite
Write-Host "Site title:" $site.Title

The the direct calls to MS Graph API:


#############################################################################################
Write-Host "##############################################"
Write-Host "Let us get token to connect to Microsoft Graph:"
# Construct URI and body needed for authentication
$uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
$body = @{
    client_id     = $clientid
    scope         = "https://graph.microsoft.com/.default"
    client_secret = $clientSc
    grant_type    = "client_credentials" 
}
$tokenRequest = Invoke-WebRequest -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing
Write-Host "Token request status:" $tokenRequest.StatusDescription
$token = ($tokenRequest.Content | ConvertFrom-Json).access_token
$headers = @{Authorization = "Bearer $token" }

Write-Host "Let us connect to SharePoint via Microsoft Graph:"
$apiUrl = "https://graph.microsoft.com/v1.0/sites/$orgname.sharepoint.com"
$rootSite = Invoke-RestMethod -Headers $Headers -Uri $apiUrl -Method Get
Write-Host "Root site Url:" $rootSite.WebUrl
Write-Host "Root site display name:" $rootSite.displayName
#############################################################################################

Check function invocations, ensure function is working good getting SharePoint site via Microsoft Graph, but failing trying to get SharePoint site via PnP. The reason is PnP use SharePoint API under the hood, and calling SharePoint API require authentication with a certificate (but we have a secret here), though it’s OK to call Microsoft Graph API being authenticated with secret.

We will solve the SharePoint API secret/certificate problem below. Now it’s time to secure function according to the best practices.

Securing Azure Function App

TBC

SelectedOperations.Selected permissions in SharePoint

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.SelectedProvides application access to a specific list
ListItems.SelectedOperations.SelectedProvides application access to one or more list items, files, or folders
Files.SelectedOperations.SelectedProvides 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.

Admin App

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:

Target Site and Dev Setup

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"

Client Application

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…

References

Securing Storage Account in Azure Function

A storage account that is created as part of an Azure Function App out of the box has some configuration tradeoffs that can be considered vulnerabilities. Let’s look at what can be changed to improve the security of the storage account without affecting the functionality of the Azure Function App.

Public (anonymous) Access

Storage account blob anonymous (public ) access should be disallowed. Though having this Enabled does not allow anonymous access to blobs automatically, it is recommended to disable public access unless the scenario requires it. Storage account that support Azure Function App is not supposed to be shared anonymously.

Navigate to your Storage Account at Azure portal, Settings/Configuration, then
for “Allow Blob anonymous access” select “Disabled”:

MS: “When allow blob anonymous access is enabled, one is permitted to configure container ACLs to allow anonymous access to blobs within the storage account. When disabled, no anonymous access to blobs within the storage account is permitted, regardless of underlying ACL configurations. Learn more about allowing blob anonymous access“.

Storage account key access

This is also considered as vulnerability and it is recommended to disable Storage account key access.

NB: (Jan 2025) It is not possible to Set “Allow storage account key access” to Disabled if you are using Consumption or Elastic Premium plans on both Windows and Linux without breaking the function app. For other plans – it’s possible but requires some work.

Set “Allow storage account key access” to Disabled:

MS: “When Allow storage account key access is disabled, any requests to the account that are authorized with Shared Key, including shared access signatures (SAS), will be denied. Client applications that currently access the storage account using Shared Key will no longer work. Learn more about Allow storage account key access

The problem is it seems like the function app ootb is configured to use key access, so just disabling storage account key access breaks the function app.

For the function app to adopt this new setting, you’d need:

  • provide access for the function app to the storage account
  • reconfigure the function app for authorization via Microsoft Entra credentials

Here is the separate article “Function app to access Storage via Microsoft Entra credentials” with the detailed research and step-by-step guide, so here I just summirise it:

Function App Hosting PlanAllow Storage Account Key Access
Flex ConsumptionPossible to disable Storage Account Key Access
Consumption, PremiumNot Possible to disable Storage Account Key Access
DedicatedPossible to disable Storage Account Key Access
Containertbc

References

Azure Key Vault Purge Protection

Azure key vault is something where you can store keys, secrets, certificates that you’d need to access services (e.g. call Graph API).

What if somebody malicious get access to key vault (that is not what we want but we should consider this as possible risk)? Surely leaked secrets is a serious issue (separate topic), but imagine if that somebody also deletes key vault content – if so, we will simply lose this data, which will break the functionality of the solution and our existing systems will stop working.

So enabling purge protection on key vaults is a critical security measure to prevent permanent data loss. This feature enforces a mandatory retention period for soft-deleted key vault contents (e.g. secrets), making them immune to purging during the retention period.

In a nutshell, you’d just select “Enable purge protection” under the key vault Settings/Properties (notice, that once enabled, this option cannot be disabled):

For details, please refer to the following articles:

References

Azure Function app to access Storage via Microsoft Entra credentials

There is a known problem with Azure Function App security configuration. Ootb function app access to storage is configured using shared keys. This is considered as potential vulnerability. Disabling storage account key access breaks the app. This article tells how to reconfigure the app to use Microsoft Entra credentials vs shared keys.

When the function app is created – a Storage Account is created to support function app. By default, storage account has shared keys enabled and function app is configured for shared keys. So we’d need:

  1. Enable function app managed identity
  2. Provide access for the function app managed identity to the storage account
  3. Configure function app to use managed identity

Enabling function app managed identity

It’s done via Azure portal -> function app -> Settings -> Identity:

Providing access for the function app’s managed identity to the storage account

First, you’d navigate to the Azure portal -> Storage Account -> Access Control (IAM)
and Add new role assignment.

then you’d select “Storage Blob Data Owner” role, “Managed identity” your app identity

Configure function app to use managed identity

Responsible app settings are (you can find them under your app Settings -> Environment Variables -> “App settings”:

You can remove “AzureWebJobsStorage” and replace it with “AzureWebJobsStorage__accountName” as per here.

For the WEBSITE_CONTENTAZUREFILECONNECTIONSTRING, unfortunately, it says that this env variable is required for Consumption and Elastic Premium plan apps running on both Windows and Linux… Changing or removing this setting can cause your function app to not start… Azure Files doesn’t support using managed identity when accessing the file share…

Possible error messages

If you have a Function app with Consumption or Elastic Premium plans and completed steps above to disable function storage account key access – your function app will not work. There will be no new invocations. Personally, I was able to observe the following error messages:

“We were not able to load some functions in the list due to errors. Refresh the page to try again. See details” :

We were not able to load some functions in the list due to errors. Refresh the page to try again. See details


Function App with Dedicated Plan

If you have a function app created based on dedicated plan (App Service):

then under function “Environment variables” you’ll see:

e.g. there is a AzureWebJobsStorage (that can be updated) and no WEBSITE_CONTENTAZUREFILECONNECTIONSTRING (than is required).

So, for the function app created with dedicated plan (App Service) – if you followed steps above (provide a role to managed identity, created AzureWebJobsStorage__accountName, removed AzureWebJobsStorage and disabled key access – the function should work.

Function App with Flex Consumption plan

Flex Consumption plan is a new linux-based plan (GA announced Nov 2024), and it looks promising – it’s still consumption, but supports virtual networks and allows fast start (and some more nice features).

What I do not like is it does not support installing dependencies via requirements.ps1 – you have to go with custom modules here (it says: Failure Exception: Failed to install function app dependencies. Error: ‘Managed Dependencies is not supported in Linux Consumption on Legion. Please remove all module references from requirements.psd1 and include the function app dependencies with the function app content).

For our specific needs – disabling storage key access and using function identity to access it’s own storage – I found the following promising: “By default, deployments use the same storage account (AzureWebJobsStorage) and connection string value used by the Functions runtime to maintain your app. The connection string is stored in the DEPLOYMENT_STORAGE_CONNECTION_STRING application setting. However, you can instead designate a blob container in a separate storage account as the deployment source for your code. You can also change the authentication method used to access the container.”

Let us create a function with a hosting option “Flex Consumption plan” (all the other settings are by default) via Azure Portal:

and right away we can see that app is using storage keys by default via environment variables: AzureWebJobsStorage (we know how to deal with) and DEPLOYMENT_STORAGE_CONNECTION_STRING (no description found).

Let us try to create a function app different way (customized as per this). First, we’d create a storage account. When creating a function app – we’d select an existing storage. I did not find any options to select function managed identity and configure the function to use managed identity to access storage account during function app creation wizard.

Let us try to reconfigure the existing app

After this – a system identity was created and the role “Storage Blob Data Contributor” was assigned to this identity to the storage account. Environment variables did not go. Let us disable access keys under storage account – and… and function app stopped working.

Since environment variables are still here – let us blame “AzureWebJobsStorage” and let us do the trick with it – create a new “AzureWebJobsStorage__accountName”, put our storage account name as a value, remove “AzureWebJobsStorage” and restart the app… Drumroll, please! And hooray! The function has started working again!

“Container app environment” – to be tested

tbc…

References

Authorization to Microsoft Graph: Azure Registered Apps API permissions

Being authenticated to Microsoft 365 tenant means Microsoft 365 knows who is trying to get access. To actually be able read/write or manage resource, your app must be Authorized to this resource.

For details – pls refer to MS authorization and Microsoft Graph API permissions. But again, in short in our case that means we need to have an API permission configured for our azure registered app. There are two kinds of API permissions – delegated and application.

Delegated permissions are intended to allow currently authenticated user to have access to the resource. Effective user permissions in this app would be an intersection of user own permissions and app permissions. So if an app have “Sites.FullControl.All” SharePoint delegated API permissions – that does not mean that user will have full control over all sites.

Here is an example of delegated permissions:

Permissions above allow you to search through SharePoint content being authenticated with your personal credentials. In search results you will see only content you already have access to.

Application permissions are what it says – once permissions are configured – application will have access to the resources according to API permissions.

Generally, application permissions allow an app to have access to all resources of the same kind in tenant, e.g. to get one specific groups owners an app must have “GroupMember.Read.All” permission that allows an app to read all tenant groups and their members. There are some exceptions – e.g. for Teams Microsoft developed RSC that allows scoped app access. For SharePoint there is a similar option – “Sites.Selected” API permissions.

API permissions must have an Admin consent. Here is an example of application permissions:

Permissions above allow your app to search all SharePoint content.

References

Microsoft 365 admin center: Manage ownerless Microsoft 365 groups and teams

There is a new feature published at Microsoft roadmap site:

Microsoft 365 admin center: Manage ownerless Microsoft 365 groups and teams

Teams, Outlook groups, Team Sites etc. powered by Microsoft 365 Groups supports two roles: members and owners. Members can collaborate with others in the group through files, emails, messages etc. Owners manage the group membership and monitor content and conversations. When employees leave an organization or switch projects internally, it results in their existing user accounts getting deleted. If such employees were group owners, keeping track of their groups becomes critical to ensure accountability within the organization. We have introduced a new ownership governance policy to help automate the management of ownerless groups by requesting active members to become owners of the group. Admins can define who is eligible for these notifications and configure what notifications and how often these notifications are sent to active group members. Users, who are members of the ownerless groups can simply accept or decline request via the actionable email message.

  • Feature ID: 180749
  • Added to roadmap: 10/10/2023
  • Last modified: 10/10/2023
  • Product(s): Microsoft 365 Admin Center
  • Cloud instance(s): GCC
  • Platform(s): Web
  • Release phase(s): General Availability


But based on the feature description – all looks exactly as what we already have for years as “Microsoft 365 ownerless groups policy” which you can configure under Microsoft 365 Admin Center -> Settings -> Org settings -> Microsoft 365 groups

More on Microsoft 365 ownerless groups

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.

Sites.Selected permissions provisioning automation

Scenario

You administer Microsoft 365 SharePoint Online. Part of your daily activities is providing Microsoft Graph and SharePoint Sites.Selected API permissions to other users (developers).

In Aug/Sep 2023 Microsoft pushed an update that prevents site collection admins to create or update an Azure Access Control (ACS) principal (that was the way most of developers used to get Client Id and Client secret to access SharePoint site). So your users are probably getting something like Your SharePoint tenant admin doesn’t allow site collection admins to create or update an Azure Access Control (ACS) principal message attempting to create or update SharePoint App-only principal at AppRegNew.aspx or AppInv.aspx pages. Here are more details on the issue.

Microsoft and MVPs shared some technique how to provide Sites.Selected API permissions, but dealing with scripts manually, elevating individual permissions every time you need to run the script – it all takes time and not very efficient. More and more devs are reaching you on the app. So you want to automate this process.

Solution

Solution architecture

My way to automate it includes:

  • Using Azure App registration as service principal
    with API permissions configured
  • SharePoint list as a frontend
    here you can accept intake requests, organize approval workflow and display automation results
  • Azure Function App as a backend
    here will be your PowerShell script hosted that runs on scheduled basis and takes care of actual permissions provisioning

Solution details

High-level, getting application permissions to some specific SharePoint site is a two-step process:

  1. get application registration in Azure and properly configure it
  2. get permissions for this application to a specific SharePoint site

For the first step – check this and this articles. I’ll focus on the second step below.

You can provide Sites.Selected permissions for the app to a site with

I will be using second one one. Also PnP.PowerShell will be used to get access to SharePoint intake site and read/update requests from SharePoint list and so on.

Azure App Registration

I registered an admin Application in Azure – “SharePoint Automation App”, added Graph Sites.FullControl.All and SharePoint Sites.FullControl.All permissions, then added Microsoft Graph Directory.Read.All permissions and got tenant admin consent:

I generated a self-signed certificate and added it to the app:

This app will be used to call provide permissions, and to connect to the SharePoint front-end.

Users will register their applications in Azure, add Graph Sites.Selected and SharePoint Sites.Selected permissions, got tenant admin consent, then request permissions to the specific site by creating an intake request – new list item.

Front-End SharePoint Site

I created a SharePoint site for automation. This site will play a front-end role for users. I created a list “Sites.Selected” and updated list columns so I have the following fields:

  • Target Site Url
  • Application Id
  • Permissions (read/write)
  • Automation Output

In real-world (Prod) – You can (should) also implement approval workflow as you’d provide permissions for the application to the site only with this site owner approval. The PowerShell code behind should also validate site owner’s consent with app access to site. But for the sake of simplicity I’ll skip this in my demo.

Azure Function App

I created an Azure Function App with the following parameters:
– Runtime stack: PowerShell Core
– Version: 7.2.
– OS: Windows
– Hosting plan: Consumption

And then PowerShell timer-triggered function in Visual Studio Code.

Function requirements.psd1 (it takes a few hours for Azure to install modules; while modules are installing – you might see “[Warning] 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.”):

@{
    'Az' = '10.*'
    'PnP.PowerShell' = '2.*'
}

Azure Az module to access other Azure resources. PnP.PowerShell module will be used to access SharePoint.

I will keep my admin Azure registered app in a key vault, so need somehow to let the key vault know that this specific app can access this specific credentials. So I enabled system assigned managed Identity for the Function App:

MS: “This resource is registered with Azure Active Directory. The managed identity can be configured to allow access to other resources…”.
I’m going to use an object (principal) Id of this function to grant access to keyvault.

Azure key vault

Surely we do not hard-code app secrets. So we need a key vault o store app credentials.

I created a key vault under the same resource group in Azure and named it “SharePointAutomationDemo”. Then I added a roles assignment – “Key Vault Secret User” and “Key vault Reader” to the Function App via it’s managed identity:

I also assigned “Key Vault Administrator” role to the user (developer) who will add certificates/secrets to this key vault and develop Azure function code.

With the new ‘Lists.SelectedOperations.Selected’, ‘ListItems.SelectedOperations.Selected’ and ‘Files.SelectedOperations.Selected’ permissions it is new possible to provide application permissions at a specific list, library or list item levels or at a particular document level, so automation solution would be a little more complicated.

Code repository

https://github.com/VladilenK/Sites.Selected-Automation

Videos

Part 1: Getting Azure App Registration with Sites.Selected API Permissions

Part 2: SharePoint and Microsoft Graph API Sites.Selected permissions provisioning automation

Microsoft 365 ownerless group policy to send more than 10,000 notifications

It is known that a single Microsoft Exchange account is not sending more than 10k emails per day.

It is also know that once activated – Microsoft 365 groups ownerless policy will be sending notifications for all groups in scope to specified number of group members within 24 hours.

The question is: what if there are more than 10,000 notifications to send (e.g. 4,000 ownerless groups and the policy is configured to send notification to 3 members per group – that gives us 12,000 notifications to send)? Would the policy send 10k notifications and the rest 2k notifications the next day?

I’m conducting an experiment. I created 10k groups in my lab tenant with one owner and 3 random members. Then I configured a policy that is sending notification to a 3 most active members (in this case – random members). And then I made all these groups ownerless by deleting the single owner Id from Azure AD (Microsoft Entra).

Here is what I got from users perspective:

useruser groups
number
got messages
day 1
got messages
day 2
got messages
total
1 Roger50121374
2 Dick50391349
3 Bob51083412
4 Bapu49081376
5 Stas49961437
6 David49591377
total10325

Here is what audit log says:

Events “OwnerlessGroupNotified” day 1: 4949
Events “OwnerlessGroupNotified” day 2: 95
Events “OwnerlessGroupNotified” total: 5044
Each event details says 3 members were notified.

It seems like groups are selected by policy in random order.

Massive E-mails sending was started 43 minutes after midnight UTC

“OwnerlessGroupNotified” were logged at the rate of
1925 events during 1-st hour,
2029 events during 2-nd hour,
785 events during 3-rd hour,
176 events during 4-th hour,
26 events during 5-24 th hour,
95 events during next 25-48 hours
so max rate was one event every 3 seconds in the beginning (or 1 e-mail per second) …

TBC…