Search code examples
azureazure-functionsmicrosoft-graph-apiazure-python-sdk

Adding members to MS Entra Group via MS Graph API fails via Python Azure Function App


Setting the scene: I'm trying to add members to a MS Entra Group by calling the MS Graph API from a Python application hosted on an Azure Functions App instance. The application runs on behalf of an SPN that authenticates via a client certificate and has the User.Read.All MS Graph API permission. Furthermore, the SPN is owner of the MS Entra Group. First the app fetches the user' object IDs (I) and then, it adds them to the MS Entra group as members (II).

Problem: If I run the Python logic from my local machine on behalf of the SPN, the members are added to my MS Entra Group. If I run the logic on the Azure Fuction app, the app is able to retrieve the users' object IDs but fails to add the members. The MS Graph API returns: Error: 403, {"code":"Authorization_RequestDenied","message":"Insufficient privileges to complete the operation.".

Evironment Variables for DefaultAzureCredential

$env:AZURE_CLIENT_CERTIFICATE_PATH = ".\cert.pem"
$env:AZURE_CLIENT_ID = "00000000-0000-0000-0000-000000000000"
$env:AZURE_TENANT_ID = "11111111-1111-1111-1111-111111111111"

Python script

import requests, re 
from azure.identity import DefaultAzureCredential

class GraphClient:
    def __init__(self, az_credential):
        self._graph_token = az_credential.get_token("https://graph.microsoft.com/.default").token

    def get_members_object_ids(self, members):
        url = "https://graph.microsoft.com/v1.0/users"
        headers = {
            "Authorization": f"Bearer {self._graph_token}",
            "Content-Type": "application/json"
        }
        members = re.findall(r'\((.*?)\)', members)
        members_ids = []
        for member in members:
            query_params = {
                "$filter": f"mailNickname eq '{member}'"
            }
            try:
                response = requests.get(url=url, headers=headers, params=query_params)
                response.raise_for_status()
                members_ids.append(response.json()['value'][0]['id'])
            except Exception as e:
                raise e
        return members_ids
    
    def add_members_to_group(self, group_id, member_ids: list):
        url = f"https://graph.microsoft.com/v1.0/groups/{group_id}/members/$ref"
        headers = {
            "Authorization": f"Bearer {self._graph_token}",
            "Content-Type": "application/json"
        }
        for member_id in member_ids:
            data = {
                "@odata.id": f"https://graph.microsoft.com/v1.0/users/{member_id}"
            }
            try:
                response = requests.post(url=url, json=data, headers=headers)
                response.raise_for_status()
                print(f"Member with id {member_id} added to group with id {group_id}")
            except Exception as e:
                print(f"{response.status_code}: {response.text}")
                raise e
            
members = "Tiger Woods (AAAA), Taylor Swift (BBBB)"
adgr_id = "22222222-2222-2222-2222-222222222222"
          
az_credential = DefaultAzureCredential()
graph_client = GraphClient(az_credential)
members_ids = graph_client.get_members_object_ids(members)
graph_client.add_members_to_group(adgr_id, members_ids)

Output Running Locally If I run the script locally (on behalf of the SPN) everything runs as expected and I receive the following output: enter image description here

Output Running on Azure Functions If I run the logic on Azure Functions app in the Azure Cloud, I am able to retrieve the users' object IDs but I receive the following error output once I try to add the MS Entra Group members: enter image description here

2024-08-28T13:50:04Z   [Information]   CertificateCredential.get_token succeeded
2024-08-28T13:50:04Z   [Information]   EnvironmentCredential.get_token succeeded
2024-08-28T13:50:04Z   [Information]   DefaultAzureCredential acquired a token from EnvironmentCredential
2024-08-28T13:50:05Z   [Error]   403: {"error":{"code":"Authorization_RequestDenied","message":"Insufficient privileges to complete the operation.","innerError":{"date":"2024-08-28T13:50:04","request-id":"603ee6e0-07b1-44b4-ad82-43b4709aa761","client-request-id":"603ee6e0-07b1-44b4-ad82-43b4709aa761"}}}
2024-08-28T13:50:05Z   [Error]   Executed 'Functions.create_ad_group' (Failed, Id=25c267f3-49df-4d78-b826-b4de42311f83, Duration=8280ms)

Tokens I have decoded the tokens and both have the following roles key-value pair:

"roles": [
  "User.Read.All"
],

Is there a way to fix this issue without granting broader API permissions?


Solution

  • During the troubleshooting process, I tested using a locally generated token in the Azure Function App, which successfully added members to the group. Subsequently, I used the Azure Function App-generated token in my local environment, and this also resulted in successful member additions. This confirmed that both tokens were functioning correctly.

    To address the issue, I introduced a delay in my Python logic between the creation of the MS Entra Group and the addition of members. This resolved the problem, leading me to suspect that the MS Entra Group was not fully provisioned when I initially attempted to call the MS Graph API for adding members.