Least principle all the way

You might already have heard of the Sites.Selected Graph permission scope, to restrict service principal access to individual SharePoint (or OneDrive) sites. But are you aware that you can even go as far as restricting access to library, folder or even item level?

As always, the security principle of least privilege should be the default operating procedure. Reducing access is the first step in reducing the possible attack paths and preventing exposure.

If Sites.Selected is too much permission, we have the following permission scopes to our disposal:

Graph Permission Description
Lists.SelectedOperations.Selected Allow the application to access a subset of lists on behalf of the signed in user.

Note that a list in SharePoint can be anything from a classic SharePoint List, to Libraries, to calendars.
ListItems.SelectedOperations.Selected Allow the application to access a subset of listitems on behalf of the signed in user.
Files.SelectedOperations.Selected Allow the application to access files explicitly permissioned to the application on behalf of the signed in user.

This is a more specific case of the Lists.SelectedOperations.Selected permission, where only items in libraries can be processed, but not items of all list types (e.g. no access to events in a calendar list).

What might look unusual is the SelectedOperations part. This means that we not only either grant full control or nothing but have more nuanced permission roles available. However, these permissions are not granted in Entra. Instead, they are given directly in SharePoint Online using PowerShell Cmdlets. The permissions are:

Permission Role Description
Read Read the metadata and contents of the resource.
write Read and modify the metadata and contents of the resource.
owner Represents the owner role.
fullcontrol Represents full control of the resource.

To the best of my knowledge, and the tests I performed the roles fullcontrol and owner are the same with regards to permissions. If you know any difference leave a comment below.

How to assign Selected permissions

Setting up such permission is only scarcely documented, if at all. So, let’s dive into what we need to make these selected permissions work.

We need to differentiate between two scopes:

  1. The permission giver who has the right to assign permissions.
    This can be an admin, but formally it is enough to have full control on the parent scope to give access to a sub-scope.

  2. The selected permission user, who is unprivileged until receiving permissions by the permission giver.

As we have different levels where we can give permissions lets break down what access we need to be a permission giver:

  1. On the site level you always need Sites.FullControl.All as the anchor permission:
  2. For all other levels (list, library, listitem, file) you can also work with the SelectedOperations permission scope and assign the permission giver full control. Then the permission giver can also work with least privilege.

Graph Permissions available to set permissions at the site level

Permissions required to give permissions on a site level (Own Drawing)

Graph Permissions available to set permissions at the list level

Permissions required to give permissions on a list item level (Own Drawing)

A practical example

Imagine the following scenario:

You are an admin at retail operations. Most stores are centrally managed, but select stores are managed individually. All stores report their inventory using a dedicated application which stores CSV-Files in a SharePoint site. You want to limit access for the individually managed stores to a single folder for each store.

In this case you as an admin are the permission giver with full access. You need to create a dedicated application registration for each store to assign dedicated permissions to.

The process to prepare the individual upload application is:

  1. Create a new Application Registration for the restricted file upload
  2. Identify the path you want to make writeable (Library, Folder or Item)
  3. Assign the write permission on the path to the created App Registration

In this example we have FullControl on SharePoint as admin. Then we can do the following in PowerShell Graph (check out my GitHub for a more detailed explanation of the involved steps):

# Connect with high privileged app (e.g. Sites.FullControll.All + Application.ReadWrite.OwnedBy)
Connect-MgGraph -Scopes "Application.ReadWrite.All" -TenantId "contoso.onmicrosoft.com"
$context = Get-MgContext

# Create new low-privileged app
$graphResourceId = "00000003-0000-0000-c000-000000000000"
$FilesSelectedOperationsSelected = @{
    Id   = "bd61925e-3bf4-4d62-bc0b-06b06c96d95c"
    Type = "Role"
}
$ResourceAccess = @{
    ResourceAppId  = $graphResourceId
    ResourceAccess = @($FilesSelectedOperationsSelected)
}
$AppName = "Restricted Inventory Upload - Store 653"

# Create low privileged app registration
$appRegistration = New-MgApplication -DisplayName $AppName `
    -Web @{ RedirectUris = "http://localhost"; } -AdditionalProperties @{}`
    -RequiredResourceAccess $ResourceAccess
Write-Host -ForegroundColor Cyan "App registration created with app ID" $appRegistration.AppId

# Create corresponding service principal
New-MgServicePrincipal -AppId $appRegistration.AppId -AdditionalProperties @{} | Out-Null
Write-Host -ForegroundColor Cyan "Service principal created"
Write-Host

# Generate admin consent URL
$adminConsentUrl = "https://login.microsoftonline.com/" + $context.TenantId + "/adminconsent?client_id=" `
    + $appRegistration.AppId
Write-Host -ForegroundColor Yellow "Please go to the following URL in your browser to provide admin consent"
Write-Host $adminConsentUrl
Write-Host

# As of version 2.32 the Microsoft.Graph.Beta cmdlets are still required to assign Permissions on Lists, ListItems and Files. Install if necessary
# Install-PSResource Microsoft.Graph.Beta

$siteId = "contoso.sharepoint.com,5747c09b-c9e3-43f9-ad9d-518d08b73533,20bd8c53-bc8e-4a6c-8bfc-ef5cec62805e"
$driveId = "b!m8BHV-PJ-UOtnVGNCLc1M1OMvSCOvGxKi_zvXOxigF5YWDUb_H9USKNZDcOFTq09"

$FolderPath = "root:/Inventory/Store 653"
$driveItemID = (Get-MgDriveItem -DriveId $driveId -DriveItemId $FolderPath).Id

# Configure write Permission
$params = @{
    roles = @(
        "write"
    )
    grantedTo = @{
        application = @{
            id = "$($appRegistration.AppId)"
        }
    }
}

# Assign write permission to low privileged app
New-MgBetaDriveItemPermission -DriveId $driveId -DriveItemId $driveItemID -BodyParameter $params

Get-MgBetaDriveItemPermission -DriveId $driveId -DriveItemId $driveItemID

With these steps we have successfully created a low privileged app which can only read and write to a single SharePoint library under the folder path Inventory/Store 653. To complete our example, we just need a script which uploads inventory to the given path:

# Connect with low privileged app with Files.SelectedOperations.Selected Permission
Connect-MgGraph -ClientId 99391266-3b04-4293-a5d7-3836de018b39 -CertificateThumbprint 8D2734DE76F3ADD1552D7E17BCD76BF1DD3D539A -TenantId "contoso.onmicrosoft.com"

$sourceFileLocation = "C:\Inventory\Inventory.csv"

$driveId = "b!m8BHV-PJ-UOtnVGNCLc1M1OMvSCOvGxKi_zvXOxigF5YWDUb_H9USKNZDcOFTq09"
$FolderPath = "root:/Inventory/Store 653"
$targetFileLocation = "$FolderPath/Inventory.csv:"

$file = Get-ChildItem $sourceFileLocation

$uploadedResponse = Set-MgDriveItemContent -DriveId $driveId -DriveItemId $targetFileLocation -InFile $file.FullName -ErrorAction Stop

Get-MgDriveItem -DriveId $driveId -DriveItemId $FolderPath

That’s it. Our low privileged app can successfully upload inventory to SharePoint but cannot even access inventory information of other stores, as access is restricted to the path.

Conclusion

With some extra effort we can effectively restrict application access further than just on a site level. The provided script gives you a starting point for your own adaptations. Further restricting access might prove especially useful for larger sites with many different lists and libraries.

Even though the Graph API did not change for these commands in the past 6 months, we have yet to see a GA version of giving permissions in these scopes.