Connecting to Microsoft 365 and Graph API from the Azure Function App via Managed Identity

There is a well-known and well-documented way of connecting to Microsoft 365 SharePoint and Graph API from Azure Function App via keeping credentials (Client id and client secret) in the Azure Key Vault. In this article I explained how to configure Azure Key Vault so Azure Function can get credential and use them to access Microsoft 365 SharePoint and call Graph API. For this to work we need an App registration with permissions provided. But what if we assign permissions directly to the Function App?
As per Microsoft, managed identities enable Azure resources to authenticate to cloud services (e.g. Azure Key Vault) without storing credentials in code. Is it possible to use function managed identity to access Microsoft 365 SharePoint via PnP or Graph API? Follow me.

I assume we already have an Entra Id, a Microsoft 365 subscription in it and an Azure subscription in the same tenant.

Create a User Assigned Managed Identity

There are two types of managed identities in Azure – System assigned and user assigned.
If you go to Azure Function app -> Settings -> Identity – you’ll see these two options:

  • System assigned Managed Identity will be created automatically if you select Status:On and Save. Having System-assigned Managed Identity you can provide permissions to Azure resources (e.g. storage, key vault) to this specific instance of function app. If you create another function app that’d need the same access – you’d need to enable system assigned Managed Identity again, for this new instance and again provide permissions.
  • User assigned managed identity is created as standalone Azure resource, and will have it’s own lifecycle. A single Azure resource (e.g. Function App) can utilize multiple user assigned managed identities. Similarly, a single user assigned managed identity can be shared across multiple resources. So if you deploy another Function App that need the same permissions as your existing function app – you’d just assign the same managed identity to this new function app.

So before we assign a user-assigned managed identity to a resource, we need to create a user-assigned managed identity:

For the Name of your User Assigned Managed Identity consider something that would uniquely identify you (your team) and your project/app at tenant level, e.g. “m365-Enterprise-SharePoint-Engineering-Managed-Identity-Demo”:

Create and Configure a Function App

Create your function app as you usually do that. (If you want to kill two birds with one stone – create a function app with “App Service” – aka Dedicated plan – you’ll see how to secure your function app storage account access with Managed Identity).

I use PowerShell Core as runtime stack and Windows.

Once Function App is created – we need to create a function. I’ll do it via Azure Portal for simplicity and I’ll select timer-triggered function:

Ensure that this function works ootb correctly by triggering test run:

Then we’d need to assign a managed identity earlier created to this function app.
Navigate to Function App -> Settings -> Identity, select “User Assigned” and managed identity:

Disable Az

Navigate to Function App -> Functions -> App Files. Select “profile.ps1”.
Remove or comment out part that use Az module cmdlets:

Update function dependencies

Since I use PowerShell and PnP for this demo, I need PnP module loaded.
Navigate to Function App -> Functions -> App Files. Select “requirements.psd1”. Update your code by adding ‘PnP.PowerShell’ = ‘2.12.0’ to the required modules. Do not enable Az module:

It takes time for the function app to download and install PnP module so you can use it in functions.

Update function code

Add the following code to function:

# My custom code
Write-Host "My custom code started"
$siteUrl = "https://contoso.sharepoint.com/sites/Test101/"
$adminUrl = "https://contoso-admin.sharepoint.com"
$UserAssignedManagedIdentityObjectId = "b0bfe72c-73a9-4072-a78b-391e9670f4b9"

Write-Host "Connecting as admin..."
$connectionAdmin = Connect-PnPOnline -Url $adminUrl -ManagedIdentity -UserAssignedManagedIdentityObjectId $UserAssignedManagedIdentityObjectId -ReturnConnection -ValidateConnection
Write-Host "ConnectionAdmin:" $connectionAdmin.Url
Write-Host "Getting tenant site as admin..."
$site = Get-PnPTenantSite -Identity $siteUrl -connection $connectionAdmin
Write-Host "Got site:" $site.Url
Write-Host "Getting admin site..."
$site = Get-PnPSite -connection $connectionAdmin
Write-Host "Got site:" $site.Url

Now you can Test/run the function or wait 5 minutes, then check what is in logs. You should see, that

  1. connection ran successfully, but
  2. getting site failed with “ERROR: The remote server returned an error: (401) Unauthorized.”

And that is ok, as

  1. With Connect-PnPOnline we are authenticating. And since managed id exist – we were recognized
  2. Our managed Id does not have any permissions yet, so any request will fail

Now it’s time to provide actual permissions for the managed identity to the site.

Grant permissions for the managed identity to access SharePoint via Graph API

Here is the most interesting part – somehow we need to provide our user assigned managed identity with permissions to access SharePoint (or any other Microsoft 365 service) via Microsoft Graph API and/or SharePoint API. We already know how to grant permissions to an App Registration in Azure – there is a GUI for that. But with respect to managed identity – there is no GUI. It’s done via Microsoft Graph API or PowerShell. And we need admin permissions to assign roles to a managed identity.

Who can grant roles to a managed identities

It says a Global Admin permissions required to provide roles to a managed identity.

As usual, there are two options: delegated permissions and application permissions (here is where differences explained). In both cases you’d need an App Registration with the following API permissions assigned and consented :

  • Application.Read.All (or higher)
  • AppRoleAssignment.ReadWrite.All

If you have an app with delegated permissions – you’d need a Global admin role to be activated. Or you need an app with application permissions configured as below:

If you are getting something like this:

That means you configured an app incorrectly.

Assigning permissions with Microsoft Graph API:

tbp…

Assigning permissions with PnP PowerShell

Here is the code:

$Id = "..." # User Assigned Managed Identity Object Id = Principal Id
Get-PnPAzureADServicePrincipalAssignedAppRole -Principal $Id 
$role = "Sites.FullControl.All"
Add-PnPAzureADServicePrincipalAppRole -Principal $Id -AppRole $role -BuiltInType MicrosoftGraph
Add-PnPAzureADServicePrincipalAppRole -Principal $Id -AppRole $role -BuiltInType SharePointOnline

Issues found

What I found is that connection to a specific site does not work, .i.e. the following code:

$siteUrl = "https://jvkdev.sharepoint.com/sites/Test101/"
$UserAssignedManagedIdentityObjectId = "b0bfe72c-73a9-4072-a78b-391e9670f4b9"
Write-Host "Connecting to a specific site..."
$connectionSite = Connect-PnPOnline -Url $siteUrl -ManagedIdentity -UserAssignedManagedIdentityObjectId $UserAssignedManagedIdentityObjectId -ReturnConnection
Write-Host "ConnectionSite:" $connectionSite.Url
Write-Host "Getting site..."
$site = Get-PnPSite -connection $connectionSite
Write-Host "Got site:" $site.Url

returns [Error] ERROR: The remote server returned an error: (401) Unauthorized.

Update: it’s probably just a matter of time… After some hours the same code started working well.
Though here Microsoft says “To ensure that changes to permissions for managed identities take effect quickly, we recommend that you group Azure resources using a user-assigned managed identity with permissions applied directly to the identity”

Sites.Selected

Would everything above work if we need to provide access for the function app user assigned managed identity to a specific SharePoint site via Sites.Selected?

It works! I used a separate user-assigned managed identity, provided it with Sites.Selected API permissions, provided access for the managed identity to a specific site and it worked!

PnP.PowerShell version

Is there a difference in behavior of PnP.PowerShell v2 and v3? Let us see…

As for now, it works with both versions – PnP.PowerShell 2.12.0 and PnP.PowerShell 3.1.0

References

SharePoint Inactive Site Policies

SharePoint Advanced Management includes Inactive Site Policies under Site lifecycle management. Effective content lifecycle management is a key pillar of SharePoint governance. It plays a vital role in optimizing storage, preserving data integrity, and ensuring regulatory compliance. By systematically removing inactive or outdated sites, it also enhances security. Additionally, it supports successful Copilot implementation by ensuring that the information accessed is both accurate and current. So, how exactly this Inactive site policy works and what is the difference between Entra Id groups expiration policy and SharePoint Inactive Site Policy.

Inactive site policy vs groups expiration policy

The Groups Expiration Policy has been a feature of Azure AD (Entra ID) for quite some time. It is included at no additional cost. This policy automatically notifies group owners about upcoming expirations and provides options to renew or delete the group. Since all self-created Teams teams and Viva Engage communities are backed by SharePoint sites and managed through Microsoft 365 Groups, this policy also plays a significant role in SharePoint governance ensuring that information stored in SharePoint remains current and properly maintained. I have an article Microsoft 365 group expiration policy deep dive.

Inactive Site Policy is a feature of SharePoint Advanced Management (SAM), which is an add-on and require premium SharePoint license. It also Identifies inactive sites, Sends notifications to site owners and can automatically archive or make sites read-only. Sound like very similar to to groups expiration policy.

Key differences

Inactive site policy user experience

Here is how the email notification looks like:

Note that

The email subject includes “Action required” and site title (name).
It always says “… has been inactive for more than a month” even if the policy configured for “6 months”.
It shows SharePoint logo, which might mislead “teams-oriented” users.
Site title is not clickable, so site admin/owner cannot just click site link but have to navigate to site manually.
When user clicks button “Certify site” – a message “The action completed successfully” pops up at the bottom of the email for a few seconds and then disappears. The email itself does not change, so when a user opens the same email again – there is no visual evidences the action was taken.

At the bottom of the email Microsoft mentions tenant name.

The email template is the same for all kinds of policies – it does not matter if the policy action is configured configured as “do nothing”, or to automatically enforce archive site or set it to read-only. I.e. email just says “Select Certify site to confirm if it’s still in use, or consider deleting it if the site is no longer needed.”. Email does not inform users that site will be set to read-only or archived.

Also there is no link where a user can get more info on the subject.


Admin – Inactive sites report

You can download a csv report of inactive sites generated by policy.
Report includes fields:
Site name, URL, Template, Connected to Teams, Sensitivity label, Retention Policy, Site lock state, Last activity date (UTC), Site creation date (UTC), Storage used (GB), Number of site owners, Email address of site owners, Number of site admins, Email address of site admins, Action status, Total notifications count, Action taken on (UTC), Duration in Read-Only.

There is no GUI to see the list of inactive sites (you can only download a csv file), but there is a magic button “Get AI insights”.

Get AI insights

Here are insights I have seen so far:

  • Inactive sites with significant storage usage
  • Multiple sites owned by the same account
  • Sites inactive for over a year

Inactive sites policy behavior

tbc…

References

Content Shared with Everyone: Access Review

This article is for SharePoint or Microsoft 365 admins focusing on governance and information protection. If you have SharePoint Advanced Management (SAM) – aka SharePoint Premium licensed or you got at least one Copilot for Microsoft 365 license (as having m365 Copilot license automatically enables SharePoint Advanced Management in tenant), then under reports – Data access governance (in SharePoint admin center) – you can not only get Content shared with ‘Everyone except external users’ (EEEU) reports, but also initiate access review. Let us look more closely at this functionality and discuss the pros and cons..

I’ll not repeat Microsoft documents:
SharePoint Advanced Management
Content shared with ‘Everyone except external users’ (EEEU) reports
Site access reviews for Data access governance reports
but I’ll focus on what is not there and a real-world experience.

Reports

First of all, report does not provide you with all SharePoint content shared with “Everyone except external users”. Report helps you with what was shared with EEEU in the last 28 days. That drastically limits usage of this feature. I.e. you should first get initial report on the all content shared with EEEU, and somehow take care of it by other means (consider How to Find Content Shared with Everyone in SharePoint and Teams), and only then you can use this Microsoft’s content shared with EEEU report and access review.

You can share content with EEEU or directly – by adding EEEU to resource permissions directly or by including EEEU into SharePoint group. So content shared with EEEU reports come in two flavors – “Specific files, folders and lists” and “Site membership”

“Specific files, folders and lists” user experience

When you initiate access review from the “Specific files, folders and lists” type of report – users (site admins/owners) get email notification that says “You have sites with specific files, folders or lists shared with ‘Everyone except external users’. This means everyone in your organization has access to this content. Review the items shared for potential oversharing and manage their access.

Scrolling down, in the email, site owner can see a list (table) of incompliant sites with the following columns: Site name, privacy, sensitivity, external sharing and “Items shared”. Site name is clickable and sends user to the root of the site.

Below the list of sites there is a button “View shared items” that sends user to the special system page –
“https://orgname.sharepoint.com/teams/site01/_layouts/15/siteaccessreview.aspx/<id>” where he/she can see list of SharePoint items shared with EEEU. Columns are: (item) Name, Shared on (date), Shared by (email), Action (manage access). Item name and manage access are clickable.

If item is a library item – e.g. document – it is displayed correctly – with icon according to the doc type and doc name. Clickin on the doc name – an actual document opens so you can review it’s content.

If item is a list item – it is displayed incorrectly – no icon, no meaningful info about the item (it is displayed as “”). Clicking on the link – a warning icon and message “Can’t preview this file. Open the file or download it to view in your desktop app”. Buttons “Open” and “Download” are there but not helpful as well.

Clicking on “Manage access” opens almost standard “Manage access” dialogue you can have via “manage access” item context menu, but with no “…” more options at the top right:

which makes this dialogue screen useless, as you can only provide additional access to the item or remove all access. You cannot remove EEEU from access without three dots “More options”.

“Stop sharing” literally remove all permissions to the item except owners

By clicking on a group name – admin opens standard SharePoint “People and Group” membership page:
/_layouts/15/people.aspx?MembershipGroupId=X, which is nice.

“Site membership” user experience

In the case with a “Site membership” report, text would be slightly different: “You have sites where ‘Everyone except external users’ has been added to the site membership. This means everyone in your organization has access to this site. Review site permissions for potential oversharing and manage access.“, which makes sense.

Right after that, in the email, site owner can see a list of incompliant sites with the following columns: Site name, privacy, sensitivity, external sharing and “Groups with org-wide access”. Site name is clickable and sends user to the root of the site.

Then there is a button “View SharePoint groups” that sends user to the special system page –
“https://orgname.sharepoint.com/teams/site01/_layouts/15/siteaccessreview.aspx/<id>” where he/she can see list of SharePoint groups (clickable) with EEEU as members. By clicking on a group name – admin opens standard SharePoint “People and Group” membership page:
/_layouts/15/people.aspx?MembershipGroupId=X, which is nice.

siteaccessreview.aspx page

User can navigate directly to the reviews page:
“https://orgname.sharepoint.com/teams/site01/_layouts/15/siteaccessreview.aspx” and if there were reviews initiated by SharePoint admins – and it’ll work – admin will see all access reviews initiated for this site – columns are: Review name, Description, Requested on (date), Status, reviewed by (email) and admin comment. In case no reviews were initiated against tie site – “You have no reviews to take action on” will be displayed. That’s good.

GUI only

Once you got report – you can initiate access review. All must be done in GUI, click-click-click selecting sites… But what if you have thousands? There is no PowerShell cmdlets or API for this functionality, which really limits your ability to implement it gracefully, especially in large Microsoft 365 environments and automate it.

Download detailed report

Report “Specific files/folders/lists…” does not include files, folders, list – i.e. it does not include what exactly is shared with EEEU. Report includes site id, url, name, template, is it teams-connected, sensitivity (?), privacy, external sharing, primary admin name and email, and number of items (?) shared with EEEU.

So technically you can communicate to site owners, but you’d need to rely on them to figure out what content is shared with everyone.

Email template

When you initiate Site access review – an e-mail notification is send to site owners. This e-mail is not customizable at all. The only admin can do is to add a message (for every “initiate Site access review” action).

This email comes from “SharePoint Online <no-reply@sharepointonline>” address (not customizable), so comes “from outside of your organization” and can be considered as scam.

Microsoft’s logos and other graphics are blocked by default and e-mail includes a button “View shared items” – enough red flags for users to consider it as spam. Keep this in mind.

The good news is e-mail contains site name – so site owner can recognize it at act accordingly.

Usage scenarios

Small tenants

In small Microsoft 365 environments – yes, this functionality probably can be used “as is” (and should be used). Especially for new tenants – I’d recommend enable reports and use this feature on a regular basis.

Medium-size tenants

I’m not sure. It depends on your governance rules and company culture.

Enterprises

I’m very pessimistic if this functionality is useful in large environments. Reasons are:

  • if your tenant is not new – you already have a lot of overshared content, so you still need to come up with your custom solution (idk – PowerShell scripts?) to deal with oversharing. But once you designed your custom solution – why don’t you continue to use it?
  • In enterprises usually all the communication must follow approved templates, branding and so on.
  • User experience of reviewing shares with everyone… and managing permissions designed very poorly… In enterprise you do not want to deal with thousands of tickets from site owners who could not figure it out
  • In enterprises you’d think of automation

SharePoint Advanced Management

SharePoint Advanced Management is an add-on to Microsoft 365. Microsoft says that it is a powerful suite of tools for IT admins to bolster content governance throughout the Microsoft Copilot deployment journey. Let us have a closer look at what SharePoint Advanced Management (SAM) is how exactly it helps with governance enforcement in the Copilot era.

Microsoft classifies SAM’s features as “Manage content sprawl”, “Manage content lifecycle”, “Manage permissions and access”. I’d put SAM’s tools into these buckets: Reports, Policies, Search, Features.

SAM Reports

Reports are something that provides you with data, so you can analyze these date and use them to build/update your own governance strategy and maybe take some immediate action. Reports available are:

  • Change history reports
    you can choose org-wide or site-level settings, specify date range, sites and all or specific admins
  • Enterprise App Insights
  • OneDrive Accounts report
  • Data Access Governance (3 different ones)
    • Sharing Links with 3 pre-configured reports:
      Anyone links, People in your org links and Specific People links shared externally
    • Sensitivity labels applied to files: select label -> generate report
    • Content Shared with Everyone Except External Users:
      to discover specific sites whose content was made accessible for EEEU
      you can choose from two types of report: where specific files/folders/lists are shared with EEEU or “Site membership” where EEEU was added as a member and initialize access review
      (see Deep Dive into SAM DAG Content shared with EEEU access review)

SAM Policies

Policies allows you to set some governance rules, so that rules will be applied automatically, with no or little your intervention. SharePoint Advanced Management policies are:

  • Site Lifecycle management: Inactive Site Policy. Allows:
    • identify inactive sites
    • send notifications to site owners and/or admins
    • automatically archive or make sites read-only (or do nothing – just report)
  • Site Lifecycle management: Site Ownership Policy
    (not available at this moment, expected re-launch before end of summer 2025)

AI Insights – report feature that uses a language model to identify patterns and potential issues and provide actionable recommendations to solve issues

Features

Features are smaller that policies, more like an update to existing functionality.

  • Conditional access to SharePoint site policy
    This enhances existing conditional access Entra Id feature with the ability to apply the policy to SharePoint sites directly or via Site sensitivity label.
  • OneDrive access restriction
  • SharePoint site-level access restrictions
  • Block download policy
  • Your recent actions
  • Default sensitivity labels for document libraries
  • Site access review

SAM for Search

I put it separately:

  • restrict discovery of SharePoint sites and content
  • Restricted SharePoint search

TBP

Microsoft 365 admin center displays incorrect sensitivity label

Found an issue in Microsoft 365 – it seems like if a sensitivity label is not published for a user – this user will see inconsistency when different sources display sensitivity label differently.

Scenario: A site/group sensitivity label was created under the Purview center. Label enforces some settings to the group/site, like privacy and external sharing. User cannot assign label to site/group until label is published to that user. In this scenario sensitivity label was published to the admin1 but not published to the admin2.

Admin1 assigned sensitivity label to a group-based site.

What admin2 can see in this case is:

ProductSensitivity Label
Teams correct sensitivity label
SharePoint Site correct sensitivity label
My Groups – Groups I Own – View experiencecorrect sensitivity label
My Groups – Groups I Own – Edit experienceno label
SharePoint Admin Centercorrect sensitivity label
Teams Admin Centercorrect sensitivity label
Microsoft 365 Admin Center – Teams&Groupsno label
Entra Id – Groupsno label

Moreover, in some cases you could even see incorrect sensitivity label under Entra Id…

Microsoft says it’s by design. For me this kind of design simply does not make sense.

There is a Design Change Request:
https://feedback.azure.com/d365community/idea/4246b7a8-2119-f011-9d47-7c1e52d4bdd3

If you think this behavior must be changed – please vote for the DCR.

Below are some screenshots:

In particular,

Tableau Cloud Connection to SharePoint

Recently I helped one client to connect his Tableau Cloud to SharePoint, so let me share how it’s done, as Tableau documentation was not very helpful, so I had to do my own research.

What you’d need is:

  1. Your Tableau Cloud site location
  2. Azure App Registration (Entra Id), configured properly
  3. OAuth Client configured at the Tableau site settings

Having these steps done – you should be able to connect to data (SharePoint or Onedrive site or SharePoint list).

Here are the details…

Your Tableau Cloud location

You need it to build a Redirect URI. Tableau documentation says:

...Note the pod your Tableau Cloud site is located to ensure you enter the correct redirect URL during the registration process in Step 2 below. The redirect URL uses the following format:
https://<your_pod>.online.tableau.com/auth/add_oauth_token
For example, https://us-west-2b.online.tableau.com/auth/add_oauth_token

So, you would check this part in bold of your Tableau cloud instance:
https://us-west-2b.online.tableau.com/
and construct a Redirection URI:
https://us-west-2b.online.tableau.com/auth/add_oauth_token

Examples are:

https://10ay.online.tableau.com/auth/add_oauth_token
https://dub01.online.tableau.com/auth/add_oauth_token
https://us-east-1.online.tableau.com/auth/add_oauth_token

Azure (Entra Id) App Registration

You need an App Registration under Entra Id, with API permissions consented and Authentication configured

API permissions must be the following: Under Graph API, delegated Files.Read.All, Sites.Read.All, User.Read, offline_access:

Authentication blade. You’d add platform: Web and use Redirect URI as above. Example:

Secret

Secret you’d generate under App Registration Certificates and secrets:

Once secret is generated, copy the secret value in a safe place and do not share it.

Also, get your app id and tenant id (those are not secrets but I still prefer not to share):

At this moment you should have from your App registration:

  • Tenant Id
  • Client (App) Id
  • Client Secret
  • Redirect Url

Now we are ready to configure

OAuth Client at the Tableau Site Settings

Having Site Admin permissions (Tableau Site Admin, not SharePoint), you should be able from the left menu navigate to the bottom “Settings” and under General tab scroll down to the “OAuth client registry” and click “Add OAuth Client”.

You’d need two OAuth client configured – one for “OneDrive and SharePoint Online” and the other one for “SharePoint List (JDBC)”.

OneDrive and SharePoint Online” Experience is:

Here your OAuth instance Url would be:
https://login.microsoftonline.com/<Teanan tId>/

e.g.:
https://login.microsoftonline.com/332d223e-c55f-4c38-af69-214fe2a73f0a/

Client Id, Client secret and Reirect Url you can get from Step 2.

SharePoint List (JDBC)” experience:

Same here.
OAuth instance Url is: https://login.microsoftonline.com/<your tenant id>/
Client Id, client secret and redirect Url you get from Step2.

Now you are ready to connect…

Tableau Connect to Data: OneDrive and SharePoint Online

Connecting to Data from Tableau, you’d select “OneDrive and SharePoint Online” or “SharePoint List (JDBC)”

Connecting to “OneDrive and SharePoint Online” – you’ll be asked to provide “OAuth Instance Url” again:

So, again, you’d put your tenant Id instead of “common”. After connected, you’d see something like this:

Under OneDrive (personal files) – you’d see your own content located at your personal OneDrive site.
Under OneDrive (shared with you) – you’d see content shared with you and located at other’s personal OneDrive sites.
Under SharePoint sites – you’d see content of SharePoint sites you have access to – all content – documents, lists etc.

Connecting to “SharePoint List (JDBC)” – experience would be

So, you’d provide a specific site collection Url (not list), e.g.
https://contoso.sharepoint.com/teams/Test-Site-01
and you’d provide “OAuth Instance Url” again, just remember – replace “common” with your Tenant Id.

In both cases you should get a pop-up authentication window – provide your credentials after that you should be able to see data in SharePoint.

Possible error messages

Client secret

Client secret is an essential part. It is not market as required in the form, but without secret connection is not working. You can get something like this:

Tableau received an OAuth error from your request. Please see the error message for more information: 401 Unauthorized POST https://login.microsoftonline.com/—/oauth2/v2.0/token. (errorCode=170006)

Reply address

If you did not configure Authentication at your App Reg or configured incorrectly – you might get error message “Sorry, but we’re having trouble signing you in” “AADSTS900971: No reply address provided.”


Solving Issues with m365 Copilot Agents

Issue # 1 – Unable to create

It says “Unable to create” and “We were unable to create this agent due to an error. Try again.”
or
“Unable to update” and “We were unable to update this agent due to an error. Try again.”

Unable to create
We were unable to create this agent due to an error. Try again.
Unable to update

We were unable to update this agent due to an error. Try again.

More info:
The problem appears to be when copilot tries to save agent (POST to https://powervamg.us-il105.gateway.prod.island.powerapps.com/chatbotmanagement/tenants/…/environments/Default-…/minimalBots/api/…/publish – it returns 500 Internal Server Error).

Cause:
In my case the cause was my license was assigned a few hours before, so it seems like it takes some time for Microsoft to populate updates.
In the other case I troubleshooted it turned out user did not have a license assigned (license was removed), but “Create an agent” button was available.

Solution:
Ensure you have a license for Microsoft 365 copilot assigned and (ideally) wait 24 hours.
Microsoft: “If you recently purchased a license or started a free trial, it may take up to 24 hours for the license to take effect.”

Python connect to SharePoint via Graph API with Delegated Permissions

Below is the sample Python code to authenticate against Microsoft 365 as current user with MSA library and to call Microsoft Graph API – specifically get SharePoint Site, get Site lists with requests library.

But first, you have to have an App Registration in Azure (Entra ID) with delegated permissions consented and authentication configured.

Delegated Permissions

If your solution needs access to all SharePoint sites – consider Sites.FullControl.All or Sites.Manage.All or Sites.ReadWrite.All or Sites.Read.All depending on access level you need. Effective permissions would be a min from both – permissions configured for app registration and permissions current user have. Once consented at the app registration – these permissions will work right away.

If your solution needs access to one (or a few) SharePoint sites – consider Sites.Selected API permissions as it will scope down access to only sites that are required for your solution to work. Remember, Sites.Selected API permissions, even consented at the app registration, does require second step – SharePoint admin should provide (read or write or manage or fullcontrol) permissions for the app registration to a specific site or sites.

Authentication

You’d also need to configure authentication blade. How? It depends on the kind of application you are building etc. For example for native apps I do:
– add platform – “Mobile and Desktop app”
– select “https://login.microsoftonline.com/common/oauth2/nativeclient”
– select “msal096fd951-7285-4e4f-9c1f-23a393556b19://auth (MSAL only)”
– add custom Redirect URI: “http://localhost”

This config works for Python code below

Python Code

You’d need to install/import the libraries: json, configparser, msal, requests

Here is the code:

import json
import configparser
import msal
import requests

config = configparser.ConfigParser()
config.read('config.cfg')
client_id = config.get('delegated','clientId')
authority = config.get('delegated','authority')
scopes = config.get('delegated','scope').split()
siteId = config.get('delegated','siteId')
print( client_id)
print( authority)
print( scopes)
print( siteId)

global_token_cache = msal.TokenCache()
global_app = msal.PublicClientApplication(
    client_id,
    authority=authority,  # For Entra ID or External ID
    token_cache=global_token_cache,  
    )

def acquire_and_use_token():
    # The pattern to acquire a token looks like this.
    result = None
    result = global_app.acquire_token_interactive(scopes)

    if "access_token" in result:
        print("Token was obtained from:", result["token_source"])  # Since MSAL 1.25
        # print("Token acquisition result", json.dumps(result, indent=2))
        return result["access_token"]
    else:
        print("Token acquisition failed", result)  # Examine result["error_description"] etc. to diagnose error
        return None


token = acquire_and_use_token()


http_headers = {'Authorization': 'Bearer ' + token,
                'Accept': 'application/json',
                'Content-Type': 'application/json'}

graph_url = 'https://graph.microsoft.com/v1.0/sites/' + siteId + '?$select=id,webUrl,displayName'

siteResponse = requests.get(graph_url, headers=http_headers)
print(siteResponse.status_code)
site = json.loads(siteResponse.content)
# print("Site (raw) : ")
# print(site)
print("Site webUrl : ", site["webUrl"])
print("Site displayName : ", site["displayName"])

# Lists
graph_url = 'https://graph.microsoft.com/v1.0/sites/' + siteId + '/lists'
listsResponse = requests.get(graph_url, headers=http_headers)
print(listsResponse.status_code)
lists = json.loads(listsResponse.content)
# print("Site lists (raw):")
# print(lists)
print("Site lists:")
for list in lists["value"]:
    print("  Display Name:", list["displayName"])
    print("   Id:", list["id"])
    print("   Web Url:", list["webUrl"])
    print("   Created Date:", list["createdDateTime"])
    print("   Last Modified Date:", list["lastModifiedDateTime"])

Application permissions

If your scenario is to call Graph API from Python with application permissions (aka unattended or daemon app) – the main difference is authentication. It is described here. It also requires App registration configured like this.