Modules/M365DSCPermissions.psm1
using namespace System.Management.Automation.Language <# .Description This function lists all Graph or Exchange permissions required for the specified resources, both for reading and updating. With the parameters, you can specify which permissions you are interested in. .Parameter ResourceNameList An array of resource names for which the permissions should be determined. .Parameter PermissionsType Specifies what type of Graph permissions need to get returned. .Parameter Source Specifies the workload of the permissions that need to get returned. .Example Get-M365DSCCompiledPermissionList -ResourceNameList @('O365User', 'AADApplication') -Source 'Graph' -PermissionsType 'Delegated' .Example Get-M365DSCCompiledPermissionList -ResourceNameList @('O365User', 'AADApplication') -Source 'Graph' -PermissionsType 'Application' .Example Get-M365DSCCompiledPermissionList -ResourceNameList @('EXOAcceptedDomain') -Source 'Exchange' .Functionality Public #> function Get-M365DSCCompiledPermissionList { [CmdletBinding(DefaultParametersetName = 'None')] [OutputType([System.Collections.Hashtable])] param ( [Parameter(Mandatory = $true, Position = 0)] [System.String[]] $ResourceNameList, [Parameter()] [ValidateSet('Delegated', 'Application')] [System.String] $PermissionsType = 'Delegated', [Parameter()] [ValidateSet('Exchange', 'Graph', 'SharePoint')] [System.String] $Source = 'Graph' ) switch ($Source) { { $_ -in @('Graph', 'SharePoint') } { Write-Verbose -Message "Retrieving $Source $PermissionsType Permissions" $results = @{ ReadPermissions = @() UpdatePermissions = @() } } 'Exchange' { Write-Verbose -Message "Retrieving Exchange Permissions" $results = @{ RequiredRoles = @() RequiredRoleGroups = @() } } } foreach ($resourceName in $ResourceNameList) { Write-Verbose -Message "Processing $resourceName" $settingsFilePath = $null try { $settingsFilePath = Join-Path -Path $PSScriptRoot ` -ChildPath "..\DSCResources\MSFT_$resourceName\settings.json" ` -Resolve ` -ErrorAction Stop } catch { Write-Host "File settings.json was not found for resource {$resourceName}" -ForegroundColor Red } if ($null -ne $settingsFilePath) { $fileContent = Get-Content $settingsFilePath -Raw $resourceSettings = ConvertFrom-Json -InputObject $fileContent if ($null -eq $resourceSettings.permissions) { Write-Warning "Error in reading permissions. Missing permissions node in settings.json for $resourceName." continue } switch ($Source) { 'Graph' { if ($null -ne $resourceSettings.permissions.graph) { # Update permissions foreach ($updatePermission in $resourceSettings.permissions.graph.$PermissionsType.update) { if (-not $results.UpdatePermissions.Contains($updatePermission.name)) { Write-Verbose -Message "Found new Update permission {$($updatePermission.name)}" $results.UpdatePermissions += $updatePermission.name } else { Write-Verbose -Message "Update permission {$($updatePermission.name)} was already added" } } # Read permissions foreach ($readPermission in $resourceSettings.permissions.graph.$PermissionsType.read) { if (-not $results.ReadPermissions.Contains($readPermission.name)) { Write-Verbose -Message "Found new Read permission {$($readPermission.name)}" $results.ReadPermissions += $readPermission.name } else { Write-Verbose -Message "Read permission {$($readPermission.name)} was already added" } } } else { Write-Warning "Error in reading Graph permissions. Missing graph node in settings.json for $resourceName." continue } } 'Exchange' { if ($null -ne $resourceSettings.permissions.exchange) { # Required Role foreach ($requiredRole in $resourceSettings.permissions.exchange.requiredroles) { if (-not $results.RequiredRoles.Contains($requiredRole)) { Write-Verbose -Message "Found new Required Role {$($requiredRole)}" $results.RequiredRoles += $requiredRole } else { Write-Verbose -Message "Required Role {$($requiredRole)} was already added" } } # Required RoleGroups foreach ($requiredRoleGroup in $resourceSettings.permissions.exchange.requiredrolegroups) { if (-not $results.RequiredRoleGroups.Contains($requiredRoleGroup)) { Write-Verbose -Message "Found new Required Role Group {$($requiredRoleGroup)}" $results.RequiredRoleGroups += $requiredRoleGroup } else { Write-Verbose -Message "Required Role Group {$($requiredRoleGroup)} was already added" } } } else { Write-Warning "Error in reading Exchange permissions. Missing exchange node in settings.json." continue } } 'SharePoint' { if ($null -ne $resourceSettings.permissions.sharepoint) { # Update permissions foreach ($updatePermission in $resourceSettings.permissions.sharepoint.$PermissionsType.update) { if (-not $results.UpdatePermissions.Contains($updatePermission.name)) { Write-Verbose -Message "Found new Update permission {$($updatePermission.name)}" $results.UpdatePermissions += $updatePermission.name } else { Write-Verbose -Message "Update permission {$($updatePermission.name)} was already added" } } # Read permissions foreach ($readPermission in $resourceSettings.permissions.sharepoint.$PermissionsType.read) { if (-not $results.ReadPermissions.Contains($readPermission.name)) { Write-Verbose -Message "Found new Read permission {$($readPermission.name)}" $results.ReadPermissions += $readPermission.name } else { Write-Verbose -Message "Read permission {$($readPermission.name)} was already added" } } } else { Write-Warning "Error in reading SharePoint permissions. Missing sharepoint node in settings.json for $resourceName." continue } } } } } return $results } <# .Description This function updates the required permissions for the specified resources and type for the Microsoft Graph delegated application in Azure Active Directory. .Parameter ResourceNameList An array of resource names for which the permissions should be determined. .Parameter All Specifies that the permissions should be determined for all resources. .Parameter Type For which action should the permissions be updated: Read or Update. .Example Update-M365DSCAllowedGraphScopes -ResourceNameList @('O365User', 'AADApplication') -Type 'Read' .Example Update-M365DSCAllowedGraphScopes -All -Type 'Update' -Environment 'Global' .Functionality Public #> function Update-M365DSCAllowedGraphScopes { [CmdletBinding()] [OutputType()] param ( [Parameter()] [System.String[]] $ResourceNameList, [Parameter()] [Switch] $All, [Parameter(Mandatory = $true)] [ValidateSet('Read', 'Update')] [System.String] $Type, [Parameter()] [ValidateSet('Global', 'China', 'USGov', 'USGovDoD', 'Germany')] [System.String] $Environment = 'Global' ) if ($All) { Write-Verbose -Message 'All parameter specified' $dscResourcesRoot = Join-Path -Path $PSScriptRoot -ChildPath '..\DSCResources' $resourceNames = (Get-ChildItem -Path $dscResourcesRoot -Directory).Name -replace "MSFT_", '' $permissions = Get-M365DSCCompiledPermissionList -ResourceNameList $resourceNames -Source 'Graph' -PermissionsType 'Delegated' } else { if ($PSBoundParameters.ContainsKey('ResourceNameList') -eq $false) { throw "You have to specify either the All or ResourceNameList parameter!" } Write-Verbose -Message "Specified resources: $($ResourceNameList -join ", ")" $permissions = Get-M365DSCCompiledPermissionList -ResourceNameList $ResourceNameList -Source 'Graph' -PermissionsType 'Delegated' } if ($Type -eq 'Read') { Write-Verbose -Message 'Specified type: Read' Write-Verbose -Message "Found permissions: $($permissions.ReadPermissions -join ", ")" $params = @{ Scopes = $permissions.ReadPermissions } } else { Write-Verbose -Message 'Specified type: Update' Write-Verbose -Message "Found permissions: $($permissions.UpdatePermissions -join ", ")" $params = @{ Scopes = $permissions.UpdatePermissions } } Write-Verbose -Message 'Connecting to MS Graph to update permissions' $result = Connect-MgGraph @params -Environment $Environment if ($result -eq 'Welcome To Microsoft Graph!') { Write-Output 'Allowed Graph scopes updated!' } else { Write-Output 'Error during updating allowed Graph scopes!' } } <# .Description This function updates the settings.json files for all resources that use Graph cmdlets. It is compiling a permissions list based on all used Graph cmdlets in the resource and retrieving the permissions for these cmdlets from the Graph. Then it updates the settings.json file .Example Update-M365DSCResourcesSettingsJSON .Functionality Public #> <# ## HIDDEN BECAUSE OF ISSUES WITH THE FIND-MGGRAPHCOMMAND CMDLET. TEMPORARILY REPLACED WITH BELOW FUNCTION. function Update-M365DSCResourcesSettingsJSON { [CmdletBinding()] param() Write-Verbose "Determining DSCResources path" $dscResourcesRoot = Join-Path -Path $PSScriptRoot -ChildPath '..\DSCResources' Write-Verbose " DSCResouces path: $dscResourcesRoot" Write-Verbose "Getting all psm1 files" $files = Get-ChildItem -Path "$dscResourcesRoot\*.psm1" -Recurse Write-Verbose " Found $($files.Count) psm1 files" $ignoredCmdlets = @("Get-MgContext") foreach ($file in $files) { $readPermissions = @() $updatePermissions = @() if ($file -notlike "*Intune*") { Write-Verbose "Processing file: $($file.BaseName)" $content = Get-Content $file -Raw $sb = [ScriptBlock]::Create($content) $functions = $sb.Ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true) $functions = $functions | Where-Object { $_.Name -in ("Get-TargetResource", "Set-TargetResource") } foreach ($function in $functions) { Write-Verbose " Function: $($function.Name)" $regex = [Regex]::new('(?<Cmdlet>(Update|Get|Remove|Set)-Mg\w*)') $regexMatches = $regex.Matches($function.Extent.Text) $cmdlets = $regexMatches.Value | Sort-Object | Select-Object -Unique $functionPermissions = @() foreach ($cmdlet in $cmdlets) { if ($cmdlet -notin $ignoredCmdlets) { $commands = Find-MgGraphCommand -Command $cmdlet $cmdletPermissions = $commands[0].Permissions $functionPermissions += $cmdletPermissions.Name } } $cleanFunctionPermissions = $functionPermissions | Sort-Object | Select-Object -Unique if ($null -ne $cleanFunctionPermissions) { switch ($function.Name) { "Get-TargetResource" { $readPermissions = @() foreach ($item in $cleanFunctionPermissions) { $readPermissions += [PSCustomObject]@{ name = $item } } } "Set-TargetResource" { $updatePermissions = @() foreach ($item in $cleanFunctionPermissions) { $updatePermissions += [PSCustomObject]@{ name = $item } } } } } } $settingsFile = Join-Path -Path $file.DirectoryName -ChildPath 'settings.json' if (Test-Path -Path $settingsFile) { Write-Verbose " Updating existing settings.json file" $settingsJson = Get-Content -Path $settingsFile -Raw $settings = ConvertFrom-Json $settingsJson if ($readPermissions.Count -eq 0 -and $settings.permissions.read.Count -ne 0) { [array]$readPermissions = $settings.permissions.read } if ($updatePermissions.Count -eq 0 -and $settings.permissions.update.Count -ne 0) { [array]$updatePermissions = $settings.permissions.update } $settings.permissions = @([PSCustomObject]@{ read = $readPermissions update = $updatePermissions }) } else { Write-Verbose " Creating new settings.json file" $settings = [PSCustomObject]@{ resourceName = $file.BaseName -replace 'MSFT_' description = "" permissions = @([PSCustomObject]@{ read = $readPermissions update = $updatePermissions }) } } $json = ConvertTo-Json -InputObject $settings -Depth 10 Set-Content -Path $settingsFile -Value $json -Encoding UTF8 } else { Write-Verbose "$($file.BaseName) - Skipping Intune resources (unable to process those Graph cmdlets)" } } } #> <# .Description This function updates the settings.json files for all resources that use Graph cmdlets. It is compiling a permissions list based on all used Graph cmdlets in the resource and retrieving the permissions for these cmdlets from the Graph. Then it updates the settings.json file .Example Update-M365DSCResourcesSettingsJSON .Functionality Internal #> function Update-M365DSCResourcesSettingsJSON { [CmdletBinding()] param() Write-Verbose "Determining DSCResources path" $dscResourcesRoot = Join-Path -Path $PSScriptRoot -ChildPath '..\DSCResources' Write-Verbose " DSCResouces path: $dscResourcesRoot" Write-Verbose "Reading Graph Cmdlet Permissions input file" $graphCmdletPermissionsFile = Join-Path -Path $PSScriptRoot -ChildPath '..\Dependencies\GraphCmdletPermissions.csv' $cmdletPermissions = Import-Csv -Path $graphCmdletPermissionsFile -Delimiter "," -Encoding UTF8 Write-Verbose " Input file path: $graphCmdletPermissionsFile" Write-Verbose "Getting all psm1 files" $files = Get-ChildItem -Path "$dscResourcesRoot\*.psm1" -Recurse Write-Verbose " Found $($files.Count) psm1 files" $ignoredCmdlets = @("Get-MgContext") foreach ($file in $files) { $delegatedReadPermissions = @() $delegatedUpdatePermissions = @() $applicationReadPermissions = @() $applicationUpdatePermissions = @() if ($file -notlike "*Intune*") { Write-Verbose "Processing file: $($file.BaseName)" $content = Get-Content $file -Raw $sb = [ScriptBlock]::Create($content) $functions = $sb.Ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true) $functions = $functions | Where-Object { $_.Name -in ("Get-TargetResource", "Set-TargetResource") } foreach ($function in $functions) { Write-Verbose " Function: $($function.Name)" $regex = [Regex]::new('(?<Cmdlet>(Update|Get|Remove|Set|New)-Mg\w*)') $regexMatches = $regex.Matches($function.Extent.Text) $cmdlets = $regexMatches.Value | Sort-Object | Select-Object -Unique $delegatedFunctionPermissions = @() $applicationFunctionPermissions = @() foreach ($cmdlet in $cmdlets) { if ($cmdlet -notin $ignoredCmdlets) { $delegatedFunctionPermissions += ($cmdletPermissions | Where-Object { $_.Cmdlet -eq $cmdlet }).DelegatedPermissions -split "/" $applicationFunctionPermissions += ($cmdletPermissions | Where-Object { $_.Cmdlet -eq $cmdlet }).ApplicationPermissions -split "/" } } $cleanDelegatedFunctionPermissions = $delegatedFunctionPermissions | Sort-Object | Select-Object -Unique $cleanApplicationFunctionPermissions = $applicationFunctionPermissions | Sort-Object | Select-Object -Unique if ($cleanDelegatedFunctionPermissions -contains 'NotSupported') { $cleanDelegatedFunctionPermissions = @('NotSupported') } if ($cleanApplicationFunctionPermissions -contains 'NotSupported') { $cleanApplicationFunctionPermissions = @('NotSupported') } if ($null -ne $cleanDelegatedFunctionPermissions) { switch ($function.Name) { "Get-TargetResource" { $delegatedReadPermissions = @() foreach ($item in $cleanDelegatedFunctionPermissions) { $delegatedReadPermissions += [PSCustomObject]@{ name = $item } } } "Set-TargetResource" { $delegatedUpdatePermissions = @() foreach ($item in $cleanDelegatedFunctionPermissions) { $delegatedUpdatePermissions += [PSCustomObject]@{ name = $item } } } } } if ($null -ne $cleanApplicationFunctionPermissions) { switch ($function.Name) { "Get-TargetResource" { $applicationReadPermissions = @() foreach ($item in $cleanApplicationFunctionPermissions) { $applicationReadPermissions += [PSCustomObject]@{ name = $item } } } "Set-TargetResource" { $applicationUpdatePermissions = @() foreach ($item in $cleanApplicationFunctionPermissions) { $applicationUpdatePermissions += [PSCustomObject]@{ name = $item } } } } } } $settingsFile = Join-Path -Path $file.DirectoryName -ChildPath 'settings.json' if (Test-Path -Path $settingsFile) { Write-Verbose " Updating existing settings.json file" $settingsJson = Get-Content -Path $settingsFile -Raw $settings = ConvertFrom-Json $settingsJson $newPermissions = @{ graph = @{ delegated = @{ read = @() update = @() } application = @{ read = @() update = @() } } } if ($delegatedReadPermissions.Count -eq 0 -and $settings.permissions.graph.delegated.read.Count -ne 0) { [array]$delegatedReadPermissions = $settings.permissions.graph.delegated.read } if ($delegatedUpdatePermissions.Count -eq 0 -and $settings.permissions.graph.delegated.update.Count -ne 0) { [array]$delegatedUpdatePermissions = $settings.permissions.graph.delegated.update } if ($applicationReadPermissions.Count -eq 0 -and $settings.permissions.graph.application.read.Count -ne 0) { [array]$applicationReadPermissions = $settings.permissions.graph.application.read } if ($applicationUpdatePermissions.Count -eq 0 -and $settings.permissions.graph.application.update.Count -ne 0) { [array]$applicationUpdatePermissions = $settings.permissions.graph.application.update } $settings.permissions = @{ graph = @{ delegated = [PSCustomObject]@{ read = $delegatedReadPermissions update = $delegatedUpdatePermissions } application = [PSCustomObject]@{ read = $applicationReadPermissions update = $applicationUpdatePermissions } } } } else { Write-Verbose " Creating new settings.json file" $settings = [PSCustomObject]@{ resourceName = $file.BaseName -replace 'MSFT_' description = "" permissions = @{ graph = @{ delegated = [PSCustomObject]@{ read = $delegatedReadPermissions update = $delegatedUpdatePermissions } application = [PSCustomObject]@{ read = $applicationReadPermissions update = $applicationUpdatePermissions } } } } } $json = ConvertTo-Json -InputObject $settings -Depth 10 #Set-Content -Path $settingsFile -Value $json -Encoding UTF8 } else { Write-Verbose "$($file.BaseName) - Skipping Intune resources (unable to process those Graph cmdlets)" } } } <# .Description This function updates the settings.json files for all Exchange resources. It is compiling a permissions list based on all used Exchange cmdlets in the resource and retrieving the permissions for these cmdlets. Then it updates the settings.json file .Example Update-M365DSCExchangeResourcesSettingsJSON -UserPrincipalName m365dsc@contoso.onmicrosoft.com .Functionality Internal #> function Update-M365DSCExchangeResourcesSettingsJSON { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [System.String] $UserPrincipalName ) Write-Verbose "Connecting to Exchange Online" if ($null -eq (Get-Command -Name Get-Mailbox -ErrorAction SilentlyContinue)) { Import-Module ExchangeOnlineManagement Connect-ExchangeOnline -UserPrincipalName $UserPrincipalName } Write-Verbose "Determining DSCResources path" $dscResourcesRoot = Join-Path -Path $PSScriptRoot -ChildPath '..\DSCResources' Write-Verbose " DSCResouces path: $dscResourcesRoot" Write-Verbose "Getting all psm1 files" $files = Get-ChildItem -Path "$dscResourcesRoot\MSFT_EXO*\*.psm1" -Recurse Write-Verbose " Found $($files.Count) psm1 files" foreach ($file in $files) { Write-Verbose "Processing file: $($file.BaseName)" $content = Get-Content $file -Raw $sb = [ScriptBlock]::Create($content) $functions = $sb.Ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true) $functions = $functions | Where-Object { $_.Name -in ("Get-TargetResource", "Set-TargetResource") } $roleGroups = @() $allRoles = @() foreach ($function in $functions) { $errors = $null $functionCode = [ScriptBlock]::Create($function.Extent.Text) $tokens = [System.Management.Automation.PSParser]::Tokenize($functionCode, [ref]$errors) $allCmdlets = $tokens | Where-Object { $_.Type -eq "Command" } | Select-Object -Property Content -Unique -ExpandProperty Content foreach ($cmdlet in $allCmdlets) { # Checking all cmdlets, even none-EXO ones. This because requesting # cmdlets from the module depends on the permissions the user has. # No permissions for the Role means no cmdlet. $roles = Get-ManagementRole -Cmdlet $cmdlet if ($null -eq $roles) { continue } foreach ($role in $roles) { $roleAssignments = Get-ManagementRoleAssignment -Role $role.Name -Delegating $false if ($null -eq $roleAssignments -and $allRoles -notcontains $role.Name) { $allRoles += $role.Name } else { $roleGroupAssignments = $roleAssignments | Where-Object { $_.RoleAssigneeType -eq 'RoleGroup' } if ($null -ne $roleGroupAssignments) { if ($allRoles -notcontains $role.Name) { $allRoles += $role.Name } $roleAssigneeName = $roleGroupAssignments.RoleAssigneeName if ($roleGroups.Count -eq 0) { $roleGroups += $roleAssigneeName } else { $roleGroups = (Compare-Object -ReferenceObject $roleGroups -DifferenceObject $roleAssigneeName -IncludeEqual -ExcludeDifferent).InputObject } } } } } } Write-Verbose " Required Roles : $($allRoles -join ", ")" Write-Verbose " Required Role Groups: $($roleGroups -join ", ")" $settingsFile = Join-Path -Path $file.DirectoryName -ChildPath 'settings.json' if (Test-Path -Path $settingsFile) { Write-Verbose " Updating existing settings.json file" $settingsJson = Get-Content -Path $settingsFile -Raw $settings = ConvertFrom-Json $settingsJson if ($null -eq $settings.permissions) { $settings | Add-Member -MemberType NoteProperty -Name 'permissions' -Value $value $value = [PSCustomObject]@{ requiredroles = $allRoles requiredrolegroups = $roleGroups } $settings.permissions | Add-Member -MemberType NoteProperty -Name 'exchange' -Value $value } else { if ($null -eq $settings.permissions.exchange) { $value = [PSCustomObject]@{ requiredroles = $allRoles requiredrolegroups = $roleGroups } $settings.permissions | Add-Member -MemberType NoteProperty -Name 'exchange' -Value $value } else { $settings.permissions.exchange | Add-Member -MemberType NoteProperty -Name 'requiredroles' -Value $allRoles $settings.permissions.exchange | Add-Member -MemberType NoteProperty -Name 'requiredrolegroups' -Value $roleGroups } } } else { Write-Verbose " Creating new settings.json file" $settings = [PSCustomObject]@{ resourceName = $file.BaseName -replace 'MSFT_' description = "" permissions = [PSCustomObject]@{ graph = [PSCustomObject]@{ delegated = [PSCustomObject]@{ read = @() update = @() } application = [PSCustomObject]@{ read = @() update = @() } } exchange = [PSCustomObject]@{ requiredroles = $allRoles requiredrolegroups = $roleGroups } } } } $json = ConvertTo-Json -InputObject $settings -Depth 10 Set-Content -Path $settingsFile -Value $json -Encoding UTF8 } } <# .Description This function updates the settings.json files for all SharePoint resources. It is setting the Sites.FullControl.All permissions for all available actions, since all used PnP cmdlets require this permissions. Then it updates the settings.json file .Example Update-M365DSCSharePointResourcesSettingsJSON .Functionality Internal #> function Update-M365DSCSharePointResourcesSettingsJSON { [CmdletBinding()] param () Write-Verbose "Determining DSCResources path" $dscResourcesRoot = Join-Path -Path $PSScriptRoot -ChildPath '..\DSCResources' Write-Verbose " DSCResouces path: $dscResourcesRoot" Write-Verbose "Getting all psm1 files" $files = Get-ChildItem -Path "$dscResourcesRoot\MSFT_SPO*\*.psm1" -Recurse Write-Verbose " Found $($files.Count) psm1 files" foreach ($file in $files) { Write-Verbose "Processing file: $($file.BaseName)" $settingsFile = Join-Path -Path $file.DirectoryName -ChildPath 'settings.json' if (Test-Path -Path $settingsFile) { Write-Verbose " Updating existing settings.json file" $settingsJson = Get-Content -Path $settingsFile -Raw $settings = ConvertFrom-Json $settingsJson if ($null -eq $settings.permissions) { $settings | Add-Member -MemberType NoteProperty -Name 'permissions' -Value $value $value = [PSCustomObject]@{ delegated = [PSCustomObject]@{ read = @( [PSCustomObject]@{ name = 'Sites.FullControl.All' } ) update = @( [PSCustomObject]@{ name = 'Sites.FullControl.All' } ) } application = [PSCustomObject]@{ read = @( [PSCustomObject]@{ name = 'Sites.FullControl.All' } ) update = @( [PSCustomObject]@{ name = 'Sites.FullControl.All' } ) } } $settings.permissions | Add-Member -MemberType NoteProperty -Name 'sharepoint' -Value $value } else { if ($null -eq $settings.permissions.sharepoint) { $value = [PSCustomObject]@{ delegated = [PSCustomObject]@{ read = @( [PSCustomObject]@{ name = 'Sites.FullControl.All' } ) update = @( [PSCustomObject]@{ name = 'Sites.FullControl.All' } ) } application = [PSCustomObject]@{ read = @( [PSCustomObject]@{ name = 'Sites.FullControl.All' } ) update = @( [PSCustomObject]@{ name = 'Sites.FullControl.All' } ) } } $settings.permissions | Add-Member -MemberType NoteProperty -Name 'sharepoint' -Value $value } else { $value = [PSCustomObject]@{ read = @( [PSCustomObject]@{ name = 'Sites.FullControl.All' } ) update = @( [PSCustomObject]@{ name = 'Sites.FullControl.All' } ) } if ($null -eq $settings.permissions.sharepoint.delegated) { $settings.permissions.sharepoint | Add-Member -MemberType NoteProperty -Name 'delegated' -Value $value } else { $value = @( [PSCustomObject]@{ name = 'Sites.FullControl.All' } ) if ($null -eq $settings.permissions.sharepoint.delegated.read) { $settings.permissions.sharepoint.delegated | Add-Member -MemberType NoteProperty -Name 'read' -Value $value } else { $settings.permissions.sharepoint.delegated.read = $value } if ($null -eq $settings.permissions.sharepoint.delegated.update) { $settings.permissions.sharepoint.delegated | Add-Member -MemberType NoteProperty -Name 'update' -Value $value } else { $settings.permissions.sharepoint.delegated.update = $value } } if ($null -eq $settings.permissions.sharepoint.application) { $settings.permissions.sharepoint | Add-Member -MemberType NoteProperty -Name 'application' -Value $value } else { $value = @( [PSCustomObject]@{ name = 'Sites.FullControl.All' } ) if ($null -eq $settings.permissions.sharepoint.application.read) { $settings.permissions.sharepoint.application | Add-Member -MemberType NoteProperty -Name 'read' -Value $value } else { $settings.permissions.sharepoint.application.read = $value } if ($null -eq $settings.permissions.sharepoint.application.update) { $settings.permissions.sharepoint.application | Add-Member -MemberType NoteProperty -Name 'update' -Value $value } else { $settings.permissions.sharepoint.application.update = $value } } } } } else { Write-Verbose " Creating new settings.json file" $settings = [PSCustomObject]@{ resourceName = $file.BaseName -replace 'MSFT_' description = "" permissions = [PSCustomObject]@{ graph = [PSCustomObject]@{ delegated = [PSCustomObject]@{ read = @() update = @() } application = [PSCustomObject]@{ read = @() update = @() } } sharepoint = [PSCustomObject]@{ delegated = [PSCustomObject]@{ read = @( [PSCustomObject]@{ name = 'Sites.FullControl.All' } ) update = @( [PSCustomObject]@{ name = 'Sites.FullControl.All' } ) } application = [PSCustomObject]@{ read = @( [PSCustomObject]@{ name = 'Sites.FullControl.All' } ) update = @( [PSCustomObject]@{ name = 'Sites.FullControl.All' } ) } } } } } $json = ConvertTo-Json -InputObject $settings -Depth 10 Set-Content -Path $settingsFile -Value $json -Encoding UTF8 } } <# .Description This function creates or updates an application in Azure AD. It assigns permissions, grants consent and creates a secret or uploads a certificate to the application. This application can then be used for Application Authentication. The provided permissions have to be as an array of hashtables, with Api=Graph, SharePoint or Exchange and PermissionsName set to a list of permissions. See examples for more information. NOTE: If consent cannot be given for whatever reason, make sure all these permissions are given Admin Consent by browsing to the App Registration in Azure AD > API Permissions and clicking the "Grant admin consent for <orgname>" button. More information: Graph API permissions: https://docs.microsoft.com/en-us/graph/permissions-reference Exchange permissions: https://docs.microsoft.com/en-us/exchange/permissions-exo/permissions-exo Note: If you want to configure App-Only permission for Exchange, as described here: https://docs.microsoft.com/en-us/powershell/exchange/app-only-auth-powershell-v2?view=exchange-ps#step-2-assign-api-permissions-to-the-application Using the following permission will achieve exactly that: @{Api='Exchange';PermissionsName='Exchange.ManageAsApp'} .Example Update-M365DSCAzureAdApplication -ApplicationName 'Microsoft365DSC' -Permissions @(@{Api='SharePoint';PermissionName='Sites.FullControl.All'}) -AdminConsent -Type Secret .Example Update-M365DSCAzureAdApplication -ApplicationName 'Microsoft365DSC' -Permissions @(@{Api='Graph';PermissionName='Domain.Read.All'}) -AdminConsent -Type Certificate -CreateSelfSignedCertificate -CertificatePath c:\Temp\M365DSC.cer .Example Update-M365DSCAzureAdApplication -ApplicationName 'Microsoft365DSC' -Permissions @(@{Api='SharePoint';PermissionName='Sites.FullControl.All'},@{Api='Graph';PermissionName='Group.ReadWrite.All'},@{Api='Exchange';PermissionsName='Exchange.ManageAsApp'}) -AdminConsent -Type Certificate -CertificatePath c:\Temp\M365DSC.cer .Functionality Public #> function Update-M365DSCAzureAdApplication { [CmdletBinding(DefaultParameterSetName = 'Secret')] param ( [Parameter(ParameterSetName = 'Secret')] [Parameter(ParameterSetName = 'Certificate')] [System.String] $ApplicationName = 'Microsoft365DSC', [Parameter(Mandatory = $true, ParameterSetName = 'Secret')] [Parameter(Mandatory = $true, ParameterSetName = 'Certificate')] [System.Collections.Hashtable[]] $Permissions, [Parameter(ParameterSetName = 'Secret')] [Parameter(ParameterSetName = 'Certificate')] [ValidateSet('Secret', 'Certificate')] [System.String] $Type = 'Secret', [Parameter(ParameterSetName = 'Secret')] [Parameter(ParameterSetName = 'Certificate')] [System.Int32] $MonthsValid = 12, [Parameter(ParameterSetName = 'Secret')] [Switch] $CreateNewSecret, [Parameter(ParameterSetName = 'Certificate')] [System.String] $CertificatePath, [Parameter(ParameterSetName = 'Certificate')] [Switch] $CreateSelfSignedCertificate, [Parameter(ParameterSetName = 'Secret')] [Parameter(ParameterSetName = 'Certificate')] [Switch] $AdminConsent ) function Write-LogEntry { param ( [Parameter(Mandatory = $true)] [System.String] $Message, [Parameter()] [ValidateSet('Error', 'Warning', 'Info')] [System.String] $Type = 'Info' ) $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" switch ($Type) { 'Error' { $params = @{ Object = ("{0} - [ERROR] {1}" -f $timestamp, $Message) ForegroundColor = 'Red' } } 'Warning' { $params = @{ Object = ("{0} - [WARNING] {1}" -f $timestamp, $Message) ForegroundColor = 'Yellow' } } 'Info' { $params = @{ Object = ("{0} - {1}" -f $timestamp, $Message) ForegroundColor = 'White' } } } Write-Host @params } Write-LogEntry -Message "Checking specified parameters" switch ($Type) { 'Secret' { Write-LogEntry -Message " Using a Secret as credential" } 'Certificate' { Write-LogEntry -Message " Using a Certificate as credential" Write-LogEntry -Message " " Write-LogEntry -Message " Make sure your certificate has the following prerequisites:" Write-LogEntry -Message " KeySpec : Signature" Write-LogEntry -Message " KeyLength : 2048" Write-LogEntry -Message " KeyAlgorithm : RSA" Write-LogEntry -Message " HashAlgorithm : SHA256 or SHA1" Write-LogEntry -Message " Enhanced Key Uses : Client Authentication and Server Authentication" Write-LogEntry -Message " And the entire certificate chain is available!" Write-LogEntry -Message " " if ($PSBoundParameters.ContainsKey("CertificatePath") -eq $false) { if ($PSBoundParameters.ContainsKey("CreateSelfSignedCertificate")) { # CreateSelfSignedCertificate is specified, but CertificatePath is missing. Write-LogEntry -Message "You have to specify CertificatePath, when specifying the CreateSelfSignedCertificate parameter." -Type Error return } else { # Neither CertificatePath and CreateSelfSignedCertificate are specified. Write-LogEntry -Message "Certificate is specified as Type, but neither the CertificatePath or CreateSelfSignedCertificate parameters are specified." -Type Error return } } else { if ($PSBoundParameters.ContainsKey("CreateSelfSignedCertificate")) { # CreateSelfSignedCertificate is specified and path specified in CertificatePath already exists. if ((Test-Path -Path $CertificatePath) -eq $true) { Write-LogEntry -Message "Specified CertificatePath '$CertificatePath', where the Self Signed Certificate should be exported, already exists." -Type Error return } } else { # CreateSelfSignedCertificate is NOT specified and path specified in CertificatePath does not exists. if ((Test-Path -Path $CertificatePath) -eq $false) { Write-LogEntry -Message "Specified CertificatePath '$CertificatePath' does not exist." -Type Error return } } } } } if ($null -eq (Get-Module -Name Az.Resources -ListAvailable)) { Write-LogEntry "This function requires the Az.Resources module, which isn't currently installed. Please install before running this function." -Type Warning return } $context = Get-AzContext if ($null -eq $context) { Connect-AzAccount $context = Get-AzContext } $graphSvcprincipal = Get-AzADServicePrincipal | Where-Object -FilterScript { $_.DisplayName -eq "Microsoft Graph" } $spSvcprincipal = Get-AzADServicePrincipal | Where-Object -FilterScript { $_.DisplayName -eq "Office 365 SharePoint Online" } Write-LogEntry " " Write-LogEntry "Checking existance of AD Application" if (-not ($azureADApp = Get-AzADApplication -Filter "DisplayName eq '$($ApplicationName)'" -ErrorAction SilentlyContinue)) { $azureADApp = New-AzADApplication -DisplayName $ApplicationName Write-LogEntry " New Azure AD application '$ApplicationName' created!" } else { Write-LogEntry " Application '$ApplicationName' already exists!" } if ($null -ne $azureADApp) { Write-LogEntry " " Write-LogEntry "Checking app permissions" $permissionsSet = $false foreach ($permission in $Permissions) { if ($permission.Api -eq $null -or $permission.Api -notin @("Graph", "SharePoint")) { Write-LogEntry "Specified permission is invalid $(Convert-M365DscHashtableToString -Hashtable $permission)" -Type Warning continue } Write-LogEntry " Checking permission '$($permission.Api)\$($permission.PermissionName)'" switch ($permission.Api) { "Graph" { $svcprincipal = $graphSvcprincipal } "SharePoint" { $svcprincipal = $spSvcprincipal } } $appRole = $svcprincipal.AppRole | Where-Object -FilterScript { $_.Value -eq $permission.PermissionName } if ($null -eq $appRole) { Write-LogEntry " [ERROR] Permission '$($permission.PermissionName)' not found!" continue } if ($null -eq (Get-AzADAppPermission -ObjectId $azureAdApp.Id | Where-Object { $_.Id -eq $appRole.Id })) { $null = Add-AzADAppPermission -ObjectId $azureADApp.Id -ApiId $svcprincipal.AppId -PermissionId $appRole.Id -Type Role Write-LogEntry " Permission added to application" $permissionsSet = $true } else { Write-LogEntry " Permission '$($permission.Api)\$($permission.PermissionName)' already added to the application!" } } if ($AdminConsent) { Write-LogEntry " " Write-LogEntry "Waiting 10 seconds for application creation" Write-LogEntry " ..." Start-Sleep -Seconds 10 Write-LogEntry " " Write-LogEntry "Providing Admin Consent for application permissions" $token = [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.AuthenticationFactory.Authenticate( $context.Account, $context.Environment, $context.Tenant.Id, $null, "Never", $null, "74658136-14ec-4630-ad9b-26e160ff0fc6") $headers = @{ 'Authorization' = 'Bearer ' + $token.AccessToken 'X-Requested-With' = 'XMLHttpRequest' 'x-ms-client-request-id' = [guid]::NewGuid() 'x-ms-correlation-id' = [guid]::NewGuid() } $applicationId = $azureADApp.AppId $url = "https://main.iam.ad.ext.azure.com/api/RegisteredApplications/$applicationId/Consent?onBehalfOfAll=true" try { $null = Invoke-RestMethod -Uri $url -Headers $headers -Method POST -ErrorAction Stop Write-LogEntry " Admin Consent for application permissions provided" } catch { Write-LogEntry "[ERROR] Error while providing consent to the requested permissions. Please make sure you provide consent via the Azure AD Admin Portal." -Type Error Write-LogEntry "Error details: $($_.Exception.Message)" } } Write-LogEntry " " Write-LogEntry "Checking app credentials" $appCreds = Get-AzADAppCredential -ObjectId $azureADApp.Id $endDate = (Get-Date).AddMonths($MonthsValid) switch ($Type) { 'Secret' { # Filtering retrieved credentials for PasswordCredentials $passwordCreds = $appCreds | Where-Object -FilterScript { $_ -is [Microsoft.Azure.PowerShell.Cmdlets.Resources.MSGraph.Models.ApiV10.MicrosoftGraphPasswordCredential] } $createSecret = $false if ($null -eq $passwordCreds) { Write-LogEntry " No app credentials found, creating new" Write-LogEntry " Creating App Secret" $createSecret = $true } else { if ($CreateNewSecret) { Write-LogEntry " Existing app credentials found, but CreateNewSecret specified. Creating new secret!" $createSecret = $true } else { Write-LogEntry " Existing app credentials found, but CreateNewSecret not specified. Please use an existing secret!" } } if ($createSecret) { $appCred = New-AzADAppCredential -ObjectId $azureADApp.Id -EndDate $endDate } } 'Certificate' { $createCertificate = $false # Filtering retrieved credentials for CertificateCredentials $certCreds = $appCreds | Where-Object -FilterScript { $_ -is [Microsoft.Azure.PowerShell.Cmdlets.Resources.MSGraph.Models.ApiV10.MicrosoftGraphKeyCredential] } if ($PSBoundParameters.ContainsKey("CertificatePath")) { $cerCert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $CertificatePath } if ($null -eq $certCreds) { Write-LogEntry " Uploading App Certificate" $createCertificate = $true } else { if ($PSBoundParameters.ContainsKey("CreateSelfSignedCertificate") -eq $false) { Write-LogEntry " CertificatePath specified '$CertificatePath', using that certificate" $certCred = $certCreds | Where-Object { $_.DisplayName -eq $cerCert.Subject -and $_.EndDateTime -eq $cerCert.NotAfter.ToUniversalTime() } if ($null -eq $certCred) { Write-LogEntry " Specified certificate does not exist in the app, uploading now" $createCertificate = $true } else { Write-LogEntry " Specified certificate already exists in the app, continuing" } } else { Write-LogEntry "Parameter CreateSelfSignedCertificate has been specified, but a Certificate has already been added to the application." -Type Warning Write-LogEntry "Ignoring creating a new self signed certificate." -Type Warning } } if ($createCertificate) { if ($CreateSelfSignedCertificate) { Write-LogEntry " CreateSelfSignedCertificate specified, generating new Self Signed Certificate" $cerCert = New-SelfSignedCertificate -CertStoreLocation "Cert:\CurrentUser\My" ` -Subject "CN=$ApplicationName" ` -KeySpec Signature ` -NotAfter $endDate ` -KeyLength 2048 ` -KeyAlgorithm RSA ` -HashAlgorithm SHA256 $null = Export-Certificate -Cert $cerCert -Type CERT -FilePath $CertificatePath Write-LogEntry " Certificate exported to $CertificatePath" } $keyValue = [System.Convert]::ToBase64String($cerCert.GetRawCertData()) Write-LogEntry " Certificate details: $($cerCert.Subject) ($($cerCert.Thumbprint))" $appCred = New-AzADAppCredential -ObjectId $azureADApp.Id -CertValue $keyValue -EndDate $cerCert.NotAfter } } } Write-LogEntry " " Write-LogEntry "Application Id: $($azureADapp.AppId)" Write-LogEntry "Tenant Id : $($context.Tenant)" if ($null -ne $appCred) { Write-LogEntry "Secret : $($appCred.SecretText)" Write-LogEntry " " Write-LogEntry "IMPORTANT: A new secret has been created. This is only displayed once: Make sure you store this information!" } Write-LogEntry " " Write-LogEntry "NOTE: Make sure you add the application to the required Microsoft 365 (e.g. Global Admin) or Exchange (e.g. Organization Management) role groups as well!" Write-LogEntry " See the documentation for any required permissions." } } Export-ModuleMember -Function @( 'Get-M365DSCCompiledPermissionList', 'Update-M365DSCAllowedGraphScopes', 'Update-M365DSCAzureAdApplication', 'Update-M365DSCExchangeResourcesSettingsJSON', 'Update-M365DSCSharePointResourcesSettingsJSON', 'Update-M365DSCResourcesSettingsJSON' ) |