MSGraphPermissions.psm1
|
function Add-ClaimToResource { <# .SYNOPSIS Adds a permission claim to a resource's method/scheme collection. .DESCRIPTION Internal helper function that adds a permission claim to the nested hashtable structure representing a Graph API resource. Ensures the proper hierarchy of Method -> Scheme -> Claims is maintained and initializes collections as needed. This function is used during the permissions index building process to organize permission data by HTTP method and authentication scheme. .PARAMETER Resource The resource hashtable object to modify. Must have a Methods property (hashtable). .PARAMETER Method The HTTP method (GET, POST, PUT, PATCH, DELETE, etc.) for this claim. .PARAMETER Scheme The authentication scheme (DelegatedWork, DelegatedPersonal, Application). .PARAMETER Claim The claim object to add, containing permission details (Permission, Least, AlsoRequires). .EXAMPLE Add-ClaimToResource -Resource $resource -Method 'GET' -Scheme 'DelegatedWork' -Claim $claim Adds a claim to the resource's nested method and scheme structure. .NOTES This is an internal function not exported from the module. It mutates the Resource parameter by adding the claim to the appropriate nested collection. #> param( [Parameter(Mandatory)] $Resource, [Parameter(Mandatory)] [string]$Method, [Parameter(Mandatory)] [string]$Scheme, [Parameter(Mandatory)] $Claim ) if (-not $Resource.Methods.ContainsKey($Method)) { $Resource.Methods[$Method] = @{} } if (-not $Resource.Methods[$Method].ContainsKey($Scheme)) { $Resource.Methods[$Method][$Scheme] = [System.Collections.Generic.List[object]]::new() } $Resource.Methods[$Method][$Scheme].Add($Claim) } function Build-PermissionsIndex { <# .SYNOPSIS Builds an indexed lookup structure from raw Microsoft Graph permissions data. .DESCRIPTION Internal function that transforms the raw permissions JSON data from Microsoft Graph into an optimized hashtable index for fast lookups. The index maps API paths to ProtectedResource objects containing all available permissions organized by HTTP method and authentication scheme. This function processes the complex nested structure of the permissions data: - Iterates through all permissions and their path sets - Parses "least=" metadata to identify least privileged permissions - Parses "alsoRequires=" metadata for permission dependencies - Creates claims for each method/scheme/path combination - Normalizes paths to lowercase for case-insensitive lookups - Builds a nested structure: Path -> Method -> Scheme -> Claims The resulting index enables O(1) lookups by path for the module's query functions. .PARAMETER PermissionsData The raw permissions data hashtable deserialized from the Microsoft Graph JSON file. Must contain a 'permissions' property with the permission definitions. .OUTPUTS Hashtable Returns a hashtable indexed by normalized (lowercase) API paths, where each value is a ProtectedResource object with Methods, Schemes, and Claims organized for efficient querying. .EXAMPLE $index = Build-PermissionsIndex -PermissionsData $rawData Builds a searchable index from raw Graph permissions data. .EXAMPLE $index = Build-PermissionsIndex -PermissionsData $jsonData # Builds indexed structure from raw permissions data .NOTES This is an internal function not exported from the module. It is called by Initialize-GraphPermissions during the cache initialization process. The function processes 6000+ API paths and tens of thousands of permission combinations, so performance is important. The indexed structure allows Find-GraphLeastPrivilege and Get-GraphPermissions to run efficiently. #> [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory)] [object]$PermissionsData ) $index = [System.Collections.Generic.Dictionary[string, object]]::new([StringComparer]::OrdinalIgnoreCase) foreach ($permissionName in $PermissionsData.permissions.Keys) { $permission = $PermissionsData.permissions[$permissionName] foreach ($pathSet in $permission.pathSets) { $schemeKeys = $pathSet.schemeKeys $methods = $pathSet.methods $paths = $pathSet.paths foreach ($pathKey in $paths.Keys) { $pathValue = $paths[$pathKey] # Get the "least=" metadata $leastPrivilegeSchemes = Get-LeastPrivilegeScheme $pathValue # Normalize path $normalizedPath = $pathKey.ToLowerInvariant() if (-not $index.ContainsKey($normalizedPath)) { $index[$normalizedPath] = New-ProtectedResource -Path $normalizedPath } $resource = $index[$normalizedPath] foreach ($method in $methods) { foreach ($schemeKey in $schemeKeys) { $isLeast = $leastPrivilegeSchemes -contains $schemeKey # Parse alsoRequires if present $alsoRequires = @() if ($pathValue -match 'alsoRequires=([^,\s]+)') { $alsoRequires = $matches[1] -split '\|' } $claim = New-AcceptableClaim -Permission $permissionName -Least $isLeast -AlsoRequires $alsoRequires -Scheme $schemeKey Add-ClaimToResource -Resource $resource -Method $method -Scheme $schemeKey -Claim $claim } } } } } return $index } function Format-PermissionString { <# .SYNOPSIS Formats a permission claim into a human-readable string. .DESCRIPTION Internal helper function that converts a permission claim object into a formatted string representation. If the claim has dependencies (AlsoRequires), they are included in the output joined with "and". Examples: - Simple permission: "Mail.Read" - With dependencies: "Mail.Read and Calendars.Read and Contacts.Read" .PARAMETER Claim The claim object containing Permission and optionally AlsoRequires properties. .OUTPUTS String Returns a formatted permission string with any dependencies included. .EXAMPLE Format-PermissionString -Claim $claim Returns formatted permission string like 'Mail.Read and User.Read'. .NOTES This is an internal function not exported from the module. It's used for formatting permission output in a consistent, readable way. #> param([Parameter(Mandatory)]$Claim) if ($Claim.AlsoRequires -and $Claim.AlsoRequires.Count -gt 0) { return "$($Claim.Permission) and $($Claim.AlsoRequires -join ' and ')" } return $Claim.Permission } function Get-LeastPrivilegeScheme { <# .SYNOPSIS Gets the least privileged scheme identifiers from Microsoft Graph path value metadata. .DESCRIPTION Internal function that retrieves authentication scheme identifiers from the "least=" metadata embedded in Microsoft Graph permissions data path values. This metadata indicates which authentication schemes are considered least privileged for a specific permission on a specific path. The "least=" format supports comma-separated lists of scheme identifiers, for example: - "least=DelegatedWork" - Single scheme marked as least privileged - "least=DelegatedWork,Application" - Multiple schemes marked as least privileged - No "least=" metadata - Returns empty array (no explicit least privileged marking) This extraction is a critical part of the index building process, as it determines which permissions get marked with Least = $true in the claim objects. .PARAMETER PathValue The raw path value string from the Microsoft Graph permissions JSON, potentially containing "least=" metadata. Can be empty string. .OUTPUTS String[] Returns an array of scheme identifiers that are marked as least privileged. Returns empty array if no "least=" metadata is found. .EXAMPLE Get-LeastPrivilegeScheme -PathValue "least=DelegatedWork" Returns: @("DelegatedWork") .EXAMPLE Get-LeastPrivilegeScheme -PathValue "least=DelegatedWork,Application" Returns: @("DelegatedWork", "Application") .EXAMPLE Get-LeastPrivilegeScheme -PathValue "" Returns: @() .NOTES This is an internal function not exported from the module. It's called by Build-PermissionsIndex during the permissions data processing phase. The regex pattern matches "least=" followed by one or more scheme names separated by commas, stopping at the next comma or whitespace to handle cases where multiple metadata attributes are present. #> [CmdletBinding()] [OutputType([string[]])] param( [Parameter(Mandatory)] [AllowEmptyString()] [string]$PathValue ) if ($PathValue -match 'least=([^,\s]+(?:,[^,\s]+)*)') { return $matches[1] -split ',' } # Default: no schemes marked as least privileged return @() } function Get-ResourceLeastPrivileged { <# .SYNOPSIS Extracts least privileged permissions from a resource object. .DESCRIPTION Internal function that filters and extracts only the least privileged permissions from a ProtectedResource object. It processes the nested Method -> Scheme -> Claims structure and returns only permissions marked as least privileged. The function implements smart fallback logic: - Prefers claims explicitly marked with Least = $true - If no explicit least privileged claim exists but only one permission is available, treats that single permission as implicitly least privileged - Calls Invoke-Disambiguate when multiple least privileged options exist to handle edge cases and select the most appropriate permission Supports optional filtering by HTTP method and/or authentication scheme to narrow results to specific scenarios. .PARAMETER Resource The ProtectedResource object containing the full set of permissions organized by method and scheme. .PARAMETER Method Optional HTTP method to filter results (GET, POST, PUT, PATCH, DELETE, etc.). If not specified, returns least privileged permissions for all methods. .PARAMETER Scheme Optional authentication scheme to filter results (DelegatedWork, DelegatedPersonal, Application). .EXAMPLE Get-ResourceLeastPrivileged -Resource $resource -Method 'GET' -Scheme 'DelegatedWork' Returns the least privileged permissions for the specified method and scheme. If not specified, returns least privileged permissions for all schemes. .OUTPUTS Hashtable Returns a nested hashtable structure: Method -> Scheme -> List[String] of permission names. Only includes methods/schemes that have least privileged permissions available. .NOTES This is an internal function not exported from the module. It is called by Find-GraphLeastPrivilege to extract the minimal permissions from cached data. The implicit least privilege fallback (single permission treated as least privileged) handles cases where Microsoft's metadata doesn't explicitly mark a permission as "least" but it's the only option available. .EXAMPLE Get-ResourceLeastPrivileged -Resource $resource -Method 'GET' -Scheme 'DelegatedWork' # Extracts least privileged permissions from resource #> param( [Parameter(Mandatory)] $Resource, [string]$Method, [string]$Scheme ) $result = @{} if ($Method -and $Resource.Methods.ContainsKey($Method)) { $methodData = @{ $Method = $Resource.Methods[$Method] } } elseif ($Method) { return $result } else { $methodData = $Resource.Methods } foreach ($m in $methodData.Keys) { if ($Scheme -and $methodData[$m].ContainsKey($Scheme)) { $schemes = @{ $Scheme = $methodData[$m][$Scheme] } } elseif ($Scheme) { continue } else { $schemes = $methodData[$m] } foreach ($s in $schemes.Keys) { $claims = $schemes[$s] | Where-Object { $_.Least } # If no explicit least privileged permissions, check if there's only one permission total if (-not $claims) { $allClaims = $schemes[$s] if ($allClaims.Count -eq 1) { # Only one permission exists, treat it as implicitly least privileged $claims = $allClaims } elseif ($allClaims.Count -eq 2) { # Exactly 2 permissions - check if they follow X.Read.All and X.ReadWrite.All pattern $permissions = $allClaims | Select-Object -ExpandProperty Permission $readPerm = $permissions | Where-Object { $_ -match '^(.+)\.Read\.All$' } $readWritePerm = $permissions | Where-Object { $_ -match '^(.+)\.ReadWrite\.All$' } if ($readPerm -and $readWritePerm) { # Extract prefix before .Read.All and .ReadWrite.All $readPerm -match '^(.+)\.Read\.All$' | Out-Null $readPrefix = $Matches[1] $readWritePerm -match '^(.+)\.ReadWrite\.All$' | Out-Null $readWritePrefix = $Matches[1] # If prefixes match, the Read permission is less privileged if ($readPrefix -eq $readWritePrefix) { $claims = $allClaims | Where-Object { $_.Permission -eq $readPerm } } } # If pattern didn't match, fall through to path count logic below if (-not $claims) { # Multiple permissions exist with no explicit least privilege marking # Select the permission(s) with the fewest paths (least scope) $permissionPathCounts = @{} foreach ($claim in $allClaims) { $permName = $claim.Permission if (-not $permissionPathCounts.ContainsKey($permName)) { # Count paths for this permission across the entire permissions data $pathCount = 0 if ($script:PermissionsData -and $script:PermissionsData.permissions.ContainsKey($permName)) { $permData = $script:PermissionsData.permissions[$permName] foreach ($pathSet in $permData.pathSets) { $pathCount += $pathSet.paths.Count } } $permissionPathCounts[$permName] = $pathCount } } # Find minimum path count $minPathCount = ($permissionPathCounts.Values | Measure-Object -Minimum).Minimum # Select all permissions with the minimum path count $leastBroadPermissions = $permissionPathCounts.GetEnumerator() | Where-Object { $_.Value -eq $minPathCount } | Select-Object -ExpandProperty Key # Filter claims to only include those with minimum path count $claims = $allClaims | Where-Object { $_.Permission -in $leastBroadPermissions } } } elseif ($allClaims.Count -gt 2) { # Multiple permissions exist with no explicit least privilege marking # First, filter out ReadWrite permissions when corresponding Read permissions exist $permissions = $allClaims | Select-Object -ExpandProperty Permission $readPermissions = $permissions | Where-Object { $_ -match '^(.+)\.Read\.All$' } $filteredClaims = $allClaims | Where-Object { $perm = $_.Permission # Keep if it's not a ReadWrite permission, or if it is but no Read equivalent exists if ($perm -match '^(.+)\.ReadWrite\.All$') { $prefix = $Matches[1] $readEquivalent = "$prefix.Read.All" # Exclude if Read version exists return $readEquivalent -notin $readPermissions } return $true } Write-Verbose "Filtered from $($allClaims.Count) to $($filteredClaims.Count) permissions after removing ReadWrite variants" # Select the permission(s) with the fewest paths (least scope) $permissionPathCounts = @{} foreach ($claim in $filteredClaims) { $permName = $claim.Permission if (-not $permissionPathCounts.ContainsKey($permName)) { # Count paths for this permission across the entire permissions data $pathCount = 0 if ($script:PermissionsData -and $script:PermissionsData.permissions.ContainsKey($permName)) { $permData = $script:PermissionsData.permissions[$permName] foreach ($pathSet in $permData.pathSets) { $pathCount += $pathSet.paths.Count } } $permissionPathCounts[$permName] = $pathCount Write-Verbose "$permName has $pathCount total paths" } } # Find minimum path count $minPathCount = ($permissionPathCounts.Values | Measure-Object -Minimum).Minimum Write-Verbose "Minimum path count is $minPathCount" # Select all permissions with the minimum path count $leastBroadPermissions = $permissionPathCounts.GetEnumerator() | Where-Object { $_.Value -eq $minPathCount } | Select-Object -ExpandProperty Key # Filter claims to only include those with minimum path count $claims = $filteredClaims | Where-Object { $_.Permission -in $leastBroadPermissions } } } if ($claims) { $disambiguated = Invoke-Disambiguate -Resource $Resource -Method $m -Scheme $s -Claims $claims if (-not $result.ContainsKey($m)) { $result[$m] = @{} } if (-not $result[$m].ContainsKey($s)) { $result[$m][$s] = [System.Collections.Generic.List[string]]::new() } foreach ($perm in $disambiguated) { $result[$m][$s].Add($perm) } } } } return $result } function Get-SupportedMethods { <# .SYNOPSIS Determines which HTTP methods a permission supports for a given resource and scheme. .DESCRIPTION Internal helper function that searches through a resource's method/scheme structure to identify all HTTP methods where a specific permission claim is valid. This is used during disambiguation to understand the scope of a permission's applicability. For example, if Mail.Read supports both GET and POST methods but Mail.ReadBasic only supports GET, this function helps identify those differences. .PARAMETER Resource The ProtectedResource object containing all permissions organized by method and scheme. .PARAMETER Claim The claim object to search for, containing at minimum a Permission property. .PARAMETER Scheme The authentication scheme to check within (DelegatedWork, DelegatedPersonal, Application). .OUTPUTS String[] Returns an array of HTTP method names (GET, POST, etc.) where the claim is valid for the specified scheme. Returns an empty array if the permission is not found. .EXAMPLE Get-SupportedMethods -Resource $resource -Claim $claim -Scheme 'DelegatedWork' Returns array of HTTP methods supported by this permission. .NOTES This is an internal function not exported from the module. It's used by Invoke-Disambiguate to help select between multiple least privileged options based on their method coverage. .EXAMPLE Get-SupportedMethods -Resource $resource -Claim $claim -Scheme 'DelegatedWork' # Returns array of HTTP methods supported by this permission #> param( [Parameter(Mandatory)] $Resource, [Parameter(Mandatory)] $Claim, [Parameter(Mandatory)] [string]$Scheme ) $methods = [System.Collections.Generic.List[string]]::new() foreach ($m in $Resource.Methods.Keys) { if ($Resource.Methods[$m].ContainsKey($Scheme)) { $schemeClaims = $Resource.Methods[$m][$Scheme] if ($schemeClaims | Where-Object { $_.Permission -eq $Claim.Permission }) { $methods.Add($m) } } } return $methods.ToArray() } function Invoke-Disambiguate { <# .SYNOPSIS Selects the most appropriate permission when multiple least privileged options exist. .DESCRIPTION Internal function that implements disambiguation logic to choose between multiple permissions that are all marked as least privileged for a given endpoint. This handles edge cases in the Microsoft Graph permissions metadata where multiple permissions are equally valid. The disambiguation algorithm uses a three-tier strategy: 1. Single Option: If only one claim exists, return it immediately 2. Exclusive Method Match: Prefer permissions that only work for the specific HTTP method being queried (most specific scope) 3. Narrowest Scope: Choose the permission that supports the fewest total methods across the resource (most restrictive permission) 4. First Match Fallback: If all else fails, return the first claim This ensures that when multiple "least privileged" options exist, the most contextually appropriate one is selected based on the specific method being used. .PARAMETER Resource The ProtectedResource object containing all permissions for the endpoint. .PARAMETER Method The HTTP method being queried (GET, POST, PUT, PATCH, DELETE, etc.). .PARAMETER Scheme The authentication scheme (DelegatedWork, DelegatedPersonal, Application). .PARAMETER Claims Array of claim objects that are all marked as least privileged and need to be disambiguated. .OUTPUTS String[] Returns an array containing a single formatted permission string (may include "and" clauses if the permission has dependencies via AlsoRequires). .NOTES This is an internal function not exported from the module. It's called by Get-ResourceLeastPrivileged when multiple least privileged permissions exist. The algorithm prioritizes context-specific permissions (exclusive to the method) over broader permissions that work across multiple methods, following the principle of least privilege most strictly. .EXAMPLE Invoke-Disambiguate -Resource $resource -Method 'GET' -Scheme 'DelegatedWork' -Claims $claims Selects the most appropriate permission from multiple least privileged candidates. #> param( [Parameter(Mandatory)] $Resource, [Parameter(Mandatory)] [string]$Method, [Parameter(Mandatory)] [string]$Scheme, [Parameter(Mandatory)] [array]$Claims ) if ($Claims.Count -eq 1) { return @(Format-PermissionString -Claim $Claims[0]) } # Try to find permissions exclusive to this method foreach ($claim in $Claims) { $supportedMethods = Get-SupportedMethods -Resource $Resource -Claim $claim -Scheme $Scheme if ($supportedMethods.Count -eq 1 -and $supportedMethods[0] -eq $Method) { return @(Format-PermissionString -Claim $claim) } } # Find permission with fewest supported methods (narrowest scope) $minMethodCount = [int]::MaxValue $bestClaim = $null foreach ($claim in $Claims) { $supportedMethods = Get-SupportedMethods -Resource $Resource -Claim $claim -Scheme $Scheme if ($supportedMethods.Count -lt $minMethodCount) { $minMethodCount = $supportedMethods.Count $bestClaim = $claim } } if ($bestClaim) { return @(Format-PermissionString -Claim $bestClaim) } return @(Format-PermissionString -Claim $Claims[0]) } function New-AcceptableClaim { <# .SYNOPSIS Creates a new AcceptableClaim object representing a permission claim. .DESCRIPTION Internal factory function that creates a standardized PSCustomObject representing a permission claim with all necessary metadata. The claim object tracks: - The permission name - Whether it's marked as least privileged - Any additional required permissions (dependencies) - The authentication scheme it applies to The object includes a custom type name (GraphPermissions.AcceptableClaim) for type identification and potential future formatting customization. .PARAMETER Permission The name of the Graph API permission (e.g., "Mail.Read", "User.ReadBasic.All"). .PARAMETER Least Boolean indicating whether this permission is explicitly marked as least privileged in the Microsoft Graph metadata. .PARAMETER AlsoRequires Array of additional permission names that must be granted alongside this permission for it to function. Most permissions don't have dependencies and will be empty. .PARAMETER Scheme The authentication scheme this claim applies to (DelegatedWork, DelegatedPersonal, Application). .OUTPUTS PSCustomObject Returns a claim object with properties: Permission, Least, AlsoRequires, Scheme, and PSTypeName. .EXAMPLE New-AcceptableClaim -Permission 'Mail.Read' -Least $true -AlsoRequires @() -Scheme 'DelegatedWork' Creates a new permission claim object with the specified properties. New-AcceptableClaim -Permission 'Mail.Read' -Least $true -AlsoRequires @() -Scheme 'DelegatedWork' # Creates a new claim object for a least privileged permission .NOTES This is an internal function not exported from the module. It's used during the permissions index building process to create standardized claim objects. #> param( [string]$Permission, [bool]$Least, [string[]]$AlsoRequires, [string]$Scheme ) return [PSCustomObject]@{ Permission = $Permission Least = $Least AlsoRequires = $AlsoRequires Scheme = $Scheme PSTypeName = 'GraphPermissions.AcceptableClaim' } } function New-ProtectedResource { <# .SYNOPSIS Creates a new ProtectedResource object representing a Graph API endpoint. .DESCRIPTION Internal factory function that creates a standardized PSCustomObject representing a Microsoft Graph API resource (endpoint). The resource object serves as a container for organizing all permissions associated with an API path. The Methods property is initialized as an empty hashtable that will be populated with nested structures: Method -> Scheme -> Claims during the index building process. The object includes a custom type name (GraphPermissions.ProtectedResource) for type identification and potential future formatting customization. .PARAMETER Path The normalized (lowercase) API path for this resource (e.g., "/me/messages", "/users/{id}"). .OUTPUTS PSCustomObject Returns a resource object with properties: Path, Methods (empty hashtable), and PSTypeName. .EXAMPLE New-ProtectedResource -Path '/me/messages' Creates a new resource container for the /me/messages endpoint. .NOTES This is an internal function not exported from the module. It's used by Build-PermissionsIndex to create resource containers as API paths are discovered in the permissions data. #> param([string]$Path) return [PSCustomObject]@{ Path = $Path Methods = @{} PSTypeName = 'GraphPermissions.ProtectedResource' } } function Find-GraphLeastPrivilege { <# .SYNOPSIS Finds the least privileged permission(s) required for a Microsoft Graph API endpoint. .DESCRIPTION The Find-GraphLeastPrivilege function identifies the minimal permissions needed to access a specific Microsoft Graph API endpoint. It queries the permissions cache (automatically initialized if needed) and returns only those permissions explicitly marked as least privileged in the Microsoft Graph permissions metadata. This function helps implement the principle of least privilege by identifying the minimum permission scope required for your application to function. .PARAMETER Path The Microsoft Graph API path to query. Path matching is case-insensitive. Use {id} placeholders for dynamic segments (e.g., "/users/{id}/messages"). This parameter accepts pipeline input, allowing you to query multiple paths at once. .PARAMETER Method The HTTP method to filter by. Valid values are: GET, POST, PUT, PATCH, DELETE If not specified, returns least privileged permissions for all available methods on the path. .PARAMETER Scheme The authentication scheme to filter by. Valid values are: - DelegatedWork: Delegated permissions for work/school accounts - DelegatedPersonal: Delegated permissions for personal Microsoft accounts - Application: Application permissions (app-only access) If not specified, returns least privileged permissions for all available schemes. .EXAMPLE ``` Find-GraphLeastPrivilege -Path "/me/messages" -Method GET -Scheme DelegatedWork Returns the least privileged permission needed to read the current user's messages using delegated work/school account permissions. Output: Path Method Scheme Permission ---- ------ ------ ---------- /me/messages GET DelegatedWork Mail.ReadBasic ``` .EXAMPLE ``` Find-GraphLeastPrivilege -Path "/users/{id}/messages" -Method GET Returns the least privileged permissions for reading a user's messages across all authentication schemes. Output: Path Method Scheme Permission ---- ------ ------ ---------- /users/{id}/messages GET Application Mail.ReadBasic.All /users/{id}/messages GET DelegatedWork Mail.ReadBasic /users/{id}/messages GET DelegatedPersonal Mail.ReadBasic ``` .EXAMPLE ``` Find-GraphLeastPrivilege -Path "/me/messages" Returns least privileged permissions for all HTTP methods and schemes available for the /me/messages endpoint. ``` .EXAMPLE ``` "/me/messages", "/me/calendar/events" | Find-GraphLeastPrivilege -Method GET -Scheme DelegatedWork Demonstrates pipeline usage to query multiple endpoints at once. Returns the least privileged delegated work permission for reading messages and calendar events. Output: Path Method Scheme Permission ---- ------ ------ ---------- /me/messages GET DelegatedWork Mail.ReadBasic /me/calendar/events GET DelegatedWork Calendars.ReadBasic ``` .EXAMPLE ``` $paths = @("/users/{id}", "/groups/{id}", "/applications") $paths | Find-GraphLeastPrivilege -Method GET -Scheme Application | Select-Object Path, Permission Queries multiple paths and displays only the path and permission, useful for generating permission requirement documentation. ``` .OUTPUTS PSCustomObject Returns objects with the following properties: - Path: The API path queried - Method: The HTTP method - Scheme: The authentication scheme - Permission: The least privileged permission name .NOTES - If a path is not found, a warning is displayed and no output is returned for that path - If no least privileged permissions are defined for the specified method/scheme combination, a warning is displayed - The permissions cache is automatically initialized on first use by calling Initialize-GraphPermissions - To refresh the permissions data, run: Initialize-GraphPermissions -Force .LINK https://mynster9361.github.io/MSGraphPermissions/docs/MSGraphPermissions/Find-GraphLeastPrivilege.html #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [ValidatePattern('^/.*')] [string]$Path, [ValidateSet('GET', 'POST', 'PUT', 'PATCH', 'DELETE')] [string]$Method, [ValidateSet('DelegatedWork', 'DelegatedPersonal', 'Application')] [string]$Scheme ) begin { if (-not $script:PermissionsCache) { Initialize-GraphPermissions } } process { $normalizedPath = $Path.ToLowerInvariant() if (-not $script:PermissionsCache.ContainsKey($normalizedPath)) { Write-Warning "Path '$Path' not found in permissions data" return } $resource = $script:PermissionsCache[$normalizedPath] $leastPrivileged = Get-ResourceLeastPrivileged -Resource $resource -Method $Method -Scheme $Scheme if ($leastPrivileged.Count -eq 0) { Write-Warning "No least privileged permissions found for path '$Path'" return } foreach ($m in $leastPrivileged.Keys) { foreach ($s in $leastPrivileged[$m].Keys) { foreach ($perm in $leastPrivileged[$m][$s]) { [PSCustomObject]@{ Path = $Path Method = $m Scheme = $s Permission = $perm } } } } } } function Find-GraphPath { <# .SYNOPSIS Searches for Microsoft Graph API paths matching a wildcard pattern. .DESCRIPTION The Find-GraphPath function searches through all available Microsoft Graph API paths in the permissions cache and returns those that match the specified wildcard pattern. This is useful for discovering available endpoints, exploring the API surface, or finding related endpoints by naming patterns. The function performs case-insensitive pattern matching using PowerShell's -like operator, supporting standard wildcards (* and ?). .PARAMETER Pattern A wildcard pattern to match against API paths. Pattern matching is case-insensitive. Supports PowerShell wildcard syntax: - * matches zero or more characters - ? matches exactly one character Examples: - "*messages*" finds all paths containing "messages" - "/me/*" finds all paths under /me - "/users/{id}/mail*" finds mail-related endpoints under users .EXAMPLE ``` Find-GraphPath -Pattern "*messages*" Finds all API paths containing the word "messages" anywhere in the path. Output: Path Methods ---- ------- /me/messages POST, GET /users/{id}/messages POST, GET /me/mailfolders/{id}/messages POST, GET /chats/{id}/messages POST, GET ``` .EXAMPLE ``` Find-GraphPath -Pattern "/me/*" Finds all API paths directly under the /me endpoint. Returns hundreds of paths showing all available operations for the current user context. ``` .EXAMPLE ``` Find-GraphPath -Pattern "*accessreviews*" Discovers all access review-related endpoints across the API. Output: Path Methods ---- ------- /accessreviews POST, GET /accessreviews/{id} DELETE, PATCH, GET /identitygovernance/accessreviews/definitions POST, GET /identitygovernance/accessreviews/policy PATCH, GET ``` .EXAMPLE ``` Find-GraphPath -Pattern "/users/{id}/mail*" | Format-Table -AutoSize Finds all mail-related endpoints for a specific user and formats the output as a compact table. ``` .EXAMPLE ``` $calendarPaths = Find-GraphPath -Pattern "*calendar*" $calendarPaths | Select-Object -First 10 Finds all calendar-related paths and displays the first 10 results. ``` .EXAMPLE ``` Find-GraphPath -Pattern "/identitygovernance/lifecycleworkflows/workflows*" | Measure-Object | Select-Object -ExpandProperty Count Counts how many workflow-related endpoints exist under lifecycle workflows. ``` .OUTPUTS PSCustomObject Returns objects with the following properties: - Path: The API path that matched the pattern - Methods: Comma-separated list of HTTP methods available for this path .NOTES - Pattern matching is case-insensitive - The permissions cache is automatically initialized on first use - To refresh the permissions data, run: Initialize-GraphPermissions -Force - Use wildcards strategically to narrow down results, as some patterns may return hundreds of paths (e.g., "/me/*" returns 1000+ paths) - The Methods property shows all available HTTP methods; use Find-GraphLeastPrivilege to determine required permissions for specific methods .LINK https://mynster9361.github.io/MSGraphPermissions/docs/MSGraphPermissions/Find-GraphPath.html #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Pattern ) if (-not $script:PermissionsCache) { Initialize-GraphPermissions } $normalizedPattern = $Pattern.ToLowerInvariant() $script:PermissionsCache.Keys | Where-Object { $_ -like $normalizedPattern } | ForEach-Object { [PSCustomObject]@{ Path = $_ Methods = ($script:PermissionsCache[$_].Methods.Keys -join ', ') } } } function Get-GraphPermissions { <# .SYNOPSIS Retrieves all permissions (including non-least privileged) for a Microsoft Graph API endpoint. .DESCRIPTION The Get-GraphPermissions function returns comprehensive permission information for a Microsoft Graph API endpoint, including all available permissions regardless of whether they are marked as least privileged. This provides a complete view of all permissions that can access an endpoint. Unlike Find-GraphLeastPrivilege which only returns minimal permissions, this function shows every permission that grants access, along with indicators showing which are least privileged and what additional permissions they may require. Use this function when you need to: - Understand the full permission landscape for an endpoint - See what higher-privileged alternatives exist - Audit existing permissions against available options - Understand permission dependencies (AlsoRequires) .PARAMETER Path The Microsoft Graph API path to query. Path matching is case-insensitive. Use {id} placeholders for dynamic segments (e.g., "/users/{id}/messages"). This parameter accepts pipeline input, allowing you to query multiple paths at once. .PARAMETER Method The HTTP method to filter by. Valid values are: GET, POST, PUT, PATCH, DELETE If not specified, returns permissions for all available methods on the path. .PARAMETER Scheme The authentication scheme to filter by. Valid values are: - DelegatedWork: Delegated permissions for work/school accounts - DelegatedPersonal: Delegated permissions for personal Microsoft accounts - Application: Application permissions (app-only access) If not specified, returns permissions for all available schemes. .EXAMPLE ``` Get-GraphPermissions -Path "/users/{id}" -Method GET Returns all permissions (least privileged and higher) that can be used to read a user object, across all authentication schemes. Output shows IsLeastPrivileged column to identify minimal permissions: Path Method Scheme Permission IsLeastPrivileged ---- ------ ------ ---------- ----------------- /users/{id} GET Application User.Read.All False /users/{id} GET Application User.ReadBasic.All True /users/{id} GET Application Directory.Read.All False ``` .EXAMPLE ``` Get-GraphPermissions -Path "/me/messages" -Method GET -Scheme DelegatedWork Returns all delegated work permissions that can read the current user's messages, showing both least privileged and broader permissions. Output: Path Method Scheme Permission IsLeastPrivileged ---- ------ ------ ---------- ----------------- /me/messages GET DelegatedWork Mail.ReadBasic True /me/messages GET DelegatedWork Mail.Read False /me/messages GET DelegatedWork Mail.ReadWrite False ``` .EXAMPLE ``` Get-GraphPermissions -Path "/users/{id}/messages" -Method GET | Where-Object { $_.IsLeastPrivileged } | Format-Table Permission, Scheme Gets all permissions for reading user messages, then filters to show only the least privileged options across all schemes. ``` .EXAMPLE ``` Get-GraphPermissions -Path "/me/calendar/events" -Method POST -Scheme Application | Select-Object Permission, IsLeastPrivileged, AlsoRequires Shows all application permissions that can create calendar events, including any additional permissions required (AlsoRequires column). ``` .EXAMPLE ``` "/me/messages", "/me/calendar" | Get-GraphPermissions -Method GET | Group-Object Permission | Sort-Object Count -Descending Compares permissions across multiple endpoints to identify which permissions grant access to multiple resources. ``` .EXAMPLE ``` Get-GraphPermissions -Path "/groups/{id}/members" -Method GET | Format-Table Scheme, Permission, IsLeastPrivileged -GroupBy Scheme Displays permissions grouped by authentication scheme for better readability. ``` .OUTPUTS PSCustomObject Returns objects with the following properties: - Path: The API path queried - Method: The HTTP method - Scheme: The authentication scheme - Permission: The permission name - IsLeastPrivileged: Boolean indicating if this is a least privileged permission - AlsoRequires: Comma-separated list of additional required permissions (usually empty) .NOTES - Returns ALL permissions, not just least privileged ones - Use the IsLeastPrivileged property to identify minimal permissions - If a path is not found, a warning is displayed and no output is returned - The permissions cache is automatically initialized on first use - To refresh the permissions data, run: Initialize-GraphPermissions -Force - The AlsoRequires property indicates permission dependencies; most permissions don't have dependencies and will show an empty string .LINK https://mynster9361.github.io/MSGraphPermissions/docs/MSGraphPermissions/Get-GraphPermissions.html #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [string]$Path, [ValidateSet('GET', 'POST', 'PUT', 'PATCH', 'DELETE')] [string]$Method, [ValidateSet('DelegatedWork', 'DelegatedPersonal', 'Application')] [string]$Scheme ) begin { if (-not $script:PermissionsCache) { Initialize-GraphPermissions } } process { $normalizedPath = $Path.ToLowerInvariant() if (-not $script:PermissionsCache.ContainsKey($normalizedPath)) { Write-Warning "Path '$Path' not found in permissions data" return } $resource = $script:PermissionsCache[$normalizedPath] if ($Method -and $resource.Methods.ContainsKey($Method)) { $methodData = @{ $Method = $resource.Methods[$Method] } } elseif ($Method) { return } else { $methodData = $resource.Methods } foreach ($m in $methodData.Keys) { if ($Scheme -and $methodData[$m].ContainsKey($Scheme)) { $schemes = @{ $Scheme = $methodData[$m][$Scheme] } } elseif ($Scheme) { continue } else { $schemes = $methodData[$m] } foreach ($s in $schemes.Keys) { foreach ($claim in $schemes[$s]) { [PSCustomObject]@{ Path = $Path Method = $m Scheme = $s Permission = $claim.Permission IsLeastPrivileged = $claim.Least AlsoRequires = $claim.AlsoRequires -join ', ' } } } } } } function Initialize-GraphPermissions { <# .SYNOPSIS Downloads and initializes the Microsoft Graph permissions cache. .DESCRIPTION The Initialize-GraphPermissions function downloads the latest Microsoft Graph API permissions metadata from the official Microsoft Graph GitHub repository and builds an in-memory cache for fast lookups. The permissions data is automatically cached in memory after the first download, so subsequent calls are instantaneous. The cache persists for the duration of the PowerShell session. This function is automatically called by other module functions (Find-GraphLeastPrivilege, Get-GraphPermissions, Find-GraphPath) if the cache is not already initialized, so you typically don't need to call it explicitly unless you want to force a refresh. .PARAMETER Force Forces a fresh download of permissions data from the remote source, even if the cache is already populated. Use this to refresh the data with the latest permissions from Microsoft Graph. Without this switch, the function will use existing cached data if available. .EXAMPLE ``` Initialize-GraphPermissions Downloads permissions data if not already cached. If data is already in memory, does nothing and returns immediately. ``` .EXAMPLE ``` Initialize-GraphPermissions -Force Forces a fresh download of the latest permissions data, replacing any existing cache. Use this when you need to ensure you have the most recent permissions metadata. ``` .EXAMPLE ``` Initialize-GraphPermissions -Force -Verbose Forces a refresh and shows detailed progress information about the download and indexing process. ``` .OUTPUTS None This function does not return any output. It populates the internal module cache. .NOTES - Downloads from: https://raw.githubusercontent.com/microsoftgraph/microsoft-graph-devx-content/refs/heads/master/permissions/new/permissions.json - Data is cached in memory for the current PowerShell session only - Cache is automatically initialized by other module functions if needed - Use -Force to refresh data without restarting PowerShell - Requires internet connectivity to download permissions data - The permissions file is typically several MB in size - First download may take a few seconds depending on connection speed - Cached lookups are instantaneous after initialization .LINK https://mynster9361.github.io/MSGraphPermissions/docs/MSGraphPermissions/Initialize-GraphPermissions.html #> [CmdletBinding()] param( [switch]$Force ) if ($script:PermissionsCache -and -not $Force) { Write-Verbose "Using cached permissions data" return } $permissionsUrl = "https://raw.githubusercontent.com/microsoftgraph/microsoft-graph-devx-content/refs/heads/master/permissions/new/permissions.json" Write-Verbose "Downloading permissions from $permissionsUrl" try { $response = Invoke-WebRequest -Uri $permissionsUrl -ErrorAction Stop $jsonData = $response.Content | ConvertFrom-Json -AsHashtable $script:PermissionsData = $jsonData $script:PermissionsCache = Build-PermissionsIndex $jsonData Write-Verbose "Successfully loaded permissions data with $($script:PermissionsCache.Count) paths" } catch [System.Net.WebException] { throw "Network error downloading permissions. Check your internet connection: $_" } catch [System.ArgumentException] { throw "Invalid JSON format in permissions data: $_" } catch { throw "Failed to download permissions: $_" } } |