Highlight

Do you want to query SharePoint and get the files or list information in a secure way using Azure’s Service Principals or Managed Identities? Here is a short guide on how to do it.

Why using Graph API us better and what changed from 2019?

In 2021 Microsoft published this blog entry (https://devblogs.microsoft.com/microsoft365dev/controlling-app-access-on-specific-sharepoint-site-collections/) where they show the newest way to connect to SharePoint sites in a secure way using Graph permissions. This was previously possible using Sites.*.All permissions, but the issue was that Sites.ReadWrite.All or Sites.FullControl.All was highly privileged permissions, meaning a service principal could literally read any site on your SharePoint. As we know there are many sites that should not be accessed by dev teams like accounting, financial controlling, HR, etc.

Now graph allows new permission called Sites.Selected which means you can use graph endpoints, but access the sites that your service principal was excplicitly granted access to.

This blog entry will describe how to assign these permissions using PowerShell so that you can automate this process.

Permissions

In order to use Graph API your service principal will need two permissions assigned

  1. On Microsoft Graph API (in Azure Active Directory)
  2. On SharePoint Site via
    1. Graph call performed by Site admin
    2. Old interface to grant SharePoint app permissions

All ways are described in steps below.

Step 1 - assign graph API Sites.Selected permissios

Option #1 - Azure Portal route

  1. Navigate to Azure AD
  2. Find your app registration
  3. Add new Graph Permissions (Note: This can be done by App Owner)
  4. Find Sites.Selected permission and click add
  5. Notice that the permission was added but is NOT active because it requires admin consent. (Note: This step must be performed by Azure AD Global Administrator)
  6. Verify that the permission was granted

PowerShell route

$principalNames = "sp-demo2"
$RequiredPermissions = "Sites.Selected"

foreach ($principalName in $principalNames) {  
  Write-Host "Assigning privileges for $principalName" -ForegroundColor Green   
  $sp = Get-AzureADServicePrincipal -SearchString $principalName  
  if ($sp.AppId -eq $null) {
      
      $errorMessage = "Principal $principalName was not found. Please check principal name."
      Write-Host $errorMessage -ForegroundColor Magenta
      Write-Error $errorMessage  
  } else {
    
    $PrincipalId = $sp.AppId  
    $Graph = Get-AzureADServicePrincipal -Filter "AppId eq '00000003-0000-0000-c000-000000000000'"
    $Principal = Get-AzureADServicePrincipal -Filter "AppId eq '$PrincipalId'"  
    $RequiredPermissions | %{
      $permission = $_
      Write-Output "Assigning permission $permission"
      $Role = $Graph.AppRoles | Where-Object { $_.Value -eq $permission } | Select-Object -First 1  
      New-AzureADServiceAppRoleAssignment `
        -Id $Role.Id `
        -PrincipalId $Principal.ObjectId `
        -ResourceId $Graph.ObjectId `
        -ObjectId $Principal.ObjectId
    }
  }  
}

Step 2 - Assign SharePoint Site Permissions

You need to assign following read or write permissions to service principal using Permissions API. There are two ways to do it.

  1. via Graph API /sites/{siteId}/permissionss endpoint
  2. via App Permission Panel (old way)

Prerequisites

  1. Get SharePoint site ID by opening URL https://<domain>.sharepoint.com/sites/<site_name>/_api/site/id (replace domain and site_name with your site and domain)

Option #1 - Graph Endpoint way

POST https://graph.microsoft.com/v1.0/sites/{siteId}/permissions

Content-Type: application/json

{
  "roles": ["write"],
  "grantedToIdentities": [{
    "application": {
      "id": "89ea5c94-7736-4e25-95ad-3fa95f62b66e",
      "displayName": "Foo App"
    }
  }]
}

Documentation links

PowerShell

# MS Graph PowerShell Modules Docs: https://docs.microsoft.com/en-us/powershell/microsoftgraph/overview
# Installation (Run those commands - one time setup)
#   Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
#   Install-Module Microsoft.Graph -Scope CurrentUser
#   Import-Module Microsoft.Graph

# INPUTS
$siteId = "cfe98761-6d30-4fc9-ab6e-90e3eb304c03"
$appDisplayName = "sp-demo2"
$permission = "read"

# CONNECT
Connect-MgGraph -Scopes "Sites.Read.All","Sites.FullControl.All"
Connect-AzureAD

$sp = Get-AzureADServicePrincipal -SearchString $appDisplayName  
$appId = $sp.AppId

$site = Get-MGSite -SiteId $siteId 

# TEST - Get Current Permissions 
Write-Host "Before the update" -ForegroundColor Green
$permissions = Get-MgSitePermission -SiteId $siteId
$permissions | Select -ExpandProperty GrantedToIdentities | Select -ExpandProperty Application

# ADD - new Service Principal
$body = @{
  'roles' = @(
      $permission
   );
  'grantedToIdentities' = @(
      @{
        'application' = @{
          'id' = $appId
          'displayName' = $appDisplayName
        }
      }
   );
}
$bodyJson = $body | ConvertTo-Json -Depth 10 -Compress
Invoke-MgGraphRequest `
    -Method POST `
    -Uri "v1.0/sites/$SiteId/permissions" `
    -Body $bodyJson

# TEST - Get Updated Permissions 
Write-Host "After the update" -ForegroundColor Green
$permissions = Get-MgSitePermission -SiteId $siteId
$permissions | Select -ExpandProperty GrantedToIdentities | Select -ExpandProperty Application

White it’s not a recommended way, it seems to be working too for Graph API access. App consent is defined here https://learn.microsoft.com/en-us/sharepoint/dev/solution-guidance/security-apponly-azureacs

  1. Open Site app insite URL https://<domain>.sharepoint.com/sites/<site_name>/_layouts/15/appinv.aspx
  2. Fill in the form
  3. Use following payload (use proper permissions Reader/Contributor/FullControl)
    <AppPermissionRequests AllowAppOnlyPolicy="true">
      <AppPermissionRequest Scope="http://sharepoint/content/sites/<site_name>" Right="Reader" />
    </AppPermissionRequests>
    
    • Reader
    • Contributor
    • FullControl
  4. Trust the app

DONE! Time for testing

Now you can use two graph endpoints to get the data

  1. Sites https://learn.microsoft.com/en-us/graph/api/resources/site - to query all website information based on assigned permissions
  2. Drive https://learn.microsoft.com/en-us/graph/api/resources/drive - to upload / download files on SharePoint sites

POSTMAN call

Example 1: Logic apps with Service Principal (hardcoded for demo)

  1. Logic App design
  2. Code the cope into code view
    {
        "definition": {
            "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
            "actions": {
                "For_each_Site_Drive": {
                    "actions": {
                        "Condition": {
                            "actions": {
                                "For_each_Drive_Item": {
                                    "actions": {
                                        "Create_blob_(V2)": {
                                            "inputs": {
                                                "body": "@binary(body('HTTP_-_SharePoint_Get_Drive_Item_Content'))",
                                                "headers": {
                                                    "Content-Type": "@{body('HTTP_-_SharePoint_Get_Drive_Item_Content')?['$content-type']}",
                                                    "ReadFileMetadataFromServer": true
                                                },
                                                "host": {
                                                    "connection": {
                                                        "name": "@parameters('$connections')['azureblob']['connectionId']"
                                                    }
                                                },
                                                "method": "post",
                                                "path": "/v2/datasets/@{encodeURIComponent(encodeURIComponent('azsharepoint'))}/files",
                                                "queries": {
                                                    "folderPath": "hr-files/logic-app-sp",
                                                    "name": "@{body('HTTP_-_SharePoint_Get_Drive_Item_by_ID')?['name']}",
                                                    "queryParametersSingleEncoded": true
                                                }
                                            },
                                            "runAfter": {
                                                "HTTP_-_SharePoint_Get_Drive_Item_Content": [
                                                    "Succeeded"
                                                ]
                                            },
                                            "runtimeConfiguration": {
                                                "contentTransfer": {
                                                    "transferMode": "Chunked"
                                                }
                                            },
                                            "type": "ApiConnection"
                                        },
                                        "HTTP_-_SharePoint_Get_Drive_Item_Content": {
                                            "inputs": {
                                                "method": "GET",
                                                "uri": "@{items('For_each_Drive_Item')?['@microsoft.graph.downloadUrl']}"
                                            },
                                            "runAfter": {
                                                "HTTP_-_SharePoint_Get_Drive_Item_by_ID": [
                                                    "Succeeded"
                                                ]
                                            },
                                            "runtimeConfiguration": {
                                                "contentTransfer": {
                                                    "transferMode": "Chunked"
                                                }
                                            },
                                            "type": "Http"
                                        },
                                        "HTTP_-_SharePoint_Get_Drive_Item_by_ID": {
                                            "inputs": {
                                                "authentication": {
                                                    "audience": "https://graph.microsoft.com",
                                                    "clientId": "@body('Set_Client_Credentials')?['ClientId']",
                                                    "secret": "@body('Set_Client_Credentials')?['ClientSecret']",
                                                    "tenant": "@body('Set_Client_Credentials')?['TenantId']",
                                                    "type": "ActiveDirectoryOAuth"
                                                },
                                                "method": "GET",
                                                "uri": "https://graph.microsoft.com/v1.0/sites/@{variables('SiteId')}/drives/@{items    ('For_each_Site_Drive')?['id']}/items/@{items('For_each_Drive_Item')?['id']}"
                                            },
                                            "runAfter": {},
                                            "type": "Http"
                                        }
                                    },
                                    "foreach": "@body('HTTP_-_SharePoint_Get_Drive_Items')?['value']",
                                    "runAfter": {
                                        "HTTP_-_SharePoint_Get_Drive_Items": [
                                            "Succeeded"
                                        ]
                                    },
                                    "type": "Foreach"
                                },
                                "HTTP_-_SharePoint_Get_Drive_Items": {
                                    "inputs": {
                                        "authentication": {
                                            "audience": "https://graph.microsoft.com",
                                            "clientId": "@body('Set_Client_Credentials')?['ClientId']",
                                            "secret": "@body('Set_Client_Credentials')?['ClientSecret']",
                                            "tenant": "@body('Set_Client_Credentials')?['TenantId']",
                                            "type": "ActiveDirectoryOAuth"
                                        },
                                        "method": "GET",
                                        "uri": "https://graph.microsoft.com/v1.0/sites/@{variables('SiteId')}/drives/@{items    ('For_each_Site_Drive')?['id']}/root/children"
                                    },
                                    "runAfter": {},
                                    "type": "Http"
                                }
                            },
                            "expression": {
                                "and": [
                                    {
                                        "equals": [
                                            "@items('For_each_Site_Drive')?['name']",
                                            "Documents"
                                        ]
                                    }
                                ]
                            },
                            "runAfter": {},
                            "type": "If"
                        }
                    },
                    "foreach": "@body('HTTP_-_SharePoint_Get_Drives')?['value']",
                    "runAfter": {
                        "HTTP_-_SharePoint_Get_Drives": [
                            "Succeeded"
                        ]
                    },
                    "type": "Foreach"
                },
                "For_each_Site_List": {
                    "actions": {
                        "HTTP_-_SharePoint_List_Files": {
                            "inputs": {
                                "authentication": {
                                    "audience": "https://graph.microsoft.com",
                                    "clientId": "@body('Set_Client_Credentials')?['ClientId']",
                                    "secret": "@body('Set_Client_Credentials')?['ClientSecret']",
                                    "tenant": "@body('Set_Client_Credentials')?['TenantId']",
                                    "type": "ActiveDirectoryOAuth"
                                },
                                "method": "GET",
                                "uri": "https://graph.microsoft.com/v1.0/sites/@{variables('SiteId')}/lists/@{items('For_each_Site_List')?    ['id']}/items"
                            },
                            "runAfter": {},
                            "type": "Http"
                        }
                    },
                    "foreach": "@body('HTTP_-_SharePoint_Get_Lists')?['value']",
                    "runAfter": {
                        "HTTP_-_SharePoint_Get_Lists": [
                            "Succeeded"
                        ]
                    },
                    "type": "Foreach"
                },
                "HTTP_-_Get_Site_Id": {
                    "inputs": {
                        "authentication": {
                            "audience": "https://graph.microsoft.com",
                            "clientId": "@body('Set_Client_Credentials')?['ClientId']",
                            "secret": "@body('Set_Client_Credentials')?['ClientSecret']",
                            "tenant": "@body('Set_Client_Credentials')?['TenantId']",
                            "type": "ActiveDirectoryOAuth"
                        },
                        "method": "GET",
                        "uri": "https://graph.microsoft.com/v1.0/sites/adammarczak.sharepoint.com:/sites/hr"
                    },
                    "runAfter": {
                        "Set_Client_Credentials": [
                            "Succeeded"
                        ]
                    },
                    "type": "Http"
                },
                "HTTP_-_SharePoint_Get_Drives": {
                    "inputs": {
                        "authentication": {
                            "audience": "https://graph.microsoft.com",
                            "clientId": "@body('Set_Client_Credentials')?['ClientId']",
                            "secret": "@body('Set_Client_Credentials')?['ClientSecret']",
                            "tenant": "@body('Set_Client_Credentials')?['TenantId']",
                            "type": "ActiveDirectoryOAuth"
                        },
                        "method": "GET",
                        "uri": "https://graph.microsoft.com/v1.0/sites/@{variables('SiteId')}/drives"
                    },
                    "runAfter": {
                        "For_each_Site_List": [
                            "Succeeded"
                        ]
                    },
                    "type": "Http"
                },
                "HTTP_-_SharePoint_Get_Lists": {
                    "inputs": {
                        "authentication": {
                            "audience": "https://graph.microsoft.com",
                            "clientId": "@body('Set_Client_Credentials')?['ClientId']",
                            "secret": "@body('Set_Client_Credentials')?['ClientSecret']",
                            "tenant": "@body('Set_Client_Credentials')?['TenantId']",
                            "type": "ActiveDirectoryOAuth"
                        },
                        "method": "GET",
                        "uri": "https://graph.microsoft.com/v1.0/sites/@{variables('SiteId')}/lists"
                    },
                    "runAfter": {
                        "Initialize_variable_-_Site_Id": [
                            "Succeeded"
                        ]
                    },
                    "type": "Http"
                },
                "Initialize_variable_-_Site_Id": {
                    "inputs": {
                        "variables": [
                            {
                                "name": "SiteId",
                                "type": "string",
                                "value": "@{split(body('HTTP_-_Get_Site_Id')?['id'],',')[1]}"
                            }
                        ]
                    },
                    "runAfter": {
                        "HTTP_-_Get_Site_Id": [
                            "Succeeded"
                        ]
                    },
                    "type": "InitializeVariable"
                },
                "Set_Client_Credentials": {
                    "inputs": {
                        "content": {
                            "ClientId": "<id_here>",
                            "ClientSecret": "<key_here_move_to_AKV_later>",
                            "TenantId": "marczak.io"
                        },
                        "schema": {
                            "properties": {
                                "ClientId": {
                                    "type": "string"
                                },
                                "ClientSecret": {
                                    "type": "string"
                                },
                                "TenantId": {
                                    "type": "string"
                                }
                            },
                            "type": "object"
                        }
                    },
                    "runAfter": {},
                    "type": "ParseJson"
                }
            },
            "contentVersion": "1.0.0.0",
            "outputs": {},
            "parameters": {
                "$connections": {
                    "defaultValue": {},
                    "type": "Object"
                }
            },
            "triggers": {
                "manual": {
                    "inputs": {},
                    "kind": "Http",
                    "type": "Request"
                }
            }
        },
        "parameters": {
            "$connections": {
                "value": {
                    "azureblob": {
                        "connectionId": "/subscriptions/f73706f8-c55b-42b7-9d31-6fc8e0d24146/resourceGroups/az-sharepoint/providers/    Microsoft.Web/connections/azureblob",
                        "connectionName": "azureblob",
                        "connectionProperties": {
                            "authentication": {
                                "type": "ManagedServiceIdentity"
                            }
                        },
                        "id": "/subscriptions/f73706f8-c55b-42b7-9d31-6fc8e0d24146/providers/Microsoft.Web/locations/westeurope/managedApis/    azureblob"
                    }
                }
            }
        }
    }
    
  3. Update values

Example 2: Data Factory with Managed Identity

  1. Graphical design (iterate over files and copy them to Blob storage)
  2. Code
    {
        "name": "PL_SHAREPOINT_SERVICE_PRINCIPAL_DEMO",
        "properties": {
            "activities": [
                {
                    "name": "HTTP - SharePoint Get Drive Items",
                    "type": "WebActivity",
                    "dependsOn": [],
                    "policy": {
                        "timeout": "7.00:00:00",
                        "retry": 0,
                        "retryIntervalInSeconds": 30,
                        "secureOutput": false,
                        "secureInput": false
                    },
                    "userProperties": [],
                    "typeProperties": {
                        "url": "https://graph.microsoft.com/v1.0/sites/cfe98761-6d30-4fc9-ab6e-90e3eb304c03/drives/    b!YYfpzzBtyU-rbpDj6zBMAzynDGDfj_pLmKL5ZkL_4LBuUyOwmNYrRYKg9JqsFX19/root/children",
                        "method": "GET",
                        "authentication": {
                            "type": "ServicePrincipal",
                            "userTenant": "3266fd7c-39d3-44b8-9551-b0ffbf68c764",
                            "username": {
                                "value": "@variables('ClientId')",
                                "type": "Expression"
                            },
                            "resource": "https://graph.microsoft.com",
                            "password": {
                                "type": "AzureKeyVaultSecret",
                                "store": {
                                    "referenceName": "AKV_LS",
                                    "type": "LinkedServiceReference"
                                },
                                "secretName": "ClientSecret"
                            }
                        }
                    }
                },
                {
                    "name": "For each Drive Item",
                    "type": "ForEach",
                    "dependsOn": [
                        {
                            "activity": "HTTP - SharePoint Get Drive Items",
                            "dependencyConditions": [
                                "Succeeded"
                            ]
                        }
                    ],
                    "userProperties": [],
                    "typeProperties": {
                        "items": {
                            "value": "@activity('HTTP - SharePoint Get Drive Items').output.value",
                            "type": "Expression"
                        },
                        "isSequential": false,
                        "activities": [
                            {
                                "name": "HTTP - SharePoint Get Drive Item by ID",
                                "type": "WebActivity",
                                "dependsOn": [],
                                "policy": {
                                    "timeout": "7.00:00:00",
                                    "retry": 0,
                                    "retryIntervalInSeconds": 30,
                                    "secureOutput": false,
                                    "secureInput": false
                                },
                                "userProperties": [],
                                "typeProperties": {
                                    "url": {
                                        "value": "https://graph.microsoft.com/v1.0/sites/cfe98761-6d30-4fc9-ab6e-90e3eb304c03/drives/    b!YYfpzzBtyU-rbpDj6zBMAzynDGDfj_pLmKL5ZkL_4LBuUyOwmNYrRYKg9JqsFX19/items/@{item().id}",
                                        "type": "Expression"
                                    },
                                    "method": "GET",
                                    "authentication": {
                                        "type": "ServicePrincipal",
                                        "userTenant": "3266fd7c-39d3-44b8-9551-b0ffbf68c764",
                                        "username": {
                                            "value": "@variables('ClientId')",
                                            "type": "Expression"
                                        },
                                        "resource": "https://graph.microsoft.com",
                                        "password": {
                                            "type": "AzureKeyVaultSecret",
                                            "store": {
                                                "referenceName": "AKV_LS",
                                                "type": "LinkedServiceReference"
                                            },
                                            "secretName": "ClientSecret"
                                        }
                                    }
                                }
                            },
                            {
                                "name": "Save File",
                                "type": "Copy",
                                "dependsOn": [
                                    {
                                        "activity": "HTTP - SharePoint Get Drive Item by ID",
                                        "dependencyConditions": [
                                            "Succeeded"
                                        ]
                                    }
                                ],
                                "policy": {
                                    "timeout": "7.00:00:00",
                                    "retry": 0,
                                    "retryIntervalInSeconds": 30,
                                    "secureOutput": false,
                                    "secureInput": false
                                },
                                "userProperties": [],
                                "typeProperties": {
                                    "source": {
                                        "type": "BinarySource",
                                        "storeSettings": {
                                            "type": "HttpReadSettings",
                                            "requestMethod": "GET"
                                        },
                                        "formatSettings": {
                                            "type": "BinaryReadSettings"
                                        }
                                    },
                                    "sink": {
                                        "type": "BinarySink",
                                        "storeSettings": {
                                            "type": "AzureBlobStorageWriteSettings"
                                        }
                                    },
                                    "enableStaging": false
                                },
                                "inputs": [
                                    {
                                        "referenceName": "DS_HTTP_BINARY",
                                        "type": "DatasetReference",
                                        "parameters": {
                                            "URL": {
                                                "value": "@activity('HTTP - SharePoint Get Drive Item by ID').output['@microsoft.graph.    downloadUrl']",
                                                "type": "Expression"
                                            }
                                        }
                                    }
                                ],
                                "outputs": [
                                    {
                                        "referenceName": "DS_BLOB_BINARY",
                                        "type": "DatasetReference",
                                        "parameters": {
                                            "FileName": {
                                                "value": "@activity('HTTP - SharePoint Get Drive Item by ID').output.name",
                                                "type": "Expression"
                                            }
                                        }
                                    }
                                ]
                            }
                        ]
                    }
                }
            ],
            "variables": {
                "ClientId": {
                    "type": "String",
                    "defaultValue": "e02f3944-2f40-436e-a261-38ed9bcabc80"
                },
                "ClientSecret": {
                    "type": "String",
                    "defaultValue": "guo7Q~0ISDRVJDXYxtIihcgdNkj4ZtvxLkS2j"
                },
                "TenantId": {
                    "type": "String",
                    "defaultValue": "marczak.io"
                }
            },
            "annotations": [],
            "lastPublishTime": "2022-06-08T07:54:06Z"
        },
        "type": "Microsoft.DataFactory/factories/pipelines"
    }
    

This is currently the best way to query graph using application accounts (service principals and managed identities). Hope it will be useful for you!

Adam Marczak

Programmer, architect, trainer, blogger, evangelist are just a few of my titles. What I really am, is a passionate technology enthusiast. I take great pleasure in learning new technologies and finding ways in which this can aid people every day. My latest passion is running an Azure 4 Everyone YouTube channel, where I show that Azure really is for everyone!

Did you enjoy the article?

Share it!

More tagged posts