Modules/M365DSCPermissions.psm1
using namespace System.Management.Automation.Language <# .Description This function lists all Graph, SharePoint or Exchange permissions required for the specified resources, both for reading/updating and Delegated/Applications. With the parameters, you can specify a specific subset of permissions, to be use with the Permissions parameter of Update-M365DSCAzureAdApplication. .Parameter ResourceNameList An array of resource names for which the permissions should be determined. .Parameter PermissionsType Specifies what type of permissions need to get returned, Delegated or Application. .Parameter AccessType Specifies the workload of the permissions that need to get returned. .Example Get-M365DSCCompiledPermissionList -ResourceNameList @('EXOAcceptedDomain') .Example Get-M365DSCCompiledPermissionList -ResourceNameList (Get-M365DSCAllResources) .Example Get-M365DSCCompiledPermissionList -ResourceNameList (Get-M365DSCAllResources) -PermissionType 'Application' -AccessType 'Update' .Example Get-M365DSCCompiledPermissionList -ResourceNameList (Get-M365DSCAllResources) -PermissionType 'Delegated' -AccessType 'Read' .Functionality Public #> function Get-M365DSCCompiledPermissionList { [CmdletBinding(DefaultParametersetName = 'None')] [OutputType([System.Collections.Hashtable])] param ( [Parameter(Mandatory = $true, Position = 0)] [System.String[]] $ResourceNameList, [Parameter()] [System.String] [ValidateSet('Delegated', 'Application')] $PermissionType, [Parameter()] [System.String] [ValidateSet('Read', 'Update')] $AccessType ) if (($PSBoundParameters.ContainsKey('PermissionType') -eq $true -and $PSBoundParameters.ContainsKey('AccessType') -eq $false)) { throw 'You specified parameter PermissionType without AccessType. Please specify both parameters.' } if (($PSBoundParameters.ContainsKey('PermissionType') -eq $false -and $PSBoundParameters.ContainsKey('AccessType') -eq $true)) { throw 'You specified parameter AccessType without PermissionType. Please specify both parameters.' } $results = @{ Read = @( @{ API = 'Graph' Permission = @{ Name = 'Organization.Read.All' Type = 'Application' } } ) Update = @( @{ API = 'Graph' Permission = @{ Name = 'Organization.Read.All' Type = 'Application' } } ) RequiredRoles = @() RequiredRoleGroups = @() } $total = $ResourceNameList.Count $count = 1 foreach ($resourceName in $ResourceNameList) { $percentage = ($count / $total) * 100 Write-Progress -Activity 'Retrieving required permissions' -PercentComplete $percentage -Status 'Processing resource' -CurrentOperation $resourceName 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 } # Graph permissions if ($null -ne $resourceSettings.permissions.graph) { Write-Verbose -Message ' Retrieving Graph permissions' # Delegated Update permissions Update-M365DSCPermissionsMatrix -Source 'Graph' ` -PermissionType 'Delegated' ` -AccessType 'Update' ` -Matrix ([ref]$results) ` -Settings ($resourceSettings) # Application Update permissions Update-M365DSCPermissionsMatrix -Source 'Graph' ` -PermissionType 'Application' ` -AccessType 'Update' ` -Matrix ([ref]$results) ` -Settings ($resourceSettings) # Delegated Read permissions Update-M365DSCPermissionsMatrix -Source 'Graph' ` -PermissionType 'Delegated' ` -AccessType 'Read' ` -Matrix ([ref]$results) ` -Settings ($resourceSettings) # Application Read permissions Update-M365DSCPermissionsMatrix -Source 'Graph' ` -PermissionType 'Application' ` -AccessType 'Read' ` -Matrix ([ref]$results) ` -Settings ($resourceSettings) } else { Write-Verbose " No Graph node in settings.json for $resourceName." } # Exchange permissions if ($null -ne $resourceSettings.permissions.exchange) { Write-Verbose -Message ' Retrieving Exchange permissions' # 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" } } $exchangeRead = $results.Read | Where-Object -FilterScript { $_.API -eq 'Exchange' -and $_.Permission.Name -eq 'Exchange.ManageAsApp' } if ($null -eq $exchangeRead) { $results.Read += @{ API = 'Exchange' Permission = @{ Type = 'Application' Name = 'Exchange.ManageAsApp' } } } $exchangeUpdate = $results.Update | Where-Object -FilterScript { $_.API -eq 'Exchange' -and $_.Permission.Name -eq 'Exchange.ManageAsApp' } if ($null -eq $exchangeUpdate) { $results.Update += @{ API = 'Exchange' Permission = @{ Type = 'Application' Name = 'Exchange.ManageAsApp' } } } } else { Write-Verbose " No Exchange node in settings.json for $resourceName." } # SharePoint permissions if ($null -ne $resourceSettings.permissions.sharepoint) { Write-Verbose -Message ' Retrieving SharePoint permissions' # Delegated Update permissions Update-M365DSCPermissionsMatrix -Source 'SharePoint' ` -PermissionType 'Delegated' ` -AccessType 'Update' ` -Matrix ([ref]$results) ` -Settings ($resourceSettings) # Application Update permissions Update-M365DSCPermissionsMatrix -Source 'SharePoint' ` -PermissionType 'Application' ` -AccessType 'Update' ` -Matrix ([ref]$results) ` -Settings ($resourceSettings) # Delegated Read permissions Update-M365DSCPermissionsMatrix -Source 'SharePoint' ` -PermissionType 'Delegated' ` -AccessType 'Read' ` -Matrix ([ref]$results) ` -Settings ($resourceSettings) # Application Read permissions Update-M365DSCPermissionsMatrix -Source 'SharePoint' ` -PermissionType 'Application' ` -AccessType 'Read' ` -Matrix ([ref]$results) ` -Settings ($resourceSettings) } else { Write-Verbose " No SharePoint node in settings.json for $resourceName." } } $count++ } if ($PSBoundParameters.ContainsKey('PermissionType')) { $results = $results.$AccessType | Where-Object -FilterScript { $_. Permission.Type -eq $PermissionType } $results | ForEach-Object -Process { $_.PermissionName = $_.Permission.Name $_.Remove('Permission') } } return $results } <# .Description This function updates the inputted permissions matrix. It is generic code used in the Get-M365DSCCompiledPermissionList function. .Functionality Internal, Hidden #> function Update-M365DSCPermissionsMatrix { param ( [Parameter(Mandatory = $true)] [System.String] $Source, [Parameter(Mandatory = $true)] [System.String] [ValidateSet('Delegated', 'Application')] $PermissionType, [Parameter(Mandatory = $true)] [System.String] [ValidateSet('Read', 'Update')] $AccessType, [Parameter(Mandatory = $true)] [System.Collections.Hashtable] [ref]$Matrix, [Parameter(Mandatory = $true)] [PSCustomObject] $Settings ) foreach ($permission in $Settings.permissions.$Source.$PermissionType.$AccessType) { if ($permission.Name -ne 'NotSupported') { $matrixPermission = $results.$AccessType | Where-Object -FilterScript { $_.API -eq $Source -and $_.Permission.Name -eq $permission.name -and $_.Permission.Type -eq $PermissionType } if ($null -eq $matrixPermission) { Write-Verbose -Message " Found new $AccessType permission {$($permission.name)} for API {$Source}" $results.$AccessType += @{ API = $Source Permission = @{ Type = $PermissionType Name = $permission.name } } } else { Write-Verbose -Message " $AccessType permission {$($permission.name)} was already added for API {$Source}" } } } } <# .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 @('AADUSer', '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' $resourceNames = Get-M365DSCAllResources } 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 ', ')" $resourceNames = $ResourceNameList } Write-Verbose -Message "Specified type: $Type" $results = Get-M365DSCCompiledPermissionList -ResourceNameList $resourceNames -PermissionType 'Delegated' -AccessType $Type $permissions = ($results | Where-Object -FilterScript { $_.API -eq 'Graph' }).PermissionName Write-Verbose -Message "Found permissions: $($permissions -join ', ')" $params = @{ Scopes = $permissions } 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: Please make sure you have the following permissions for the 'Microsoft Graph Command Line Tools' Enterprise Application in your tenant: - Application.ReadWrite.All You can add this scope to the 'Microsoft Graph Command Line Tools' Enterprise Application by running the following command: ```powershell Connect-MgGraph -Scopes 'Application.ReadWrite.All' ``` 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'} Note 2: If you want to configure App-Only permission for Security and compliance, please refer to this information on how to setup the permissions: https://microsoft365dsc.com/user-guide/get-started/authentication-and-permissions/#security-and-compliance-center-permissions Note 3: If you want to configure App-Only permission for Power Platform, please refer to this information on how to setup the permissions: https://microsoft365dsc.com/user-guide/get-started/authentication-and-permissions/#power-apps-permissions .Parameter ApplicationName The name of the application to create or update. Default value is 'Microsoft365DSC'. .Parameter Permissions The permissions to assign to the application. This has to be an array of hashtables, with Api=Graph, SharePoint or Exchange and PermissionsName set to a list of permissions. See examples for more information. .Parameter Type The type of credential to create. Default value is 'Secret'. Valid values are 'Secret' and 'Certificate'. .Parameter MonthsValid The number of months the certificate should be valid. Default value is 12. .Parameter CreateNewSecret If specified, a new secret will be created for the application. -CreateNewSecret or -CertificatePath can be used, not both. .Parameter CertificatePath The path to the certificate to be uploaded for the app registration. If using with -CreateSelfSignedCertificate - a file with this name will be created and uploaded (file must not exist). Otherwise the file must already exist. Cannot be used with -CreateNewSecret simultaneously. .Parameter CreateSelfSignedCertificate If specified, a self-signed certificate will be created for the application. -CreateSelfSignedCertificate or -CertificatePath can be used, not both. .Parameter AdminConsent If specified, admin consent will be granted for the application. .Parameter Credential The credential to use for authenticating the request. Mutually exclusive with -TenantId. .Parameter ApplicationId The ApplicationId to use for authenticating the request. -Credential or -ApplicationId can be used, not both. .Parameter TenantId The name of the tenant to use for the request. Must be in the form of contoso.onmicrosoft.com. Mutually exclusive with -Credential. .Parameter ApplicationSecret The ApplicationSecret to use for authenticating the request. -Credential or -ApplicationSecret can be used, not both. .Parameter CertificateThumbprint Thumbprint of an existing auth certificate to use for authenticating the request. Mutually exclusive with -Credential. .Parameter ManagedIdentity If specified, Managed Identity will be used for authenticating the request. -Credential or -ApplicationId or -ManagedIdentity can be used, only one of them. .Example Update-M365DSCAzureAdApplication -ApplicationName 'Microsoft365DSC' -Permissions @(@{Api='SharePoint';PermissionName='Sites.FullControl.All'}) -AdminConsent -Type Secret -Credential $creds .EXAMPLE Update-M365DSCAzureAdApplication -ApplicationName 'Microsoft365DSC' -Permissions @(@{Api='Graph';PermissionName='Domain.Read.All'}) -AdminConsent -Credential $creds -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';PermissionName='Exchange.ManageAsApp'}) -AdminConsent -Credential $creds -Type Certificate -CertificatePath c:\Temp\M365DSC.cer .EXAMPLE Update-M365DSCAzureAdApplication -ApplicationName $Microsoft365DSC -Permissions $(Get-M365DSCCompiledPermissionList -ResourceNameList Get-M365DSCAllResources -PermissionType Application -AccessType Read) -Type Certificate -CreateSelfSignedCertificate -AdminConsent -MonthsValid 12 -Credential $creds -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, [Parameter()] [System.Management.Automation.PSCredential] $Credential, [Parameter()] [System.String] $ApplicationId, [Parameter()] [System.String] $TenantId, [Parameter()] [System.Management.Automation.PSCredential] $ApplicationSecret, [Parameter()] [System.String] $CertificateThumbprint, [Parameter()] [Switch] $ManagedIdentity ) 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 } $ConnectionMode = New-M365DSCConnection -Workload 'MicrosoftGraph' ` -InboundParameters $PSBoundParameters $requireWait = $false 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 } } } } } $resourceAppIdMsGraph = '00000003-0000-0000-c000-000000000000' $resourceAppIdSharePoint = '00000003-0000-0ff1-ce00-000000000000' $resourceAppIdExchange = '00000002-0000-0ff1-ce00-000000000000' $graphSvcprincipal = Get-MgServicePrincipal -Filter "AppId eq '$resourceAppIdMsGraph'" $spSvcprincipal = Get-MgServicePrincipal -Filter "AppId eq '$resourceAppIdSharePoint'" $exSvcprincipal = Get-MgServicePrincipal -Filter "AppId eq '$resourceAppIdExchange'" Write-LogEntry ' ' Write-LogEntry 'Checking existance of AD Application' if (-not ($azureADApp = Get-MgApplication -Filter "DisplayName eq '$($ApplicationName)'" -ErrorAction SilentlyContinue)) { $azureADApp = New-MgApplication -DisplayName $ApplicationName Write-LogEntry " New Azure AD application '$ApplicationName' created!" $requireWait = $true } else { Write-LogEntry " Application '$ApplicationName' already exists!" } if ($null -ne $azureADApp) { Write-LogEntry ' ' Write-LogEntry 'Checking app permissions' $allRequiredAccess = @{} foreach ($permission in $Permissions) { if ($null -eq $permission.Api -or $permission.Api -notin @('Graph', 'SharePoint', 'Exchange')) { 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 } 'Exchange' { $svcprincipal = $exSvcprincipal } } $appRole = $azureADApp.AppRoles | Where-Object -FilterScript { $_.Value -eq $permission.PermissionName } if ($null -eq $appRole) { $currentAPIAccess = $allRequiredAccess.($svcprincipal.AppId) if ($null -eq $currentAPIAccess) { $allRequiredAccess.Add(($svcprincipal.AppId), @()) } $role = $svcPrincipal.AppRoles | Where-Object -FilterScript { $_.Value -eq $permission.PermissionName } if ($null -eq $role) { $ObjectGuid = [System.Guid]::empty if ([System.Guid]::TryParse($permission.PermissionName , [System.Management.Automation.PSReference]$ObjectGuid)) { $appPermission = @{ Id = $permission.PermissionName Type = 'Role' } } else { continue } } else { $appPermission = @{ Id = $role.Id Type = 'Role' } } $allRequiredAccess.($svcprincipal.AppId) += $appPermission } else { Write-LogEntry " Permission '$($permission.Api)\$($permission.PermissionName)' already added to the application!" } } $requiredResourceAccess = @() foreach ($provider in $allRequiredAccess.Keys) { $valueToAdd = @{ ResourceAppId = $provider ResourceAccess = @() } foreach ($permissionEntry in $allRequiredAccess.$provider) { $permissionToAdd = @{ Type = $permissionEntry.Type Id = $permissionEntry.Id } $valueToAdd.ResourceAccess += $permissionToAdd } $requiredResourceAccess += $valueToAdd } Update-MgApplication -ApplicationId ($azureADApp.Id) ` -RequiredResourceAccess $requiredResourceAccess | Out-Null Write-LogEntry ' Permission updated for application' if ($AdminConsent) { if (-not $PSBoundParameters.ContainsKey('Credential')) { Write-LogEntry '[ERROR] You need to provide admin credentials when specifying the AdminConsent parameter.' } else { $ConnectionMode = New-M365DSCConnection -Workload 'MicrosoftGraph' ` -InboundParameters $PSBoundParameters if ($requireWait) { 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' $tenantid = $Credential.UserName.Split('@')[1] $username = $Credential.UserName $password = $Credential.GetNetworkCredential().password $url = "https://main.iam.ad.ext.azure.com/api/Directories/$($tenant.tenantId)/Details" $uri = 'https://login.microsoftonline.com/{0}/oauth2/token' -f $tenantid $body = 'resource=74658136-14ec-4630-ad9b-26e160ff0fc6&client_id=1950a258-227b-4e31-a9cf-717495945fc2&grant_type=password&username={1}&password={0}' -f [System.Web.HttpUtility]::UrlEncode($password), $username $token = Invoke-RestMethod $uri ` -Method POST ` -Body $body ` -ContentType 'application/x-www-form-urlencoded' ` -ErrorAction SilentlyContinue $headers = @{ Authorization = "Bearer $($token.access_token)" 'x-ms-client-request-id' = [guid]::NewGuid().ToString() 'x-ms-client-session-id' = [guid]::NewGuid().ToString() } $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' $endDate = (Get-Date).AddMonths($MonthsValid) switch ($Type) { 'Secret' { # Filtering retrieved credentials for PasswordCredentials $passwordCreds = $azureADApp.PasswordCredentials $createSecret = $false if ($passwordCreds.Count -eq 0) { 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) { $passwordCred = @{ displayName = 'Created by Microsoft365DSC' endDateTime = $endDate } $appCred = Add-MgApplicationPassword -ApplicationId $azureADApp.Id -PasswordCredential $passwordCred } } 'Certificate' { $createCertificate = $false # Filtering retrieved credentials for CertificateCredentials $certCreds = $azureADApp.KeyCredentials if (($PSBoundParameters.ContainsKey('CertificatePath') -and (-not $CreateSelfSignedCertificate))) { $cerCert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $CertificatePath } if ($certCreds.Count -eq 0) { 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" } Write-LogEntry " Certificate details: $($cerCert.Subject) ($($cerCert.Thumbprint))" $params = @{ Type = 'AsymmetricX509Cert' Usage = 'Verify' Key = $cerCert.GetRawCertData() } $maxRetries = 3 $retryCount = 0 $retryDelay = 10 # seconds do { try { $appCred = Update-MgApplication -ApplicationId $azureAdApp.Id -KeyCredentials $params -ErrorAction Stop break # exit the loop if the operation succeeds } catch { if ($_.Exception.Message -match 'Key credential end date is invalid') { Write-Host "Caught error: $($_.Exception.Message)" if ($retryCount -lt $maxRetries) { $retryCount++ Write-Host "Retrying in $retryDelay seconds..." Start-Sleep -Seconds $retryDelay } else { Write-Host 'Maximum number of retries reached.' throw # re-throw the exception if the maximum number of retries is reached } } else { throw # re-throw the exception if it's not the expected error } } } while ($true) } } } Write-LogEntry ' ' Write-LogEntry "Application Id: $($azureADapp.AppId)" 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' ) |