From 66453c3369df3793642696188391dd556d3452b8 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 28 Aug 2023 07:52:07 -0400 Subject: [PATCH 01/30] Remove Maintenance Scripts - Cleanup-TokenCache.ps1 - Migrate-CippStorage.ps1 --- .../Scripts/Clear-TokenCache.ps1 | 75 -------- .../Scripts/Migrate-CippStorage.ps1 | 174 ------------------ 2 files changed, 249 deletions(-) delete mode 100644 ExecMaintenanceScripts/Scripts/Clear-TokenCache.ps1 delete mode 100644 ExecMaintenanceScripts/Scripts/Migrate-CippStorage.ps1 diff --git a/ExecMaintenanceScripts/Scripts/Clear-TokenCache.ps1 b/ExecMaintenanceScripts/Scripts/Clear-TokenCache.ps1 deleted file mode 100644 index 352b10176500..000000000000 --- a/ExecMaintenanceScripts/Scripts/Clear-TokenCache.ps1 +++ /dev/null @@ -1,75 +0,0 @@ -$ResourceGroup = '##RESOURCEGROUP##' -$Subscription = '##SUBSCRIPTION##' -$FunctionName = '##FUNCTIONAPP##' - -$Logo = @' - _____ _____ _____ _____ - / ____|_ _| __ \| __ \ - | | | | | |__) | |__) | - | | | | | ___/| ___/ - | |____ _| |_| | | | - \_____|_____|_| |_| - -'@ -Write-Host $Logo - -function Start-SleepProgress($Seconds) { - $doneDT = (Get-Date).AddSeconds($Seconds) - while ($doneDT -gt (Get-Date)) { - $secondsLeft = $doneDT.Subtract((Get-Date)).TotalSeconds - $percent = ($seconds - $secondsLeft) / $seconds * 100 - Write-Progress -Activity 'Sleeping' -Status 'Sleeping...' -SecondsRemaining $secondsLeft -PercentComplete $percent - [System.Threading.Thread]::Sleep(500) - } - Write-Progress -Activity 'Sleeping' -Status 'Sleeping...' -SecondsRemaining 0 -Completed -} - -Write-Host '- Connecting to Azure' -Connect-AzAccount -Identity -Subscription $Subscription | Out-Null -$Function = Get-AzFunctionApp -ResourceGroupName $ResourceGroup -Name $FunctionName -try { - Write-Host 'Phase 1: Renaming settings and stopping function app' - $Settings = $Function | Get-AzFunctionAppSetting - $Function | Update-AzFunctionAppSetting -AppSetting @{ - RefreshToken2 = $Settings.RefreshToken - } | Out-Null - $Function | Remove-AzFunctionAppSetting -AppSettingName RefreshToken | Out-Null - $Function | Remove-AzFunctionAppSetting -AppSettingName ExchangeRefreshToken | Out-Null # Leave this in to clean up ExchangeRefreshToken entry - $Function | Stop-AzFunctionApp -Force | Out-Null -} -catch { - Write-Host "Phase 1: Exception caught - $($_.Exception.Message)" - exit 1 -} - -try { - Write-Host 'Phase 2: Waiting 5 minutes and starting function app' - Start-SleepProgress -Seconds 300 - $Function | Start-AzFunctionApp | Out-Null -} -catch { - Write-Host "Phase 2: Exception caught - $($_.Exception.Message)" - exit 1 -} - -try { - Write-Host 'Phase 3: Changing settings back, stopping function app, waiting 5 minutes and restarting.' - $Settings = $Function | Get-AzFunctionAppSetting - $Function | Update-AzFunctionAppSetting -AppSetting @{ - RefreshToken = $Settings.RefreshToken2 - } | Out-Null - $Function | Stop-AzFunctionApp -Force | Out-Null - Start-SleepProgress -Seconds 300 - $Function | Start-AzFunctionApp | Out-Null - - Write-Host 'Cleaning up temporary settings' - $Function | Remove-AzFunctionAppSetting -AppSettingName RefreshToken2 | Out-Null - $Function | Remove-AzFunctionAppSetting -AppSettingName ExchangeRefreshToken2 | Out-Null # Leave this in to deal with leftover ExchangeRefreshToken2 entries - $Function | Restart-AzFunctionApp -Force | Out-Null -} -catch { - Write-Host "Phase 3: Exception caught - $($_.Exception.Message)" - exit 1 -} - -Write-Host '- Update token cache completed.' diff --git a/ExecMaintenanceScripts/Scripts/Migrate-CippStorage.ps1 b/ExecMaintenanceScripts/Scripts/Migrate-CippStorage.ps1 deleted file mode 100644 index 0621054242fd..000000000000 --- a/ExecMaintenanceScripts/Scripts/Migrate-CippStorage.ps1 +++ /dev/null @@ -1,174 +0,0 @@ -if (!(Get-Module -ListAvailable AzTable)) { - Install-Module AzTable -Confirm:$false -Force -} -$Logo = @' - _____ _____ _____ _____ - / ____|_ _| __ \| __ \ - | | | | | |__) | |__) | - | | | | | ___/| ___/ - | |____ _| |_| | | | - \_____|_____|_| |_| - -'@ -Write-Host $Logo -Write-Host '- Connecting to Azure' -Connect-AzAccount -Subscription '##SUBSCRIPTION##' -$RGName = '##RESOURCEGROUP##' -$FunctionApp = '##FUNCTIONAPP##' - -$StandardTableCols = @('PartitionKey', 'RowKey', 'TableTimestamp', 'Etag') -$FunctionStorageSettings = @('AzureWebJobsStorage', 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING') - -function Start-SleepProgress($Seconds) { - $doneDT = (Get-Date).AddSeconds($Seconds) - while ($doneDT -gt (Get-Date)) { - $secondsLeft = $doneDT.Subtract((Get-Date)).TotalSeconds - $percent = ($seconds - $secondsLeft) / $seconds * 100 - Write-Progress -Activity 'Sleeping' -Status 'Sleeping...' -SecondsRemaining $secondsLeft -PercentComplete $percent - [System.Threading.Thread]::Sleep(500) - } - Write-Progress -Activity 'Sleeping' -Status 'Sleeping...' -SecondsRemaining 0 -Completed -} - -if (Get-AzResourceGroup -Name $RGName) { - Write-Host '- Getting storage account details' - $StorageAccounts = Get-AzResource -ResourceGroupName $RGName -ResourceType 'Microsoft.Storage/storageAccounts' - $StorageV1Present = $false - $StorageV2Present = $false - - foreach ($StorageAccount in $StorageAccounts) { - switch ($StorageAccount.Kind) { - 'StorageV2' { - $StorageV2Present = $true - $SourceResource = $StorageAccount - } - 'Storage' { - $StorageV1Present = $true - $DestinationResource = $StorageAccount - } - } - } - - if ($SourceResource) { - Write-Host '- Source resource exists, getting connection string' - $saKey = (Get-AzStorageAccountKey -ResourceGroupName $RGName -Name $SourceResource.Name)[0].Value - $SourceConnectionString = 'DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix=core.windows.net' -f $SourceResource.Name, $saKey - $SourceContext = New-AzStorageContext -ConnectionString $SourceConnectionString - } - - if ($StorageV2Present -and -not $StorageV1Present) { - try { - Write-Host '- Exporting storage resource template' - Export-AzResourceGroup -ResourceGroupName $RGName -Resource $SourceResource.ResourceId - $Template = Get-Content -Path .\$RGName.json | ConvertFrom-Json - Remove-Item .\$RGName.json - - # Convert type to Storage and remove accessTier property - $Template.resources[0].kind = 'Storage' - $Template.resources[0].properties = $Template.resources[0].properties | Select-Object minimumTlsVersion, allowBlobPublicAccess, networkAcls, supportsHttpsTrafficOnly, encryption - - $DestResourceNameProp = $Template.parameters.psobject.properties.name - $DestResourceName = '{0}v1' -f $SourceResource.Name - $Parameters = @{ - $DestResourceNameProp = $DestResourceName - } - - $Template | ConvertTo-Json -Depth 100 | Out-File .\DestinationStorageTemplate.json - - Write-Host '- Importing V1 storage resource' - New-AzResourceGroupDeployment -ResourceGroupName $RGName -Name CippStorageMigration -TemplateParameterObject $Parameters -TemplateFile .\DestinationStorageTemplate.json - - $DestinationResource = Get-AzResource -ResourceGroupName $RGName -ResourceName $DestResourceName - } - catch { - Write-Host "Error detected during template deployment, waiting 5 minutes before continuing: $($_.Exception.Message)" - Start-SleepProgress -Seconds 300 - } - } - - if ($DestinationResource) { - Write-Host '- Destination resource exists, getting connection string' - $Keys = Get-AzStorageAccountKey -ResourceGroupName $RGName -Name $DestinationResource.Name - if (($Keys | Measure-Object).Count -eq 0) { - Write-Host 'Creating account key' - $Keys = New-AzStorageAccountKey -ResourceGroupName $RGName -Name $DestinationResource.Name -KeyName key1 - } - $saKey = $Keys[0].Value - $DestinationConnectionString = 'DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix=core.windows.net' -f $DestinationResource.Name, $saKey - $DestinationContext = New-AzStorageContext -ConnectionString $DestinationConnectionString - } - - if ($SourceContext -and $DestinationContext) { - Write-Host '- Migrating table data' - $SourceTables = Get-AzStorageTable -Context $SourceContext | Where-Object { $_.Name -notmatch 'AzureWebJobsHostLogs*' -and $_.Name -notmatch 'History' -and $_.Name -notmatch 'Instances' -and $_.Name -notmatch 'TestHubName' -and $_.Name -ne 'CippLogs' } - $DestinationTables = Get-AzStorageTable -Context $DestinationContext | Where-Object { $_.Name -notmatch 'AzureWebJobsHostLogs*' -and $_.Name -notmatch 'History' -and $_.Name -notmatch 'Instances' -and $_.Name -notmatch 'TestHubName' -and $_.Name -ne 'CippLogs' } - - foreach ($SourceTable in $SourceTables) { - Write-Host "`r`nTable: $($SourceTable.Name)" - $HasProperties = $false - $DestinationTable = $DestinationTables | Where-Object { $_.Name -eq $SourceTable.Name } - Write-Host 'Getting Rows' - $TableRows = Get-AzTableRow -Table $SourceTable.CloudTable - if (($TableRows | Measure-Object).Count -gt 0) { - $PropertyNames = $TableRows[0].PSObject.Properties.Name | Where-Object { $_ -notin $StandardTableCols } - if (($PropertyNames | Measure-Object).Count -gt 0) { - $HasProperties = $true - } - $RowCount = ($TableRows | Measure-Object).Count - Write-Host "Rows to migrate: $RowCount" - foreach ($Row in $TableRows) { - $AddRow = @{ - Table = $DestinationTable.CloudTable - RowKey = $Row.RowKey - PartitionKey = $Row.PartitionKey - } - if ($HasProperties) { - $Property = @{} - foreach ($PropertyName in $PropertyNames) { - $Property.$PropertyName = $Row.$PropertyName - } - $AddRow.Property = $Property - } - Add-AzTableRow @AddRow -UpdateExisting | Out-Null - } - } - else { - Write-Host 'No rows to migrate' - } - } - } - if ($DestinationConnectionString) { - Write-Host '- Getting function app' - - $Function = Get-AzFunctionApp -ResourceGroupName $RGName -Name $FunctionApp - $AppSettings = $Function.ApplicationSettings - - Write-Host 'Backing up settings' - $AppSettings | ConvertTo-Json | Out-File .\AppSettingsBackup.json -NoClobber - - Write-Host 'Changing connection strings' - foreach ($StorageSetting in $FunctionStorageSettings) { - $AppSettings.$StorageSetting = $DestinationConnectionString - } - - Write-Host 'Removing AzureWebJobsDashboard setting' - $Function | Remove-AzFunctionAppSetting -AppSettingName AzureWebJobsDashboard -Force - - Write-Host 'Updating function app' - $Function | Update-AzFunctionAppSetting -AppSetting $AppSettings - - Write-Host 'Restarting function app' - $Function | Restart-AzFunctionApp - - Write-Host 'Waiting 5 minutes before trying to sync with GitHub' - Start-SleepProgress -Seconds 300 - - Write-Host 'Synchronizing with GitHub' - & az functionapp deployment source sync --name $FunctionApp --resource-group $RGName - - Write-Host 'Done.`r`n`r`nIMPORTANT NOTE: Please rememeber to delete the StorageV2 resource once you have confirmed that the function app is running as expected.' - } -} -else { - Write-Error "Resource group '$RGName' does not exist on this account" -} \ No newline at end of file From fc2698b7e911d72e84898244e105e0cacc96bb23 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 29 Aug 2023 08:17:34 -0400 Subject: [PATCH 02/30] Update Dev Environment scripts --- Tools/Clear-DevEnvironment.ps1 | 2 +- Tools/Initialize-DevEnvironment.ps1 | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Tools/Clear-DevEnvironment.ps1 b/Tools/Clear-DevEnvironment.ps1 index 3a891ddec246..a96f369dc04b 100644 --- a/Tools/Clear-DevEnvironment.ps1 +++ b/Tools/Clear-DevEnvironment.ps1 @@ -1,4 +1,4 @@ -$EnvironmentVariables = @('TenantId', 'ApplicationId', 'ApplicationSecret', 'RefreshToken', 'AzureWebJobsStorage', 'PartnerTenantAvailable') +$EnvironmentVariables = @('TenantId', 'ApplicationId', 'ApplicationSecret', 'RefreshToken', 'AzureWebJobsStorage', 'PartnerTenantAvailable', 'SetFromProfile') ForEach ($Key in $EnvironmentVariables) { [Environment]::SetEnvironmentVariable($Key, $null) } \ No newline at end of file diff --git a/Tools/Initialize-DevEnvironment.ps1 b/Tools/Initialize-DevEnvironment.ps1 index 4a2bea36cbb1..46d6088ea4ce 100644 --- a/Tools/Initialize-DevEnvironment.ps1 +++ b/Tools/Initialize-DevEnvironment.ps1 @@ -2,7 +2,7 @@ $CippRoot = (Get-Item $PSScriptRoot).Parent.FullName ### Read the local.settings.json file and convert to a PowerShell object. $CIPPSettings = Get-Content "$CippRoot\local.settings.json" | ConvertFrom-Json | Select-Object -ExpandProperty Values ### Loop through the settings and set environment variables for each. -$ValidKeys = @('TenantId', 'ApplicationId', 'ApplicationSecret', 'RefreshToken', 'AzureWebJobsStorage', 'PartnerTenantAvailable') +$ValidKeys = @('TenantId', 'ApplicationId', 'ApplicationSecret', 'RefreshToken', 'AzureWebJobsStorage', 'PartnerTenantAvailable', 'SetFromProfile') ForEach ($Key in $CIPPSettings.PSObject.Properties.Name) { if ($ValidKeys -Contains $Key) { [Environment]::SetEnvironmentVariable($Key, $CippSettings.$Key) @@ -13,3 +13,4 @@ Import-Module "$CippRoot\GraphHelper.psm1" Import-Module "$CippRoot\Modules\AzBobbyTables" Import-Module "$CippRoot\Modules\DNSHealth" Import-Module "$CippRoot\Modules\CippQueue" +Import-Module "$CippRoot\Modules\CippCore" From 7937a27b16810da293ca04e7dea99fd4c4b76b19 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 29 Aug 2023 08:29:27 -0400 Subject: [PATCH 03/30] GraphHelper Bugfixes Get-Tenants - Get last timestamp by most recent refresh to prevent extra calls to lighthouse Get-AuthorisedRequest - Check TenantID variable against customerId and defaultDomainName properties in tenant list --- GraphHelper.psm1 | 217 ++++++++++++++++++++--------------------------- 1 file changed, 92 insertions(+), 125 deletions(-) diff --git a/GraphHelper.psm1 b/GraphHelper.psm1 index 8cb6ede0850e..bc0e1979eddf 100644 --- a/GraphHelper.psm1 +++ b/GraphHelper.psm1 @@ -39,7 +39,7 @@ function Get-NormalizedError { function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $refreshToken, $ReturnRefresh) { if (!$scope) { $scope = 'https://graph.microsoft.com/.default' } - if (!$env:setfromprofile) { $CIPPAuth = Get-CIPPAuthentication; Write-Host "Could not get Refreshtoken from environment variable. Reloading token." } + if (!$env:SetFromProfile) { $CIPPAuth = Get-CIPPAuthentication; Write-Host 'Could not get Refreshtoken from environment variable. Reloading token.' } $AuthBody = @{ client_id = $env:ApplicationID client_secret = $env:ApplicationSecret @@ -73,8 +73,7 @@ function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $refreshToken, $Retur if ($script:AccessTokens.$TokenKey -and [int](Get-Date -UFormat %s -Millisecond 0) -lt $script:AccessTokens.$TokenKey.expires_on) { Write-Host 'Graph: cached token' $AccessToken = $script:AccessTokens.$TokenKey - } - else { + } else { Write-Host 'Graph: new token' $AccessToken = (Invoke-RestMethod -Method post -Uri "https://login.microsoftonline.com/$($tenantid)/oauth2/v2.0/token" -Body $Authbody -ErrorAction Stop) $ExpiresOn = [int](Get-Date -UFormat %s -Millisecond 0) + $AccessToken.expires_in @@ -86,8 +85,7 @@ function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $refreshToken, $Retur if ($ReturnRefresh) { $header = $AccessToken } else { $header = @{ Authorization = "Bearer $($AccessToken.access_token)" } } return $header #Write-Host $header['Authorization'] - } - catch { + } catch { # Track consecutive Graph API failures $TenantsTable = Get-CippTable -tablename Tenants $Filter = "PartitionKey eq 'Tenants' and (defaultDomainName eq '{0}' or customerId eq '{0}')" -f $tenantid @@ -105,8 +103,7 @@ function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $refreshToken, $Retur $Tenant.LastGraphError = if ( $_.ErrorDetails.Message) { $msg = $_.ErrorDetails.Message | ConvertFrom-Json "$($msg.error):$($msg.error_description)" - } - else { + } else { $_.Exception.message } $Tenant.GraphErrorCount++ @@ -119,8 +116,7 @@ function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $refreshToken, $Retur function Write-LogMessage ($message, $tenant = 'None', $API = 'None', $user, $sev) { try { $username = ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($user)) | ConvertFrom-Json).userDetails - } - catch { + } catch { $username = $user } @@ -159,45 +155,43 @@ function New-GraphGetRequest { [switch]$CountOnly ) - if ($scope -eq 'ExchangeOnline') { - $AccessToken = Get-ClassicAPIToken -resource 'https://outlook.office365.com' -Tenantid $tenantid - $headers = @{ Authorization = "Bearer $($AccessToken.access_token)" } - } - else { - $headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp - } + if ((Get-AuthorisedRequest -Uri $uri -TenantID $tenantid) -or $NoAuthCheck) { + if ($scope -eq 'ExchangeOnline') { + $AccessToken = Get-ClassicAPIToken -resource 'https://outlook.office365.com' -Tenantid $tenantid + $headers = @{ Authorization = "Bearer $($AccessToken.access_token)" } + } else { + $headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp + } - if ($ComplexFilter) { - $headers['ConsistencyLevel'] = 'eventual' - } - $nextURL = $uri - - # Track consecutive Graph API failures - $TenantsTable = Get-CippTable -tablename Tenants - $Filter = "PartitionKey eq 'Tenants' and (defaultDomainName eq '{0}' or customerId eq '{0}')" -f $tenantid - $Tenant = Get-AzDataTableEntity @TenantsTable -Filter $Filter - if (!$Tenant) { - $Tenant = @{ - GraphErrorCount = 0 - LastGraphError = $null - PartitionKey = 'TenantFailed' - RowKey = 'Failed' + if ($ComplexFilter) { + $headers['ConsistencyLevel'] = 'eventual' } - } - if ((Get-AuthorisedRequest -Uri $uri -TenantID $tenantid) -or $NoAuthCheck) { + $nextURL = $uri + + # Track consecutive Graph API failures + $TenantsTable = Get-CippTable -tablename Tenants + $Filter = "PartitionKey eq 'Tenants' and (defaultDomainName eq '{0}' or customerId eq '{0}')" -f $tenantid + $Tenant = Get-AzDataTableEntity @TenantsTable -Filter $Filter + if (!$Tenant) { + $Tenant = @{ + GraphErrorCount = 0 + LastGraphError = $null + PartitionKey = 'TenantFailed' + RowKey = 'Failed' + } + } + $ReturnedData = do { try { $Data = (Invoke-RestMethod -Uri $nextURL -Method GET -Headers $headers -ContentType 'application/json; charset=utf-8') if ($CountOnly) { $Data.'@odata.count' $nextURL = $null - } - else { + } else { if ($data.value) { $data.value } else { ($Data) } if ($noPagination) { $nextURL = $null } else { $nextURL = $data.'@odata.nextLink' } } - } - catch { + } catch { $Message = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.message if ($Message -eq $null) { $Message = $($_.Exception.Message) } if ($Message -ne 'Request not applicable to target tenant.') { @@ -211,39 +205,34 @@ function New-GraphGetRequest { $Tenant.LastGraphError = '' Update-AzDataTableEntity @TenantsTable -Entity $Tenant return $ReturnedData - } - else { + } else { Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' } } function New-GraphPOSTRequest ($uri, $tenantid, $body, $type, $scope, $AsApp, $NoAuthCheck) { - - $headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp - Write-Verbose "Using $($uri) as url" - if (!$type) { - $type = 'POST' - } - if ((Get-AuthorisedRequest -Uri $uri -TenantID $tenantid) -or $NoAuthCheck) { + $headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp + Write-Verbose "Using $($uri) as url" + if (!$type) { + $type = 'POST' + } + try { $ReturnedData = (Invoke-RestMethod -Uri $($uri) -Method $TYPE -Body $body -Headers $headers -ContentType 'application/json; charset=utf-8') - } - catch { + } catch { #setting ErrorMess because the error from a failed json conversion overwrites the exception. - $ErrorMess = $($_.Exception.Message) + $ErrorMess = $($_.Exception.Message) try { $Message = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction Stop).error.message - } - catch { + } catch { $Message = $ErrorMess } - + throw $Message } return $ReturnedData - } - else { + } else { Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' } } @@ -261,8 +250,7 @@ function Get-ClassicAPIToken($tenantID, $Resource) { if ($script:classictoken.$TokenKey -and [int](Get-Date -UFormat %s -Millisecond 0) -lt $script:classictoken.$TokenKey.expires_on) { Write-Host 'Classic: cached token' return $script:classictoken.$TokenKey - } - else { + } else { Write-Host 'Using classic' $uri = "https://login.microsoftonline.com/$($TenantID)/oauth2/token" $Body = @{ @@ -276,8 +264,7 @@ function Get-ClassicAPIToken($tenantID, $Resource) { if (!$script:classictoken) { $script:classictoken = [HashTable]::Synchronized(@{}) } $script:classictoken.$TokenKey = Invoke-RestMethod $uri -Body $body -ContentType 'application/x-www-form-urlencoded' -ErrorAction SilentlyContinue -Method post return $script:classictoken.$TokenKey - } - catch { + } catch { # Track consecutive Graph API failures $TenantsTable = Get-CippTable -tablename Tenants $Filter = "PartitionKey eq 'Tenants' and (defaultDomainName eq '{0}' or customerId eq '{0}')" -f $tenantid @@ -318,14 +305,12 @@ function New-TeamsAPIGetRequest($Uri, $tenantID, $Method = 'GET', $Resource = '4 } $Data if ($noPagination) { $nextURL = $null } else { $nextURL = $data.NextLink } - } - catch { + } catch { throw "Failed to make Teams API Get Request $_" } } until ($null -eq $NextURL) return $ReturnedData - } - else { + } else { Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' } } @@ -347,14 +332,12 @@ function New-ClassicAPIGetRequest($TenantID, $Uri, $Method = 'GET', $Resource = } $Data if ($noPagination) { $nextURL = $null } else { $nextURL = $data.NextLink } - } - catch { + } catch { throw "Failed to make Classic Get Request $_" } } until ($null -eq $NextURL) return $ReturnedData - } - else { + } else { Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' } } @@ -375,13 +358,11 @@ function New-ClassicAPIPostRequest($TenantID, $Uri, $Method = 'POST', $Resource } - } - catch { + } catch { throw "Failed to make Classic Get Request $_" } return $ReturnedData - } - else { + } else { Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' } } @@ -398,10 +379,11 @@ function Get-AuthorisedRequest { if ($Uri -like 'https://graph.microsoft.com/beta/contracts*' -or $Uri -like '*/customers/*' -or $Uri -eq 'https://graph.microsoft.com/v1.0/me/sendMail' -or $Uri -like '*/tenantRelationships/*') { return $true } - if (($TenantID -ne $env:TenantId -or $env:PartnerTenantAvailable) -and (Get-Tenants -SkipList).defaultDomainName -notcontains $TenantID) { + $Tenants = Get-Tenants -IncludeErrors + $SkipList = Get-Tenants -SkipList + if (($env:PartnerTenantAvailable -eq $true -and $SkipList.customerId -notcontains $TenantID -and $SkipList.defaultDomainName -notcontains $TenantID) -or (($Tenants.customerId -contains $TenantID -or $Tenants.defaultDomainName -contains $TenantID) -and $TenantID -ne $env:TenantId)) { return $true - } - else { + } else { return $false } } @@ -426,22 +408,19 @@ function Get-Tenants { if ($IncludeAll.IsPresent) { $Filter = "PartitionKey eq 'Tenants'" - } - elseif ($IncludeErrors.IsPresent) { + } elseif ($IncludeErrors.IsPresent) { $Filter = "PartitionKey eq 'Tenants' and Excluded eq false" - } - else { + } else { $Filter = "PartitionKey eq 'Tenants' and Excluded eq false and GraphErrorCount lt 50" } $IncludedTenantsCache = Get-AzDataTableEntity @TenantsTable -Filter $Filter - $LastRefresh = ($IncludedTenantsCache | Where-Object { $_.customerId } | Sort-Object LastRefresh | Select-Object -First 1).LastRefresh | Get-Date + $LastRefresh = ($IncludedTenantsCache | Where-Object { $_.customerId } | Sort-Object LastRefresh -Descending | Select-Object -First 1).LastRefresh | Get-Date if ($LastRefresh -lt (Get-Date).Addhours(-24).ToUniversalTime()) { try { Write-Host "Renewing. Cache not hit. $LastRefresh" $TenantList = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/managedTenants/tenants?`$top=999" -tenantid $env:TenantID ) | Select-Object id, @{l = 'customerId'; e = { $_.tenantId } }, @{l = 'DefaultdomainName'; e = { [string]($_.contract.defaultDomainName) } } , @{l = 'MigratedToNewTenantAPI'; e = { $true } }, DisplayName, domains, tenantStatusInformation | Where-Object -Property defaultDomainName -NotIn $SkipListCache.defaultDomainName - } - catch { + } catch { Write-Host 'probably no license for Lighthouse. Using old API.' } if (!$TenantList.customerId) { @@ -528,8 +507,7 @@ function New-ExoRequest ($tenantid, $cmdlet, $cmdParams, $useSystemMailbox, $Anc $tenant = (get-tenants | Where-Object -Property defaultDomainName -EQ $tenantid).customerId if ($cmdParams) { $Params = $cmdParams - } - else { + } else { $Params = @{} } $ExoBody = ConvertTo-Json -Depth 5 -InputObject @{ @@ -559,21 +537,18 @@ function New-ExoRequest ($tenantid, $cmdlet, $cmdParams, $useSystemMailbox, $Anc } try { $ReturnedData = Invoke-RestMethod "https://outlook.office365.com/adminapi/beta/$($tenant)/InvokeCommand" -Method POST -Body $ExoBody -Headers $Headers -ContentType 'application/json; charset=utf-8' - } - catch { - $ErrorMess = $($_.Exception.Message) + } catch { + $ErrorMess = $($_.Exception.Message) $ReportedError = ($_.ErrorDetails | ConvertFrom-Json -ErrorAction SilentlyContinue) $Message = if ($ReportedError.error.details.message) { - $ReportedError.error.details.message - } - elseif ($ReportedError.error.message) { $ReportedError.error.message } + $ReportedError.error.details.message + } elseif ($ReportedError.error.message) { $ReportedError.error.message } else { $ReportedError.error.innererror.internalException.message } if ($null -eq $Message) { $Message = $ErrorMess } throw $Message } return $ReturnedData.value - } - else { + } else { Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' } } @@ -656,8 +631,7 @@ function Get-CIPPMSolUsers { if ($null -eq $page) { $Page = (Invoke-RestMethod -Uri 'https://provisioningapi.microsoftonline.com/provisioningwebservice.svc' -Method post -Body $MSOLXML -ContentType 'application/soap+xml; charset=utf-8').envelope.body.ListUsersResponse.listusersresult.returnvalue $Page.results.user - } - else { + } else { $Page = (Invoke-RestMethod -Uri 'https://provisioningapi.microsoftonline.com/provisioningwebservice.svc' -Method post -Body $MSOLXML -ContentType 'application/soap+xml; charset=utf-8').envelope.body.NavigateUserResultsResponse.NavigateUserResultsResult.returnvalue $Page.results.user } @@ -682,17 +656,14 @@ function New-DeviceLogin { if ($TenantID) { $ReturnCode = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$($TenantID)/oauth2/v2.0/devicecode" -Method POST -Body "client_id=$($Clientid)&scope=$encodedscope+offline_access+profile+openid" - } - else { + } else { $ReturnCode = Invoke-RestMethod -Uri 'https://login.microsoftonline.com/organizations/oauth2/v2.0/devicecode' -Method POST -Body "client_id=$($Clientid)&scope=$encodedscope+offline_access+profile+openid" } - } - else { + } else { $Checking = Invoke-RestMethod -SkipHttpErrorCheck -Uri 'https://login.microsoftonline.com/organizations/oauth2/v2.0/token' -Method POST -Body "client_id=$($Clientid)&scope=$encodedscope+offline_access+profile+openid&grant_type=device_code&device_code=$($device_code)" if ($checking.refresh_token) { $ReturnCode = $Checking - } - else { + } else { $returncode = $Checking.error } } @@ -710,8 +681,7 @@ function New-passwordString { if ($PasswordType -eq 'Correct-Battery-Horse') { $Words = Get-Content .\words.txt (Get-Random -InputObject $words -Count 4) -join '-' - } - else { + } else { -join ('abcdefghkmnrstuvwxyzABCDEFGHKLMNPRSTUVWXYZ23456789$%&*#'.ToCharArray() | Get-Random -Count $count) } } @@ -725,23 +695,23 @@ function New-GraphBulkRequest { $Requests ) - $headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp - - $URL = 'https://graph.microsoft.com/beta/$batch' - - # Track consecutive Graph API failures - $TenantsTable = Get-CippTable -tablename Tenants - $Filter = "PartitionKey eq 'Tenants' and (defaultDomainName eq '{0}' or customerId eq '{0}')" -f $tenantid - $Tenant = Get-AzDataTableEntity @TenantsTable -Filter $Filter - if (!$Tenant) { - $Tenant = @{ - GraphErrorCount = 0 - LastGraphError = $null - PartitionKey = 'TenantFailed' - RowKey = 'Failed' - } - } if ((Get-AuthorisedRequest -Uri $uri -TenantID $tenantid) -or $NoAuthCheck) { + $headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp + + $URL = 'https://graph.microsoft.com/beta/$batch' + + # Track consecutive Graph API failures + $TenantsTable = Get-CippTable -tablename Tenants + $Filter = "PartitionKey eq 'Tenants' and (defaultDomainName eq '{0}' or customerId eq '{0}')" -f $tenantid + $Tenant = Get-AzDataTableEntity @TenantsTable -Filter $Filter + if (!$Tenant) { + $Tenant = @{ + GraphErrorCount = 0 + LastGraphError = $null + PartitionKey = 'TenantFailed' + RowKey = 'Failed' + } + } try { $ReturnedData = for ($i = 0; $i -lt $Requests.count; $i += 20) { $req = @{} @@ -749,16 +719,15 @@ function New-GraphBulkRequest { $req['requests'] = ($Requests[$i..($i + 19)]) Invoke-RestMethod -Uri $URL -Method POST -Headers $headers -ContentType 'application/json; charset=utf-8' -Body ($req | ConvertTo-Json -Depth 10) } - + foreach ($MoreData in $ReturnedData.Responses | Where-Object { $_.body.'@odata.nextLink' }) { $AdditionalValues = New-GraphGetRequest -ComplexFilter -uri $MoreData.body.'@odata.nextLink' -tenantid $TenantFilter $NewValues = [System.Collections.Generic.List[PSCustomObject]]$MoreData.body.value $AdditionalValues | ForEach-Object { $NewValues.add($_) } $MoreData.body.value = $NewValues } - - } - catch { + + } catch { $Message = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.message if ($Message -eq $null) { $Message = $($_.Exception.Message) } if ($Message -ne 'Request not applicable to target tenant.') { @@ -771,10 +740,9 @@ function New-GraphBulkRequest { $Tenant.LastGraphError = '' Update-AzDataTableEntity @TenantsTable -Entity $Tenant - + return $ReturnedData.responses - } - else { + } else { Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' } } @@ -782,8 +750,7 @@ function New-GraphBulkRequest { function Get-GraphBulkResultByID ($Results, $ID, [switch]$Value) { if ($Value) { ($Results | Where-Object { $_.id -eq $ID }).body.value - } - else { + } else { ($Results | Where-Object { $_.id -eq $ID }).body } } From 08fe7a4f3100f3d6556beebd2916318f95751d56 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 29 Aug 2023 08:44:56 -0400 Subject: [PATCH 04/30] Update GraphHelper.psm1 --- GraphHelper.psm1 | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/GraphHelper.psm1 b/GraphHelper.psm1 index bc0e1979eddf..1699c4c7d879 100644 --- a/GraphHelper.psm1 +++ b/GraphHelper.psm1 @@ -287,11 +287,11 @@ function Get-ClassicAPIToken($tenantID, $Resource) { } function New-TeamsAPIGetRequest($Uri, $tenantID, $Method = 'GET', $Resource = '48ac35b8-9aa8-4d74-927d-1f4a14a0b239', $ContentType = 'application/json') { - $token = Get-ClassicAPIToken -Tenant $tenantid -Resource $Resource - - $NextURL = $Uri if ((Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { + $token = Get-ClassicAPIToken -Tenant $tenantid -Resource $Resource + + $NextURL = $Uri $ReturnedData = do { try { $Data = Invoke-RestMethod -ContentType "$ContentType;charset=UTF-8" -Uri $NextURL -Method $Method -Headers @{ @@ -316,11 +316,11 @@ function New-TeamsAPIGetRequest($Uri, $tenantID, $Method = 'GET', $Resource = '4 } function New-ClassicAPIGetRequest($TenantID, $Uri, $Method = 'GET', $Resource = 'https://admin.microsoft.com', $ContentType = 'application/json') { - $token = Get-ClassicAPIToken -Tenant $tenantID -Resource $Resource - - $NextURL = $Uri if ((Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { + $token = Get-ClassicAPIToken -Tenant $tenantID -Resource $Resource + + $NextURL = $Uri $ReturnedData = do { try { $Data = Invoke-RestMethod -ContentType "$ContentType;charset=UTF-8" -Uri $NextURL -Method $Method -Headers @{ @@ -344,9 +344,8 @@ function New-ClassicAPIGetRequest($TenantID, $Uri, $Method = 'GET', $Resource = function New-ClassicAPIPostRequest($TenantID, $Uri, $Method = 'POST', $Resource = 'https://admin.microsoft.com', $Body) { - $token = Get-ClassicAPIToken -Tenant $tenantID -Resource $Resource - if ((Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { + $token = Get-ClassicAPIToken -Tenant $tenantID -Resource $Resource try { $ReturnedData = Invoke-RestMethod -ContentType 'application/json;charset=UTF-8' -Uri $Uri -Method $Method -Body $Body -Headers @{ Authorization = "Bearer $($token.access_token)"; @@ -502,8 +501,8 @@ function Remove-CIPPCache { } function New-ExoRequest ($tenantid, $cmdlet, $cmdParams, $useSystemMailbox, $Anchor) { - $token = Get-ClassicAPIToken -resource 'https://outlook.office365.com' -Tenantid $tenantid if ((Get-AuthorisedRequest -TenantID $tenantid)) { + $token = Get-ClassicAPIToken -resource 'https://outlook.office365.com' -Tenantid $tenantid $tenant = (get-tenants | Where-Object -Property defaultDomainName -EQ $tenantid).customerId if ($cmdParams) { $Params = $cmdParams From f23b806049cd54583ffbc0f60ad5aee270befc1f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 29 Aug 2023 14:16:35 -0400 Subject: [PATCH 05/30] Get-Tenants update and bugfixes - Use bulk requests to get merged list of contracts and gdap relationships - Update Graph NoAuthCheck parameter order to prevent infinite loops on auth checks when tenant cache cleared --- GraphHelper.psm1 | 51 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/GraphHelper.psm1 b/GraphHelper.psm1 index 1699c4c7d879..4d6a1b2809f0 100644 --- a/GraphHelper.psm1 +++ b/GraphHelper.psm1 @@ -155,7 +155,7 @@ function New-GraphGetRequest { [switch]$CountOnly ) - if ((Get-AuthorisedRequest -Uri $uri -TenantID $tenantid) -or $NoAuthCheck) { + if ($NoAuthCheck -or (Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { if ($scope -eq 'ExchangeOnline') { $AccessToken = Get-ClassicAPIToken -resource 'https://outlook.office365.com' -Tenantid $tenantid $headers = @{ Authorization = "Bearer $($AccessToken.access_token)" } @@ -211,7 +211,7 @@ function New-GraphGetRequest { } function New-GraphPOSTRequest ($uri, $tenantid, $body, $type, $scope, $AsApp, $NoAuthCheck) { - if ((Get-AuthorisedRequest -Uri $uri -TenantID $tenantid) -or $NoAuthCheck) { + if ($NoAuthCheck -or (Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { $headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp Write-Verbose "Using $($uri) as url" if (!$type) { @@ -414,17 +414,47 @@ function Get-Tenants { } $IncludedTenantsCache = Get-AzDataTableEntity @TenantsTable -Filter $Filter - $LastRefresh = ($IncludedTenantsCache | Where-Object { $_.customerId } | Sort-Object LastRefresh -Descending | Select-Object -First 1).LastRefresh | Get-Date - if ($LastRefresh -lt (Get-Date).Addhours(-24).ToUniversalTime()) { + if (($IncludedTenantsCache | Measure-Object).Count -gt 0) { + try { + $LastRefresh = ($IncludedTenantsCache | Where-Object { $_.customerId } | Sort-Object LastRefresh -Descending | Select-Object -First 1).LastRefresh | Get-Date -ErrorAction Stop + } catch { $LastRefresh = $false } + } else { + $LastRefresh = $false + } + if (!$LastRefresh -or $LastRefresh -lt (Get-Date).Addhours(-24).ToUniversalTime()) { try { Write-Host "Renewing. Cache not hit. $LastRefresh" - $TenantList = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/managedTenants/tenants?`$top=999" -tenantid $env:TenantID ) | Select-Object id, @{l = 'customerId'; e = { $_.tenantId } }, @{l = 'DefaultdomainName'; e = { [string]($_.contract.defaultDomainName) } } , @{l = 'MigratedToNewTenantAPI'; e = { $true } }, DisplayName, domains, tenantStatusInformation | Where-Object -Property defaultDomainName -NotIn $SkipListCache.defaultDomainName + [System.Collections.Generic.List[PSCustomObject]]$BulkRequests = @( + @{ + id = 'Contracts' + method = 'GET' + url = "/contracts?`$top=999" + }, + @{ + id = 'GDAPRelationships' + method = 'GET' + url = '/tenantRelationships/delegatedAdminRelationships' + } + ) + + $BulkResults = New-GraphBulkRequest -Requests $BulkRequests -tenantid $TenantFilter -NoAuthCheck:$true + $Contracts = Get-GraphBulkResultByID -Results $BulkResults -ID 'Contracts' -Value + $GDAPRelationships = Get-GraphBulkResultByID -Results $BulkResults -ID 'GDAPRelationships' -Value + + $ContractList = $Contracts | Select-Object id, customerId, DefaultdomainName, DisplayName, domains, @{l = 'MigratedToNewTenantAPI'; e = { $true } }, @{ n = 'delegatedPrivilegeStatus'; exp = { $CustomerId = $_.customerId; if (($GDAPRelationships | Where-Object { $_.customer.tenantId -EQ $CustomerId -and $_.status -EQ 'active' } | Measure-Object).Count -gt 0) { 'delegatedAndGranularDelegetedAdminPrivileges' } else { 'delegatedAdminPrivileges' } } } | Where-Object -Property defaultDomainName -NotIn $SkipListCache.defaultDomainName + + $GDAPOnlyList = $GDAPRelationships | Where-Object { $_.status -eq 'active' -and $Contracts.customerId -notcontains $_.customer.tenantId } | Select-Object id, @{l = 'customerId'; e = { $($_.customer.tenantId) } }, @{l = 'defaultDomainName'; e = { (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/findTenantInformationByTenantId(tenantId='$($_.customer.tenantId)')" -noauthcheck $true -asApp:$true -tenant $env:TenantId).defaultDomainName } }, @{l = 'MigratedToNewTenantAPI'; e = { $true } }, @{n = 'displayName'; exp = { $_.customer.displayName } }, domains, @{n = 'delegatedPrivilegeStatus'; exp = { 'granularDelegatedAdminPrivileges' } } | Where-Object -Property defaultDomainName -NotIn $SkipListCache.defaultDomainName | Sort-Object -Property customerId -Unique + + $TenantList = @($ContractList) + @($GDAPOnlyList) + + #$TenantList = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/managedTenants/tenants?`$top=999" -tenantid $env:TenantID ) | Select-Object id, @{l = 'customerId'; e = { $_.tenantId } }, @{l = 'DefaultdomainName'; e = { [string]($_.contract.defaultDomainName) } } , @{l = 'MigratedToNewTenantAPI'; e = { $true } }, DisplayName, domains, tenantStatusInformation | Where-Object -Property defaultDomainName -NotIn $SkipListCache.defaultDomainName + } catch { Write-Host 'probably no license for Lighthouse. Using old API.' } - if (!$TenantList.customerId) { + <#if (!$TenantList.customerId) { $TenantList = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/contracts?`$top=999" -tenantid $env:TenantID ) | Select-Object id, customerId, DefaultdomainName, DisplayName, domains | Where-Object -Property defaultDomainName -NotIn $SkipListCache.defaultDomainName - } + }#> $IncludedTenantsCache = [system.collections.generic.list[hashtable]]::new() if ($env:PartnerTenantAvailable) { $IncludedTenantsCache.Add(@{ @@ -450,7 +480,7 @@ function Get-Tenants { customerId = [string]$Tenant.customerId defaultDomainName = [string]$Tenant.defaultDomainName displayName = [string]$Tenant.DisplayName - delegatedPrivilegeStatus = [string]$Tenant.tenantStatusInformation.delegatedPrivilegeStatus + delegatedPrivilegeStatus = [string]$Tenant.delegatedPrivilegeStatus domains = '' Excluded = $false ExcludeUser = '' @@ -694,7 +724,7 @@ function New-GraphBulkRequest { $Requests ) - if ((Get-AuthorisedRequest -Uri $uri -TenantID $tenantid) -or $NoAuthCheck) { + if ($NoAuthCheck -or (Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { $headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp $URL = 'https://graph.microsoft.com/beta/$batch' @@ -720,7 +750,8 @@ function New-GraphBulkRequest { } foreach ($MoreData in $ReturnedData.Responses | Where-Object { $_.body.'@odata.nextLink' }) { - $AdditionalValues = New-GraphGetRequest -ComplexFilter -uri $MoreData.body.'@odata.nextLink' -tenantid $TenantFilter + Write-Host 'Getting more' + $AdditionalValues = New-GraphGetRequest -ComplexFilter -uri $MoreData.body.'@odata.nextLink' -tenantid $TenantFilter -NoAuthCheck:$NoAuthCheck $NewValues = [System.Collections.Generic.List[PSCustomObject]]$MoreData.body.value $AdditionalValues | ForEach-Object { $NewValues.add($_) } $MoreData.body.value = $NewValues From 49622f40771f2a7abcf2f6794a0293ccbe6cf531 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 29 Aug 2023 16:28:54 -0400 Subject: [PATCH 06/30] Update GraphHelper.psm1 --- GraphHelper.psm1 | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/GraphHelper.psm1 b/GraphHelper.psm1 index 4d6a1b2809f0..5134be9a9e02 100644 --- a/GraphHelper.psm1 +++ b/GraphHelper.psm1 @@ -424,6 +424,10 @@ function Get-Tenants { if (!$LastRefresh -or $LastRefresh -lt (Get-Date).Addhours(-24).ToUniversalTime()) { try { Write-Host "Renewing. Cache not hit. $LastRefresh" + $TenantList = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/managedTenants/tenants?`$top=999" -tenantid $env:TenantID ) | Select-Object id, @{l = 'customerId'; e = { $_.tenantId } }, @{l = 'DefaultdomainName'; e = { [string]($_.contract.defaultDomainName) } } , @{l = 'MigratedToNewTenantAPI'; e = { $true } }, DisplayName, domains, tenantStatusInformation | Where-Object -Property defaultDomainName -NotIn $SkipListCache.defaultDomainName + + } catch { + Write-Host "Get-Tenants - Lighthouse Error, using contract/delegatedAdminRelationship calls. Error: $($_.Exception.Message)" [System.Collections.Generic.List[PSCustomObject]]$BulkRequests = @( @{ id = 'Contracts' @@ -446,11 +450,6 @@ function Get-Tenants { $GDAPOnlyList = $GDAPRelationships | Where-Object { $_.status -eq 'active' -and $Contracts.customerId -notcontains $_.customer.tenantId } | Select-Object id, @{l = 'customerId'; e = { $($_.customer.tenantId) } }, @{l = 'defaultDomainName'; e = { (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/findTenantInformationByTenantId(tenantId='$($_.customer.tenantId)')" -noauthcheck $true -asApp:$true -tenant $env:TenantId).defaultDomainName } }, @{l = 'MigratedToNewTenantAPI'; e = { $true } }, @{n = 'displayName'; exp = { $_.customer.displayName } }, domains, @{n = 'delegatedPrivilegeStatus'; exp = { 'granularDelegatedAdminPrivileges' } } | Where-Object -Property defaultDomainName -NotIn $SkipListCache.defaultDomainName | Sort-Object -Property customerId -Unique $TenantList = @($ContractList) + @($GDAPOnlyList) - - #$TenantList = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/managedTenants/tenants?`$top=999" -tenantid $env:TenantID ) | Select-Object id, @{l = 'customerId'; e = { $_.tenantId } }, @{l = 'DefaultdomainName'; e = { [string]($_.contract.defaultDomainName) } } , @{l = 'MigratedToNewTenantAPI'; e = { $true } }, DisplayName, domains, tenantStatusInformation | Where-Object -Property defaultDomainName -NotIn $SkipListCache.defaultDomainName - - } catch { - Write-Host 'probably no license for Lighthouse. Using old API.' } <#if (!$TenantList.customerId) { $TenantList = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/contracts?`$top=999" -tenantid $env:TenantID ) | Select-Object id, customerId, DefaultdomainName, DisplayName, domains | Where-Object -Property defaultDomainName -NotIn $SkipListCache.defaultDomainName From 8d61c651b2a9c9a25c1be5f64c8de8264ce88549 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 29 Aug 2023 16:59:15 -0400 Subject: [PATCH 07/30] Improve error handling for settings page - Catch errors with key vault / azure auth - Catch errors with extensions - Fix issues with empty notification config --- ExecExtensionMapping/run.ps1 | 27 ++++++++++--------- ListExtensionsConfig/run.ps1 | 6 ++++- ListNotificationConfig/run.ps1 | 14 ++++++---- .../Private/Get-GradientToken.ps1 | 27 ++++++++++--------- .../CippExtensions/Private/Get-HaloToken.ps1 | 23 +++++++++------- profile.ps1 | 14 +++++----- 6 files changed, 64 insertions(+), 47 deletions(-) diff --git a/ExecExtensionMapping/run.ps1 b/ExecExtensionMapping/run.ps1 index f070ed4f690a..b7d24f1a1b32 100644 --- a/ExecExtensionMapping/run.ps1 +++ b/ExecExtensionMapping/run.ps1 @@ -21,15 +21,17 @@ if ($Request.Query.List) { $Tenants = Get-Tenants #Get available halo clients $Table = Get-CIPPTable -TableName Extensionsconfig - $Configuration = ((Get-AzDataTableEntity @Table).config | ConvertFrom-Json).HaloPSA - $Token = Get-HaloToken -configuration $Configuration - $i = 1 - $RawHaloClients = do { - $Result = Invoke-RestMethod -Uri "$($Configuration.ResourceURL)/Client?page_no=$i&page_size=999&pageinate=true" -ContentType 'application/json' -Method GET -Headers @{Authorization = "Bearer $($token.access_token)" } - $Result.clients | Select-Object * -ExcludeProperty logo - $i++ - $pagecount = [Math]::Ceiling($Result.record_count / 999) - } while ($i -le $pagecount) + try { + $Configuration = ((Get-AzDataTableEntity @Table).config | ConvertFrom-Json -ErrorAction Stop).HaloPSA + $Token = Get-HaloToken -configuration $Configuration + $i = 1 + $RawHaloClients = do { + $Result = Invoke-RestMethod -Uri "$($Configuration.ResourceURL)/Client?page_no=$i&page_size=999&pageinate=true" -ContentType 'application/json' -Method GET -Headers @{Authorization = "Bearer $($token.access_token)" } + $Result.clients | Select-Object * -ExcludeProperty logo + $i++ + $pagecount = [Math]::Ceiling($Result.record_count / 999) + } while ($i -le $pagecount) + } catch { $RawHaloClients = @() } $HaloClients = $RawHaloClients | ForEach-Object { [PSCustomObject]@{ label = $_.name @@ -59,12 +61,11 @@ try { 'HaloPSAName' = "$($mapping.value.label)" } Add-AzDataTableEntity @Table -Entity $AddObject -Force - Write-LogMessage -API $APINAME -user $request.headers.'x-ms-client-principal' -message "Added mapping for $($mapping.name)." -Sev 'Info' + Write-LogMessage -API $APINAME -user $request.headers.'x-ms-client-principal' -message "Added mapping for $($mapping.name)." -Sev 'Info' } - $body = [pscustomobject]@{'Results' = "Successfully edited mapping table." } + $body = [pscustomobject]@{'Results' = 'Successfully edited mapping table.' } } -} -catch { +} catch { Write-LogMessage -API $APINAME -user $request.headers.'x-ms-client-principal' -message "mapping API failed. $($_.Exception.Message)" -Sev 'Error' $body = [pscustomobject]@{'Results' = "Failed. $($_.Exception.Message)" } } diff --git a/ListExtensionsConfig/run.ps1 b/ListExtensionsConfig/run.ps1 index 29a93735f7a6..821857516079 100644 --- a/ListExtensionsConfig/run.ps1 +++ b/ListExtensionsConfig/run.ps1 @@ -7,7 +7,11 @@ $APIName = $TriggerMetadata.FunctionName Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' $Table = Get-CIPPTable -TableName Extensionsconfig -$Body = (Get-AzDataTableEntity @Table).config | ConvertFrom-Json -Depth 10 +try { + $Body = (Get-AzDataTableEntity @Table).config | ConvertFrom-Json -Depth 10 -ErrorAction Stop +} catch { + $Body = @{} +} # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK diff --git a/ListNotificationConfig/run.ps1 b/ListNotificationConfig/run.ps1 index 9b89d666c283..82e382843126 100644 --- a/ListNotificationConfig/run.ps1 +++ b/ListNotificationConfig/run.ps1 @@ -8,19 +8,23 @@ Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -m $Table = Get-CIPPTable -TableName SchedulerConfig $Filter = "RowKey eq 'CippNotifications' and PartitionKey eq 'CippNotifications'" -$Config = Get-AzDataTableEntity @Table -Filter $Filter | ConvertTo-Json -Depth 10 | ConvertFrom-Json -Depth 10 -$config | Add-Member -NotePropertyValue @() -NotePropertyName 'logsToInclude' -Force +$Config = Get-AzDataTableEntity @Table -Filter $Filter +if ($Config) { + $Config = $Config | ConvertTo-Json -Depth 10 | ConvertFrom-Json -Depth 10 -AsHashtable +} else { + $Config = @{} +} +#$config | Add-Member -NotePropertyValue @() -NotePropertyName 'logsToInclude' -Force $config.logsToInclude = @(([pscustomobject]$config | Select-Object * -ExcludeProperty schedule, type, tenantid, onepertenant, sendtoIntegration, partitionkey, rowkey, tenant, ETag, email, logsToInclude, Severity, Alert, Info, Error, timestamp, webhook).psobject.properties.name) if (!$config.logsToInclude) { $config.logsToInclude = @('None') } if (!$config.Severity) { $config.Severity = @('Alert') -} -else { +} else { $config.Severity = $config.Severity -split ',' } -$body = $Config +$body = [PSCustomObject]$Config # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ diff --git a/Modules/CippExtensions/Private/Get-GradientToken.ps1 b/Modules/CippExtensions/Private/Get-GradientToken.ps1 index 97e22d4f75fe..b4b8023e990b 100644 --- a/Modules/CippExtensions/Private/Get-GradientToken.ps1 +++ b/Modules/CippExtensions/Private/Get-GradientToken.ps1 @@ -2,19 +2,22 @@ function Get-GradientToken { param( $Configuration ) - $null = Connect-AzAccount -Identity - $partnerApiKey = (Get-AzKeyVaultSecret -VaultName $ENV:WEBSITE_DEPLOYMENT_ID -Name "Gradient" -AsPlainText) - $authorizationToken = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("$($configuration.vendorKey):$($partnerApiKey)")) + if ($Configuration.vendorKey) { + $null = Connect-AzAccount -Identity + $partnerApiKey = (Get-AzKeyVaultSecret -VaultName $ENV:WEBSITE_DEPLOYMENT_ID -Name 'Gradient' -AsPlainText) + $authorizationToken = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("$($configuration.vendorKey):$($partnerApiKey)")) - $headers = [hashtable]@{ - 'Accept' = 'application/json' - 'GRADIENT-TOKEN' = $authorizationToken - } + $headers = [hashtable]@{ + 'Accept' = 'application/json' + 'GRADIENT-TOKEN' = $authorizationToken + } - try { - return [hashtable]$headers - } - catch { - Write-Error $_.Exception.Message + try { + return [hashtable]$headers + } catch { + Write-Error $_.Exception.Message + } + } catch { + throw 'No Gradient configuration' } } diff --git a/Modules/CippExtensions/Private/Get-HaloToken.ps1 b/Modules/CippExtensions/Private/Get-HaloToken.ps1 index ac66d5fec889..4bce00797491 100644 --- a/Modules/CippExtensions/Private/Get-HaloToken.ps1 +++ b/Modules/CippExtensions/Private/Get-HaloToken.ps1 @@ -1,16 +1,19 @@ function Get-HaloToken { [CmdletBinding()] param ( - $Configuration + $Configuration ) - $null = Connect-AzAccount -Identity - $body = @{ - grant_type = 'client_credentials' - client_id = $Configuration.ClientId - client_secret = (Get-AzKeyVaultSecret -VaultName $ENV:WEBSITE_DEPLOYMENT_ID -Name "HaloPSA" -AsPlainText) - scope = 'all' + if ($Configuration.ClientId) { + $null = Connect-AzAccount -Identity + $body = @{ + grant_type = 'client_credentials' + client_id = $Configuration.ClientId + client_secret = (Get-AzKeyVaultSecret -VaultName $ENV:WEBSITE_DEPLOYMENT_ID -Name 'HaloPSA' -AsPlainText) + scope = 'all' + } + $token = Invoke-RestMethod -Uri "$($Configuration.AuthURL)/token?tenant=$($Configuration.tenant)" -Method Post -Body $body -ContentType 'application/x-www-form-urlencoded' + return $token + } else { + throw 'No Halo configuration' } - $token = Invoke-RestMethod -Uri "$($Configuration.AuthURL)/token?tenant=$($Configuration.tenant)" -Method Post -Body $body -ContentType 'application/x-www-form-urlencoded' - return $token - } \ No newline at end of file diff --git a/profile.ps1 b/profile.ps1 index 1c0b9569580f..2ab564b82902 100644 --- a/profile.ps1 +++ b/profile.ps1 @@ -12,24 +12,26 @@ # Authenticate with Azure PowerShell using MSI. # Remove this if you are not planning on using MSI or Azure PowerShell. Import-Module .\GraphHelper.psm1 -Import-Module Az.KeyVault -Import-Module Az.Accounts +try { + Import-Module Az.KeyVault -ErrorAction Stop +} catch { $_.Exception.Message } +try { + Import-Module Az.Accounts +} catch { $_.Exception.Message } Import-Module GraphRequests Import-Module CippExtensions Import-Module CippCore try { Disable-AzContextAutosave -Scope Process | Out-Null -} -catch {} +} catch {} try { if (!$ENV:SetFromProfile) { Write-Host "We're reloading from KV" $Auth = Get-CIPPAuthentication } -} -catch { +} catch { Write-LogMessage -message "Could not retrieve keys from Keyvault: $($_.Exception.Message)" -Sev 'CRITICAL' } From ec64cd1d50c3a71d8446956b34c76e7acc2bce20 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Thu, 31 Aug 2023 01:49:49 +0200 Subject: [PATCH 08/30] added try catch --- ExecCPVPermissions/run.ps1 | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ExecCPVPermissions/run.ps1 b/ExecCPVPermissions/run.ps1 index 9948143c1908..8f7b9a920aee 100644 --- a/ExecCPVPermissions/run.ps1 +++ b/ExecCPVPermissions/run.ps1 @@ -58,13 +58,17 @@ $GraphRequest = $ExpectedPermissions.requiredResourceAccess | ForEach-Object { } } +try { + $ourSVCPrincipal = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals(appId='$($ENV:applicationid)')" -tenantid $Tenantfilter + $CurrentRoles = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals/$($ourSVCPrincipal.id)/appRoleAssignments" -tenantid $tenantfilter -$ourSVCPrincipal = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals(appId='$($ENV:applicationid)')" -tenantid $Tenantfilter - +} +catch { + #this try catch exists because of 500 errors when the app principal does not exist. :) +} # if the app svc principal exists, consent app permissions $apps = $ExpectedPermissions #get current roles -$CurrentRoles = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals/$($ourSVCPrincipal.id)/appRoleAssignments" -tenantid $tenantfilter #If $Grants = foreach ($App in $apps.requiredResourceAccess) { try { From 075b9b5d865bbc46f632d19be23e903e03c84908 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Thu, 31 Aug 2023 12:43:05 +0200 Subject: [PATCH 09/30] outbound fix --- .../function.json | 0 .../run.ps1 | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {Standards_OutboundSpamAlert => Standards_OutboundSpamAlert_1}/function.json (100%) rename {Standards_OutboundSpamAlert => Standards_OutboundSpamAlert_1}/run.ps1 (100%) diff --git a/Standards_OutboundSpamAlert/function.json b/Standards_OutboundSpamAlert_1/function.json similarity index 100% rename from Standards_OutboundSpamAlert/function.json rename to Standards_OutboundSpamAlert_1/function.json diff --git a/Standards_OutboundSpamAlert/run.ps1 b/Standards_OutboundSpamAlert_1/run.ps1 similarity index 100% rename from Standards_OutboundSpamAlert/run.ps1 rename to Standards_OutboundSpamAlert_1/run.ps1 From 72781aa8496afc5503472271c6a3252758f1b079 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Thu, 31 Aug 2023 12:43:55 +0200 Subject: [PATCH 10/30] typo correction --- .../function.json | 0 .../run.ps1 | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {Standards_OutboundSpamAlert_1 => Standards_OutBoundSpamAlert}/function.json (100%) rename {Standards_OutboundSpamAlert_1 => Standards_OutBoundSpamAlert}/run.ps1 (100%) diff --git a/Standards_OutboundSpamAlert_1/function.json b/Standards_OutBoundSpamAlert/function.json similarity index 100% rename from Standards_OutboundSpamAlert_1/function.json rename to Standards_OutBoundSpamAlert/function.json diff --git a/Standards_OutboundSpamAlert_1/run.ps1 b/Standards_OutBoundSpamAlert/run.ps1 similarity index 100% rename from Standards_OutboundSpamAlert_1/run.ps1 rename to Standards_OutBoundSpamAlert/run.ps1 From 33da841baf5718655af3071d71c90a073662d10b Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 3 Sep 2023 12:53:50 -0400 Subject: [PATCH 11/30] Fix lighthouse tenant list --- GraphHelper.psm1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GraphHelper.psm1 b/GraphHelper.psm1 index 5134be9a9e02..f1fb90791a84 100644 --- a/GraphHelper.psm1 +++ b/GraphHelper.psm1 @@ -424,7 +424,7 @@ function Get-Tenants { if (!$LastRefresh -or $LastRefresh -lt (Get-Date).Addhours(-24).ToUniversalTime()) { try { Write-Host "Renewing. Cache not hit. $LastRefresh" - $TenantList = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/managedTenants/tenants?`$top=999" -tenantid $env:TenantID ) | Select-Object id, @{l = 'customerId'; e = { $_.tenantId } }, @{l = 'DefaultdomainName'; e = { [string]($_.contract.defaultDomainName) } } , @{l = 'MigratedToNewTenantAPI'; e = { $true } }, DisplayName, domains, tenantStatusInformation | Where-Object -Property defaultDomainName -NotIn $SkipListCache.defaultDomainName + $TenantList = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/managedTenants/tenants?`$top=999" -tenantid $env:TenantID ) | Select-Object id, @{l = 'customerId'; e = { $_.tenantId } }, @{l = 'DefaultdomainName'; e = { [string]($_.contract.defaultDomainName) } } , @{l = 'MigratedToNewTenantAPI'; e = { $true } }, DisplayName, domains, @{n = 'delegatedPrivilegeStatus'; exp = { $_.tenantStatusInformation.delegatedPrivilegeStatus } } | Where-Object -Property defaultDomainName -NotIn $SkipListCache.defaultDomainName } catch { Write-Host "Get-Tenants - Lighthouse Error, using contract/delegatedAdminRelationship calls. Error: $($_.Exception.Message)" From a1c910ac07a5a32d0e0160dea29e472cdd1b05b7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 3 Sep 2023 12:54:19 -0400 Subject: [PATCH 12/30] Catch standards orchestrator errors --- Standards_Orchestration/run.ps1 | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/Standards_Orchestration/run.ps1 b/Standards_Orchestration/run.ps1 index b47a1cd196af..81841633afd6 100644 --- a/Standards_Orchestration/run.ps1 +++ b/Standards_Orchestration/run.ps1 @@ -1,26 +1,25 @@ param($Context) $DurableRetryOptions = @{ - FirstRetryInterval = (New-TimeSpan -Seconds 5) - MaxNumberOfAttempts = 3 - BackoffCoefficient = 2 + FirstRetryInterval = (New-TimeSpan -Seconds 5) + MaxNumberOfAttempts = 3 + BackoffCoefficient = 2 } $RetryOptions = New-DurableRetryOptions @DurableRetryOptions $Batch = (Invoke-ActivityFunction -FunctionName 'Standards_GetQueue' -Input 'LetsGo') $ParallelTasks = foreach ($Item in $Batch) { - if ($item['Standard']) { - try { - Invoke-DurableActivity -FunctionName "Standards_$($item['Standard'])" -Input "$($item['Tenant'])" -NoWait -RetryOptions $RetryOptions - } - catch { - Write-LogMessage -API 'Standards' -tenant $tenant -message "Task error: $($_.Exception.Message)" -sev Error + if ($item['Standard']) { + try { + Invoke-DurableActivity -FunctionName "Standards_$($item['Standard'])" -Input "$($item['Tenant'])" -NoWait -RetryOptions $RetryOptions -ErrorAction Stop + } catch { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Task error: $($_.Exception.Message)" -sev Error + } } - } } -if (($ParallelTasks).count -gt 0) { - $Outputs = Wait-ActivityFunction -Task $ParallelTasks - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Deployment finished.' -sev Info +if (($ParallelTasks).count -gt 0) { + $Outputs = Wait-ActivityFunction -Task $ParallelTasks + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Deployment finished.' -sev Info } From e5b8519c0fd4e1a1fc9f53a522323d1cf86c5bc5 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Wed, 6 Sep 2023 15:11:05 +0200 Subject: [PATCH 13/30] add members csv --- ListGroups/run.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ListGroups/run.ps1 b/ListGroups/run.ps1 index c7c728de28e9..8739c362b27e 100644 --- a/ListGroups/run.ps1 +++ b/ListGroups/run.ps1 @@ -13,7 +13,7 @@ Write-Host "PowerShell HTTP trigger function processed a request." # Interact with query parameters or the body of the request. $TenantFilter = $Request.Query.TenantFilter -$selectstring = "id,createdDateTime,displayName,description,mail,mailEnabled,mailNickname,resourceProvisioningOptions,securityEnabled,visibility,organizationId,onPremisesSamAccountName,membershipRule,grouptypes,onPremisesSyncEnabled,resourceProvisioningOptions,userPrincipalName" +$selectstring = "id,createdDateTime,displayName,description,mail,mailEnabled,mailNickname,resourceProvisioningOptions,securityEnabled,visibility,organizationId,onPremisesSamAccountName,membershipRule,grouptypes,onPremisesSyncEnabled,resourceProvisioningOptions,userPrincipalName&`$expand=members(`$select=userPrincipalName)" if ($Request.Query.GroupID) { $groupid = $Request.query.groupid @@ -30,6 +30,7 @@ if ($Request.Query.owners) { } try { $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups/$($GroupID)/$($members)?`$top=999&select=$selectstring" -tenantid $TenantFilter | Select-Object *, @{ Name = 'primDomain'; Expression = { $_.mail -split "@" | Select-Object -Last 1 } }, + @{Name = 'membersCsv'; Expression = { $_.members.userPrincipalName -join "," } }, @{Name = 'teamsEnabled'; Expression = { if ($_.resourceProvisioningOptions -Like '*Team*') { $true }else { $false } } }, @{Name = 'calculatedGroupType'; Expression = { From 3488c1c7c11d1a94a792c314b66de9d4c3df1c6f Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Wed, 6 Sep 2023 18:07:22 +0200 Subject: [PATCH 14/30] edit user copy from bugfix --- EditUser/run.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EditUser/run.ps1 b/EditUser/run.ps1 index faede0b1bcc0..9c414e521610 100644 --- a/EditUser/run.ps1 +++ b/EditUser/run.ps1 @@ -99,7 +99,7 @@ catch { } if ($Request.body.CopyFrom -ne "") { - $CopyFrom = Set-CIPPCopyGroupMembers -ExecutingUser $request.headers.'x-ms-client-principal' -tenantid $Userobj.tenantid -CopyFromId $Request.body.CopyFrom -UserID $user -TenantFilter $Userobj.tenantid + $CopyFrom = Set-CIPPCopyGroupMembers -ExecutingUser $request.headers.'x-ms-client-principal' -tenantid $Userobj.tenantid -CopyFromId $Request.body.CopyFrom -UserID $UserprincipalName -TenantFilter $Userobj.tenantid $results.AddRange($CopyFrom) } $body = @{"Results" = @($results) } From 9fbb0e014ec81c59a6257e4db11179c277b41344 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Wed, 6 Sep 2023 18:20:10 +0200 Subject: [PATCH 15/30] default extractFields to array --- Config/CIPPDefaultTable.BPATemplate.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Config/CIPPDefaultTable.BPATemplate.json b/Config/CIPPDefaultTable.BPATemplate.json index 7261b2b95ab6..9cdf9c2f5f9d 100644 --- a/Config/CIPPDefaultTable.BPATemplate.json +++ b/Config/CIPPDefaultTable.BPATemplate.json @@ -6,7 +6,7 @@ "name": "PasswordNeverExpires", "API": "Graph", "URL": "https://graph.microsoft.com/beta/domains", - "ExtractFields": "passwordValidityPeriodInDays", + "ExtractFields": ["passwordValidityPeriodInDays"], "where": "$_.passwordValidityPeriodInDays -eq 2147483647", "StoreAs": "bool", "FrontendFields": [ @@ -21,7 +21,7 @@ "name": "OAuthAppConsent", "API": "Graph", "URL": "https://graph.microsoft.com/v1.0/policies/authorizationPolicy?$select=defaultUserRolePermissions", - "ExtractFields": "defaultuserrolepermissions", + "ExtractFields": ["defaultuserrolepermissions"], "where": "'ManagePermissionGrantsForSelf.microsoft-user-default-legacy' -notin $_.defaultuserrolepermissions.permissionGrantPoliciesAssigned", "StoreAs": "bool", "FrontendFields": [ @@ -36,7 +36,7 @@ "name": "UnifiedAuditLog", "API": "Exchange", "Command": "Get-AdminAuditLogConfig", - "ExtractFields": "UnifiedAuditLogIngestionEnabled", + "ExtractFields": ["UnifiedAuditLogIngestionEnabled"], "StoreAs": "bool", "FrontendFields": [ { @@ -65,7 +65,7 @@ "name": "TAPEnabled", "API": "Graph", "URL": "https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/TemporaryAccessPass", - "ExtractFields": "State", + "ExtractFields": ["State"], "StoreAs": "bool", "FrontendFields": [ { @@ -79,7 +79,7 @@ "name": "SecureDefaultState", "API": "Graph", "URL": "https://graph.microsoft.com/beta/policies/identitySecurityDefaultsEnforcementPolicy", - "ExtractFields": "IsEnabled", + "ExtractFields": ["IsEnabled"], "StoreAs": "bool", "FrontendFields": [ { @@ -93,7 +93,7 @@ "name": "AnonymousPrivacyReports", "API": "Graph", "URL": "https://graph.microsoft.com/beta/admin/reportSettings", - "ExtractFields": "displayConcealedNames", + "ExtractFields": ["displayConcealedNames"], "StoreAs": "bool", "where": "$_.displayConcealedNames -eq $false", "FrontendFields": [ From 1dfef93a856ce307673b7b8345f9eb361875dc20 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Wed, 6 Sep 2023 18:21:10 +0200 Subject: [PATCH 16/30] bool --- Config/CIPPDefaultTenantPage.BPATemplate.json | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Config/CIPPDefaultTenantPage.BPATemplate.json b/Config/CIPPDefaultTenantPage.BPATemplate.json index aa4b828e4ff8..48c62139c647 100644 --- a/Config/CIPPDefaultTenantPage.BPATemplate.json +++ b/Config/CIPPDefaultTenantPage.BPATemplate.json @@ -4,7 +4,7 @@ "Fields": [ { "name": "PasswordNeverExpires", - "UseExistingInfo": "true", + "UseExistingInfo": true, "StoreAs": "bool", "FrontendFields": [ { @@ -17,7 +17,7 @@ }, { "name": "OAuthAppConsent", - "UseExistingInfo": "true", + "UseExistingInfo": true, "StoreAs": "bool", "FrontendFields": [ { @@ -30,7 +30,7 @@ }, { "name": "UnifiedAuditLog", - "UseExistingInfo": "true", + "UseExistingInfo": true, "StoreAs": "bool", "FrontendFields": [ { @@ -43,7 +43,7 @@ }, { "name": "MFANudgeState", - "UseExistingInfo": "true", + "UseExistingInfo": true, "StoreAs": "bool", "FrontendFields": [ { @@ -56,7 +56,7 @@ }, { "name": "TAPEnabled", - "UseExistingInfo": "true", + "UseExistingInfo": true, "StoreAs": "bool", "FrontendFields": [ { @@ -69,7 +69,7 @@ }, { "name": "SecureDefaultState", - "UseExistingInfo": "true", + "UseExistingInfo": true, "StoreAs": "bool", "FrontendFields": [ { @@ -82,7 +82,7 @@ }, { "name": "AnonymousPrivacyReports", - "UseExistingInfo": "true", + "UseExistingInfo": true, "StoreAs": "bool", "FrontendFields": [ { @@ -95,7 +95,7 @@ }, { "name": "MessageCopyforSentAsDisabled", - "UseExistingInfo": "true", + "UseExistingInfo": true, "StoreAs": "JSON", "FrontendFields": [ { @@ -108,7 +108,7 @@ }, { "name": "SharedMailboxeswithenabledusers", - "UseExistingInfo": "true", + "UseExistingInfo": true, "StoreAs": "JSON", "FrontendFields": [ { @@ -121,7 +121,7 @@ }, { "name": "Unusedlicenses", - "UseExistingInfo": "true", + "UseExistingInfo": true, "StoreAs": "JSON", "FrontendFields": [ { @@ -134,7 +134,7 @@ }, { "name": "CurrentSecureScore", - "UseExistingInfo": "true", + "UseExistingInfo": true, "StoreAs": "JSON", "FrontendFields": [ { From a957346603b4acb674052107beeabf5e30832877 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Wed, 6 Sep 2023 23:48:33 +0200 Subject: [PATCH 17/30] added edit template --- ExecEditTemplate/function.json | 19 ++++++++++++++++++ ExecEditTemplate/run.ps1 | 35 ++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 ExecEditTemplate/function.json create mode 100644 ExecEditTemplate/run.ps1 diff --git a/ExecEditTemplate/function.json b/ExecEditTemplate/function.json new file mode 100644 index 000000000000..306b0c51e560 --- /dev/null +++ b/ExecEditTemplate/function.json @@ -0,0 +1,19 @@ +{ + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "Request", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "Response" + } + ] +} \ No newline at end of file diff --git a/ExecEditTemplate/run.ps1 b/ExecEditTemplate/run.ps1 new file mode 100644 index 000000000000..54c3cc33635f --- /dev/null +++ b/ExecEditTemplate/run.ps1 @@ -0,0 +1,35 @@ +using namespace System.Net + +# Input bindings are passed in via param block. +param($Request, $TriggerMetadata) + +$APIName = $TriggerMetadata.FunctionName +Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Accessed this API" -Sev "Debug" + +try { + $Table = Get-CippTable -tablename 'templates' + $Table.Force = $true + $guid = $request.body.guid + $JSON = $request.body | Select-Object * -ExcludeProperty GUID | ConvertTo-Json + $Type = $request.Query.Type + Add-AzDataTableEntity @Table -Entity @{ + JSON = "$JSON" + RowKey = "$GUID" + PartitionKey = "$Type" + GUID = "$GUID" + } + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Edited template $($Request.body.name) with GUID $GUID" -Sev "Debug" + $body = [pscustomobject]@{ "Results" = "Successfully saved the template" } + +} +catch { + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Failed to edit template: $($_.Exception.Message)" -Sev "Error" + $body = [pscustomobject]@{"Results" = "Editing template failed: $($_.Exception.Message)" } +} + + +# Associate values to output bindings by calling 'Push-OutputBinding'. +Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $body + }) From 93dd371f31b1d07ba1e29754d1134bf591b9dff2 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 6 Sep 2023 21:37:59 -0400 Subject: [PATCH 18/30] ListIntuneTemplates - Fix template property error - Adjust depth for template output --- ListIntuneTemplates/run.ps1 | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ListIntuneTemplates/run.ps1 b/ListIntuneTemplates/run.ps1 index 63b32dc5af22..0990b9d06b4f 100644 --- a/ListIntuneTemplates/run.ps1 +++ b/ListIntuneTemplates/run.ps1 @@ -4,16 +4,16 @@ using namespace System.Net param($Request, $TriggerMetadata) $APIName = $TriggerMetadata.FunctionName -Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Accessed this API" -Sev "Debug" +Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' Set-Location (Get-Item $PSScriptRoot).Parent.FullName $Table = Get-CippTable -tablename 'templates' -$Templates = Get-ChildItem "Config\*.IntuneTemplate.json" | ForEach-Object { +$Templates = Get-ChildItem 'Config\*.IntuneTemplate.json' | ForEach-Object { $Entity = @{ JSON = "$(Get-Content $_)" RowKey = "$($_.name)" - PartitionKey = "IntuneTemplate" + PartitionKey = 'IntuneTemplate' GUID = "$($_.name)" } Add-AzDataTableEntity @Table -Entity $Entity -Force @@ -21,16 +21,16 @@ $Templates = Get-ChildItem "Config\*.IntuneTemplate.json" | ForEach-Object { #List new policies $Table = Get-CippTable -tablename 'templates' -$Filter = "PartitionKey eq 'IntuneTemplate'" +$Filter = "PartitionKey eq 'IntuneTemplate'" $Templates = (Get-AzDataTableEntity @Table -Filter $Filter).JSON | ConvertFrom-Json if ($Request.query.View) { $Templates = $Templates | ForEach-Object { $data = $_.RAWJson | ConvertFrom-Json - $data | Add-Member -NotePropertyName "displayName" -NotePropertyValue $_.Displayname - $data | Add-Member -NotePropertyName "description" -NotePropertyValue $_.Description - $data | Add-Member -NotePropertyName "Type" -NotePropertyValue $_.Type - $data | Add-Member -NotePropertyName "GUID" -NotePropertyValue $_.GUID - $data + $data | Add-Member -NotePropertyName 'displayName' -NotePropertyValue $_.Displayname -Force + $data | Add-Member -NotePropertyName 'description' -NotePropertyValue $_.Description -Force + $data | Add-Member -NotePropertyName 'Type' -NotePropertyValue $_.Type + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $_.GUID + $data } } @@ -40,5 +40,5 @@ if ($Request.query.ID) { $Templates = $Templates | Where-Object -Property guid - # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK - Body = @($Templates) + Body = ($Templates | ConvertTo-Json -Depth 10) }) From 0e25f0f8c86c5b3f57bbc9abfec4dcc39acfa606 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 6 Sep 2023 22:19:11 -0400 Subject: [PATCH 19/30] BPA - Error Handling - Handle empty parameters object - Compress JSON string - Set root directory for importing configurations --- BestPracticeAnalyser_All/run.ps1 | 70 +++++++++++++++----------------- ListBPA/run.ps1 | 20 ++++----- 2 files changed, 43 insertions(+), 47 deletions(-) diff --git a/BestPracticeAnalyser_All/run.ps1 b/BestPracticeAnalyser_All/run.ps1 index 9185c07e02d6..dffb59e46639 100644 --- a/BestPracticeAnalyser_All/run.ps1 +++ b/BestPracticeAnalyser_All/run.ps1 @@ -1,8 +1,8 @@ param($tenant) $TenantName = Get-Tenants | Where-Object -Property defaultDomainName -EQ $tenant -Set-Location (Get-Item $PSScriptRoot).Parent.FullName -$TemplatesLoc = Get-ChildItem "Config\*.BPATemplate.json" +$CippRoot = (Get-Item $PSScriptRoot).Parent.FullName +$TemplatesLoc = Get-ChildItem "$CippRoot\Config\*.BPATemplate.json" $Templates = $TemplatesLoc | ForEach-Object { $Template = $(Get-Content $_) | ConvertFrom-Json [PSCustomObject]@{ @@ -26,89 +26,85 @@ $AddRow = foreach ($Template in $templates) { if ($Field.Where) { $filterscript = [scriptblock]::Create($Field.Where) } else { $filterscript = { $true } } try { switch ($field.API) { - "Graph" { + 'Graph' { $paramsField = @{ uri = $field.URL tenantid = $TenantName.defaultDomainName } - if ($Field.parameters) { + if ($Field.parameters.psobject.properties.name) { $field.Parameters | ForEach-Object { Write-Host "Doing: $($_.psobject.properties.name) with value $($_.psobject.properties.value)" - $paramsField.Add($_.psobject.properties.name, $_.psobject.properties.value) + $paramsField[$_.psobject.properties.name] = $_.psobject.properties.value } } $FieldInfo = New-GraphGetRequest @paramsField | Where-Object $filterscript | Select-Object $field.ExtractFields } - "Exchange" { - if ($field.Command -notlike "get-*") { - Write-LogMessage -API "BPA" -tenant $tenant -message "The BPA only supports get- exchange commands. A set or update command was used." -sev Error + 'Exchange' { + if ($field.Command -notlike 'get-*') { + Write-LogMessage -API 'BPA' -tenant $tenant -message 'The BPA only supports get- exchange commands. A set or update command was used.' -sev Error break - } - else { + } else { $paramsField = @{ tenantid = $TenantName.defaultDomainName cmdlet = $field.Command } - if ($Field.Parameters) { $paramsfield.add('cmdparams', $field.parameters) } - $FieldInfo = New-ExoRequest @paramsField | Where-Object $filterscript | Select-Object $field.ExtractFields + if ($Field.Parameters) { $paramsfield.'cmdparams' = $field.parameters } + $FieldInfo = New-ExoRequest @paramsField | Where-Object $filterscript | Select-Object $field.ExtractFields } } - "CIPPFunction" { - if ($field.Command -notlike "get-CIPP*") { - Write-LogMessage -API "BPA" -tenant $tenant -message "The BPA only supports get-CIPP commands. A set or update command was used, or a command that is not allowed." -sev Error + 'CIPPFunction' { + if ($field.Command -notlike 'get-CIPP*') { + Write-LogMessage -API 'BPA' -tenant $tenant -message 'The BPA only supports get-CIPP commands. A set or update command was used, or a command that is not allowed.' -sev Error break } $paramsField = @{ TenantFilter = $TenantName.defaultDomainName } - if ($field.parameters) { + if ($field.parameters.psobject.properties.name) { $field.Parameters | ForEach-Object { - $paramsField.Add($_.psobject.properties.name, $_.psobject.properties.value) + $paramsField[$_.psobject.properties.name] = $_.psobject.properties.value } } - $FieldInfo = & $field.Command @paramsField | Where-Object $filterscript | Select-Object $field.ExtractFields + $FieldInfo = & $field.Command @paramsField | Where-Object $filterscript | Select-Object $field.ExtractFields } } - } - catch { + } catch { Write-Host "Error getting $($field.Name) in $($field.api) for $($TenantName.displayName) with GUID $($TenantName.customerId). Error: $($_.Exception.Message)" - Write-LogMessage -API "BPA" -tenant $tenant -message "Error getting $($field.Name) for $($TenantName.displayName) with GUID $($TenantName.customerId). Error: $($_.Exception.Message)" -sev Error - $fieldinfo = "FAILED" - $field.StoreAs = "string" - } + Write-LogMessage -API 'BPA' -tenant $tenant -message "Error getting $($field.Name) for $($TenantName.displayName) with GUID $($TenantName.customerId). Error: $($_.Exception.Message)" -sev Error + $fieldinfo = 'FAILED' + $field.StoreAs = 'string' + } try { switch -Wildcard ($field.StoreAs) { - "*bool" { + '*bool' { if ($field.ExtractFields.Count -gt 1) { - Write-LogMessage -API "BPA" -tenant $tenant -message "The BPA only supports 1 field for a bool. $($field.ExtractFields.Count) fields were specified." -sev Error + Write-LogMessage -API 'BPA' -tenant $tenant -message "The BPA only supports 1 field for a bool. $($field.ExtractFields.Count) fields were specified." -sev Error break } if ($null -eq $FieldInfo.$($field.ExtractFields)) { $FieldInfo = $false } $Result.Add($field.Name, [bool]$FieldInfo.$($field.ExtractFields)) } - "JSON" { - if ($FieldInfo -eq $null) { $JsonString = '{}' } else { $JsonString = (ConvertTo-Json -Depth 15 -InputObject $FieldInfo) } + 'JSON' { + if ($FieldInfo -eq $null) { $JsonString = '{}' } else { $JsonString = (ConvertTo-Json -Depth 15 -InputObject $FieldInfo -Compress) } $Result.Add($field.Name, $JSONString) } - "string" { + 'string' { $Result.Add($field.Name, [string]$FieldInfo) } } - } - catch { - Write-LogMessage -API "BPA" -tenant $tenant -message "Error storing $($field.Name) for $($TenantName.displayName) with GUID $($TenantName.customerId). Error: $($_.Exception.Message)" -sev Error - $Result.Add($field.Name, "FAILED") + } catch { + Write-LogMessage -API 'BPA' -tenant $tenant -message "Error storing $($field.Name) for $($TenantName.displayName) with GUID $($TenantName.customerId). Error: $($_.Exception.Message)" -sev Error + $Result.Add($field.Name, 'FAILED') } } - + if ($Result) { try { Add-AzDataTableEntity @Table -Entity $Result -Force - } - catch { - Write-LogMessage -API "BPA" -tenant $tenant -message "Error getting saving data for $($template.Name) - $($TenantName.customerId). Error: $($_.Exception.Message)" -sev Error + } catch { + Write-LogMessage -API 'BPA' -tenant $tenant -message "Error getting saving data for $($template.Name) - $($TenantName.customerId). Error: $($_.Exception.Message)" -sev Error } } diff --git a/ListBPA/run.ps1 b/ListBPA/run.ps1 index cd6b5b450bb0..0fae94c1b8a8 100644 --- a/ListBPA/run.ps1 +++ b/ListBPA/run.ps1 @@ -8,12 +8,13 @@ $APIName = $TriggerMetadata.FunctionName $Table = get-cipptable 'cachebpav2' $name = $Request.query.Report -if ($name -eq $null) { $name = "CIPP Best Practices v1.0 - Table view" } +if ($name -eq $null) { $name = 'CIPP Best Practices v1.0 - Table view' } # Get all possible JSON files for reports, find the correct one, select the Columns $JSONFields = @() $Columns = $null -(Get-ChildItem -Path "Config\*.BPATemplate.json" -Recurse | Select-Object -ExpandProperty FullName | ForEach-Object { +$CippRoot = (Get-Item $PSScriptRoot).Parent.FullName +(Get-ChildItem -Path "$CippRoot\Config\*.BPATemplate.json" -Recurse | Select-Object -ExpandProperty FullName | ForEach-Object { $Template = $(Get-Content $_) | ConvertFrom-Json if ($Template.Name -eq $NAME) { $JSONFields = $Template.Fields | Where-Object { $_.StoreAs -eq 'JSON' } | ForEach-Object { $_.name } @@ -23,14 +24,14 @@ $Columns = $null }) -if ($Request.query.tenantFilter -ne "AllTenants" -and $Style -eq "Tenant") { +if ($Request.query.tenantFilter -ne 'AllTenants' -and $Style -eq 'Tenant') { $mergedObject = New-Object pscustomobject $Data = (Get-AzDataTableEntity @Table -Filter "PartitionKey eq '$($Request.query.tenantFilter)'") | ForEach-Object { $row = $_ - $JSONFields | ForEach-Object { + $JSONFields | ForEach-Object { $jsonContent = $row.$_ - if ($jsonContent -ne $null -and $jsonContent -ne "FAILED") { + if ($jsonContent -ne $null -and $jsonContent -ne 'FAILED') { $row.$_ = $jsonContent | ConvertFrom-Json -Depth 15 } } @@ -40,13 +41,12 @@ if ($Request.query.tenantFilter -ne "AllTenants" -and $Style -eq "Tenant") { } $Data = $mergedObject -} -else { +} else { $Data = (Get-AzDataTableEntity @Table -Filter "RowKey eq '$NAME'") | ForEach-Object { $row = $_ $JSONFields | ForEach-Object { $jsonContent = $row.$_ - if ($jsonContent -ne $null -and $jsonContent -ne "FAILED") { + if ($jsonContent -ne $null -and $jsonContent -ne 'FAILED') { $row.$_ = $jsonContent | ConvertFrom-Json -Depth 15 } } @@ -64,8 +64,8 @@ $Results = [PSCustomObject]@{ if (!$Results) { $Results = @{ - Columns = @( value = "Results"; name = "Results") - Data = @(@{ Results = "The BPA has not yet run." }) + Columns = @( value = 'Results'; name = 'Results') + Data = @(@{ Results = 'The BPA has not yet run.' }) } } From 68d06351a098d66e8572fb5cc96efd7987752075 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 6 Sep 2023 22:19:38 -0400 Subject: [PATCH 20/30] Create Get-CIPPTenantCapabilities.ps1 --- .../Public/Get-CIPPTenantCapabilities.ps1 | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 Modules/CIPPCore/Public/Get-CIPPTenantCapabilities.ps1 diff --git a/Modules/CIPPCore/Public/Get-CIPPTenantCapabilities.ps1 b/Modules/CIPPCore/Public/Get-CIPPTenantCapabilities.ps1 new file mode 100644 index 000000000000..3db9c3a60562 --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPTenantCapabilities.ps1 @@ -0,0 +1,18 @@ + +function Get-CIPPTenantCapabilities { + [CmdletBinding()] + param ( + $TenantFilter, + $APIName = 'Get Tenant Capabilities', + $ExecutingUser + ) + + $Org = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/organization' -tenantid $TenantFilter + $Plans = $Org.assignedPlans | Where-Object { $_.capabilityStatus -eq 'Enabled' } | Sort-Object -Property service -Unique | Select-Object capabilityStatus, service + + $Results = @{} + foreach ($Plan in $Plans) { + $Results."$($Plan.service)" = $Plan.capabilityStatus -eq 'Enabled' + } + [PSCustomObject]$Results +} From af91fb4e3ead25122126baddb80273c596021e02 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Thu, 7 Sep 2023 11:08:38 +0200 Subject: [PATCH 21/30] added multi select to offboarding wizard --- ExecOffboardUser/run.ps1 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ExecOffboardUser/run.ps1 b/ExecOffboardUser/run.ps1 index db3860f72828..745b714d2b63 100644 --- a/ExecOffboardUser/run.ps1 +++ b/ExecOffboardUser/run.ps1 @@ -34,13 +34,14 @@ try { } { $_."OnedriveAccess" -ne "" } { - Set-CIPPOnedriveAccess -tenantFilter $tenantFilter -userid $username -OnedriveAccessUser $request.body.OnedriveAccess -ExecutingUser $request.headers.'x-ms-client-principal' -APIName "ExecOffboardUser" + $request.body.OnedriveAccess | ForEach-Object { Set-CIPPOnedriveAccess -tenantFilter $tenantFilter -userid $username -OnedriveAccessUser $_.value -ExecutingUser $request.headers.'x-ms-client-principal' -APIName "ExecOffboardUser" } } + { $_."AccessNoAutomap" -ne "" } { - Set-CIPPMailboxAccess -tenantFilter $tenantFilter -userid $username -AccessUser $request.body.AccessNoAutomap -Automap $true -AccessRights @("FullAccess") -ExecutingUser $request.headers.'x-ms-client-principal' -APIName "ExecOffboardUser" + $request.body.AccessNoAutomap | ForEach-Object { Set-CIPPMailboxAccess -tenantFilter $tenantFilter -userid $username -AccessUser $_.value -Automap $true -AccessRights @("FullAccess") -ExecutingUser $request.headers.'x-ms-client-principal' -APIName "ExecOffboardUser" } } { $_."AccessAutomap" -ne "" } { - Set-CIPPMailboxAccess -tenantFilter $tenantFilter -userid $username -AccessUser $request.body.AccessNoAutomap -Automap $false -AccessRights @("FullAccess") -ExecutingUser $request.headers.'x-ms-client-principal' -APIName "ExecOffboardUser" + $request.body.AccessNoAutomap | ForEach-Object { Set-CIPPMailboxAccess -tenantFilter $tenantFilter -userid $username -AccessUser $_.value -Automap $false -AccessRights @("FullAccess") -ExecutingUser $request.headers.'x-ms-client-principal' -APIName "ExecOffboardUser" } } { $_."OOO" -ne "" } { From 7aacca070dce55c51a968a79a1decc328c22195d Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Thu, 7 Sep 2023 13:38:10 +0200 Subject: [PATCH 22/30] corrected anchor mailbox and used user --- Modules/CIPPCore/Public/Remove-CIPPMobileDevice.ps1 | 2 +- Modules/CIPPCore/Public/Remove-CIPPRules.ps1 | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Remove-CIPPMobileDevice.ps1 b/Modules/CIPPCore/Public/Remove-CIPPMobileDevice.ps1 index 78625e367fbd..6f65ff4da439 100644 --- a/Modules/CIPPCore/Public/Remove-CIPPMobileDevice.ps1 +++ b/Modules/CIPPCore/Public/Remove-CIPPMobileDevice.ps1 @@ -9,7 +9,7 @@ function Remove-CIPPMobileDevice { ) try { - $devices = New-ExoRequest -tenantid $tenantFilter -cmdlet "Get-MobileDevice" -Anchor $username -cmdParams @{mailbox = $userid } | ForEach-Object { + $devices = New-ExoRequest -tenantid $tenantFilter -cmdlet "Get-MobileDevice" -Anchor $username -cmdParams @{mailbox = $username } | ForEach-Object { try { New-ExoRequest -tenantid $tenantFilter -cmdlet "Remove-MobileDevice" -Anchor $username -cmdParams @{Identity = $_.Identity } "Removed device: $($_.FriendlyName)" diff --git a/Modules/CIPPCore/Public/Remove-CIPPRules.ps1 b/Modules/CIPPCore/Public/Remove-CIPPRules.ps1 index c83c334d0bfd..c7e481bd0943 100644 --- a/Modules/CIPPCore/Public/Remove-CIPPRules.ps1 +++ b/Modules/CIPPCore/Public/Remove-CIPPRules.ps1 @@ -9,14 +9,16 @@ function Remove-CIPPRules { ) try { - $rules = New-ExoRequest -tenantid $TenantFilter -cmdlet "Get-InboxRule" -cmdParams @{mailbox = $userid } + Write-Host "Checking rules for $username" + $rules = New-ExoRequest -tenantid $TenantFilter -cmdlet "Get-InboxRule" -cmdParams @{mailbox = $username } + Write-Host "$($rules.count) rules found" if ($rules -eq $null) { Write-LogMessage -user $ExecutingUser -API $APIName -message "No Rules for $($username) to delete" -Sev "Info" -tenant $TenantFilter return "No rules for $($username) to delete" } else { ForEach ($rule in $rules) { - New-ExoRequest -tenantid $TenantFilter -cmdlet "Remove-InboxRule" -Anchor $userid -cmdParams @{Identity = $rule.Identity } + New-ExoRequest -tenantid $TenantFilter -cmdlet "Remove-InboxRule" -Anchor $username -cmdParams @{Identity = $rule.Identity } } Write-LogMessage -user $ExecutingUser -API $APIName -message "Deleted Rules for $($username)" -Sev "Info" -tenant $TenantFilter return "Deleted Rules for $($username)" From 8c5169ccb51cbaf15b29219713a434a47f0294df Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 7 Sep 2023 09:23:42 -0400 Subject: [PATCH 23/30] ListBPATemplates - Add option for Raw JSON reports --- ListBPATemplates/run.ps1 | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/ListBPATemplates/run.ps1 b/ListBPATemplates/run.ps1 index b00d60b7854e..53b7a23034fc 100644 --- a/ListBPATemplates/run.ps1 +++ b/ListBPATemplates/run.ps1 @@ -4,23 +4,30 @@ using namespace System.Net param($Request, $TriggerMetadata) $APIName = $TriggerMetadata.FunctionName -Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Accessed this API" -Sev "Debug" +Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' -Write-Host "PowerShell HTTP trigger function processed a request." +Write-Host 'PowerShell HTTP trigger function processed a request.' Write-Host $Request.query.id Set-Location (Get-Item $PSScriptRoot).Parent.FullName -$Templates = Get-ChildItem "Config\*.BPATemplate.json" -$Templates = $Templates | ForEach-Object { - $Template = $(Get-Content $_) | ConvertFrom-Json - @{ - Data = $Template.fields - Name = $Template.Name - Style = $Template.Style +$Templates = Get-ChildItem 'Config\*.BPATemplate.json' + +if ($Request.Query.RawJson) { + $Templates = $Templates | ForEach-Object { + $(Get-Content $_) | ConvertFrom-Json + } +} else { + $Templates = $Templates | ForEach-Object { + $Template = $(Get-Content $_) | ConvertFrom-Json + @{ + Data = $Template.fields + Name = $Template.Name + Style = $Template.Style + } } } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK - Body = @($Templates) + Body = ($Templates | ConvertTo-Json -Depth 10) }) From 6adf27f0b9ffaa63e0be0d16cd3a2e5b7c6a14f3 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Thu, 7 Sep 2023 18:29:02 +0200 Subject: [PATCH 24/30] multi group improvements. --- AddGroup/run.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AddGroup/run.ps1 b/AddGroup/run.ps1 index 7fe3680cb7fc..cc6798edd0eb 100644 --- a/AddGroup/run.ps1 +++ b/AddGroup/run.ps1 @@ -8,7 +8,7 @@ Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME - $groupobj = $Request.body $SelectedTenants = if ($Request.body.selectedTenants) { $request.body.selectedTenants.defaultDomainName } else { $Request.body.tenantid } - +if ("AllTenants" -in $SelectedTenants) { $SelectedTenants = (Get-Tenants).defaultDomainName } # Write to the Azure Functions log stream. Write-Host "PowerShell HTTP trigger function processed a request." @@ -44,13 +44,13 @@ $results = foreach ($tenant in $SelectedTenants) { } $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet "New-DistributionGroup" -cmdParams $params } - "Successfully created group." + "Successfully created group $($groupobj.displayname) for $($tenant)" Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message "Created group $($groupobj.displayname) with id $($GraphRequest.id) " -Sev "Info" } catch { Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message "Group creation API failed. $($_.Exception.Message)" -Sev "Error" - "Failed to create group. $($_.Exception.Message)" + "Failed to create group. $($groupobj.displayname) for $($tenant) $($_.Exception.Message)" } } From 569c730be1e10eb397e1180558eee177b08bdf77 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Thu, 7 Sep 2023 21:10:08 +0200 Subject: [PATCH 25/30] fixed bug with empty properties in policy --- Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 | 23 ++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index d43bc31e0ec4..dac3922fe494 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -33,18 +33,22 @@ function New-CIPPCAPolicy { } } } - $displayname = ($RawJSON | ConvertFrom-Json).Displayname $JSONObj = $RawJSON | ConvertFrom-Json | Select-Object * -ExcludeProperty ID, GUID, *time* Remove-EmptyArrays $JSONObj #Remove context as it does not belong in the payload. - $JsonObj.grantControls.PSObject.Properties.Remove('authenticationStrength@odata.context') - if ($JSONObj.conditions.users.excludeGuestsOrExternalUsers.externalTenants.Members) { - $JsonObj.conditions.users.excludeGuestsOrExternalUsers.externalTenants.PSObject.Properties.Remove('@odata.context') + try { + $JsonObj.grantControls.PSObject.Properties.Remove('authenticationStrength@odata.context') + if ($JSONObj.conditions.users.excludeGuestsOrExternalUsers.externalTenants.Members) { + $JsonObj.conditions.users.excludeGuestsOrExternalUsers.externalTenants.PSObject.Properties.Remove('@odata.context') + } + if ($State -and $State -ne 'donotchange') { + $Jsonobj.state = $State + } } - if ($State -and $State -ne 'donotchange') { - $Jsonobj.state = $State + catch { + # no issues here. } #for each of the locations, check if they exist, if not create them. These are in $jsonobj.LocationInfo @@ -69,7 +73,8 @@ function New-CIPPCAPolicy { } } } - Write-Host ($LocationLookupTable | ConvertTo-Json) + Write-Host "here5" + foreach ($location in $JSONObj.conditions.locations.includeLocations) { Write-Host "Replacting $location" $lookup = $LocationLookupTable | Where-Object -Property name -EQ $location @@ -112,7 +117,7 @@ function New-CIPPCAPolicy { } } catch { - throw $_ - Write-LogMessage -API "Standards" -tenant $tenant -message "Failed to create or update conditional access rule $($JSONObj.displayName): $($_.exception.message)" -sev "Error" + throw "Failed to create or update conditional access rule $($JSONObj.displayName): $($_.exception.message)" + Write-LogMessage -API "Standards" -tenant $tenant -message "Failed to create or update conditional access rule $($JSONObj.displayName): $($_.exception.message) " -sev "Error" } } From 1b7fc36bf51a35871a49cf6d35c8d93be76990bf Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Fri, 8 Sep 2023 00:24:06 +0200 Subject: [PATCH 26/30] New create onedrive shortcut --- Cache_SAMSetup/SAMManifest.json | 1 + ExecOneDriveShortCut/function.json | 19 ++++++++++ ExecOneDriveShortCut/run.ps1 | 28 ++++++++++++++ .../Public/New-CIPPOneDriveShortCut.ps1 | 38 +++++++++++++++++++ 4 files changed, 86 insertions(+) create mode 100644 ExecOneDriveShortCut/function.json create mode 100644 ExecOneDriveShortCut/run.ps1 create mode 100644 Modules/CIPPCore/Public/New-CIPPOneDriveShortCut.ps1 diff --git a/Cache_SAMSetup/SAMManifest.json b/Cache_SAMSetup/SAMManifest.json index 0520249e1663..ef0dfd36758c 100644 --- a/Cache_SAMSetup/SAMManifest.json +++ b/Cache_SAMSetup/SAMManifest.json @@ -149,6 +149,7 @@ { "id": "2a60023f-3219-47ad-baa4-40e17cd02a1d", "type": "Role" }, { "id": "338163d7-f101-4c92-94ba-ca46fe52447c", "type": "Role" }, { "id": "cac88765-0581-4025-9725-5ebc13f729ee", "type": "Role" }, + { "id": "75359482-378d-4052-8f01-80520e7db3cd", "type": "Role" }, { "id": "b27a61ec-b99c-4d6a-b126-c4375d08ae30", "type": "Scope" }, { "id": "84bccea3-f856-4a8a-967b-dbe0a3d53a64", "type": "Scope" }, { "id": "280b3b69-0437-44b1-bc20-3b2fca1ee3e9", "type": "Scope" }, diff --git a/ExecOneDriveShortCut/function.json b/ExecOneDriveShortCut/function.json new file mode 100644 index 000000000000..306b0c51e560 --- /dev/null +++ b/ExecOneDriveShortCut/function.json @@ -0,0 +1,19 @@ +{ + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "Request", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "Response" + } + ] +} \ No newline at end of file diff --git a/ExecOneDriveShortCut/run.ps1 b/ExecOneDriveShortCut/run.ps1 new file mode 100644 index 000000000000..2e318eca9042 --- /dev/null +++ b/ExecOneDriveShortCut/run.ps1 @@ -0,0 +1,28 @@ +using namespace System.Net + +# Input bindings are passed in via param block. +param($Request, $TriggerMetadata) + +$APIName = $TriggerMetadata.FunctionName +Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Accessed this API" -Sev "Debug" + + +# Write to the Azure Functions log stream. +Write-Host "PowerShell HTTP trigger function processed a request." + + +# Interact with query parameters or the body of the request. +Try { + $MessageCopyForSentAsEnabled = if ($request.query.MessageCopyForSentAsEnabled -eq 'false') { "false" } else { "true" } + $MessageResult = Set-CIPPMessageCopy -userid $Request.query.id -tenantFilter $Request.query.TenantFilter -APIName $APINAME -ExecutingUser $request.headers.'x-ms-client-principal' -MessageCopyForSentAsEnabled $MessageCopyForSentAsEnabled + $Results = [pscustomobject]@{"Results" = "$MessageResult" } +} +catch { + $Results = [pscustomobject]@{"Results" = "set MessageCopyForSentAsEnabled to $MessageCopyForSentAsEnabled failed - $($_.Exception.Message)" } +} + +# Associate values to output bindings by calling 'Push-OutputBinding'. +Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $Results + }) diff --git a/Modules/CIPPCore/Public/New-CIPPOneDriveShortCut.ps1 b/Modules/CIPPCore/Public/New-CIPPOneDriveShortCut.ps1 new file mode 100644 index 000000000000..6a5539ff598f --- /dev/null +++ b/Modules/CIPPCore/Public/New-CIPPOneDriveShortCut.ps1 @@ -0,0 +1,38 @@ + +function New-CIPPOneDriveShortCut { + [CmdletBinding()] + param ( + $username, + $userid, + $URL, + $TenantFilter, + $APIName = "Create OneDrive shortcut", + $ExecutingUser + ) + + try { + $SiteInfo = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/sites/" -tenantid $TenantFilter -asapp $true | Where-Object -Property weburl -EQ $url + $ListItemUniqueId = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/sites/$($siteInfo.id)/drive?`$select=SharepointIds" -tenantid $TenantFilter -asapp $true).SharePointIds + $body = [PSCustomObject]@{ + name = "$($SiteInfo.displayName)" + remoteItem = @{ + sharepointIds = @{ + listId = $($ListItemUniqueId.listid) + listItemUniqueId = "root" + siteId = $($ListItemUniqueId.siteId) + siteUrl = $($ListItemUniqueId.siteUrl) + webId = $($ListItemUniqueId.webId) + } + } + '@microsoft.graph.conflictBehavior' = "rename" + } | ConvertTo-Json -Depth 10 + New-GraphPOSTRequest -method POST "https://graph.microsoft.com/beta/users/$userid/drive/root/children" -body $body -tenantid $TenantFilter -asapp $true + return "Succesfully created Shortcut in OneDrive for $username using $url" + } + catch { + Write-LogMessage -message "Could not add Onedrive shortcut to $username : $($_.Exception.Message)" -Sev 'error' -API $APIName -user $ExecutingUser + return $false + } +} + + From be8ca10580a2b1e390ef602b0428a25cb2adec30 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Fri, 8 Sep 2023 00:28:28 +0200 Subject: [PATCH 27/30] fix function --- ExecOneDriveShortCut/run.ps1 | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/ExecOneDriveShortCut/run.ps1 b/ExecOneDriveShortCut/run.ps1 index 2e318eca9042..439909c1182c 100644 --- a/ExecOneDriveShortCut/run.ps1 +++ b/ExecOneDriveShortCut/run.ps1 @@ -6,19 +6,12 @@ param($Request, $TriggerMetadata) $APIName = $TriggerMetadata.FunctionName Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Accessed this API" -Sev "Debug" - -# Write to the Azure Functions log stream. -Write-Host "PowerShell HTTP trigger function processed a request." - - -# Interact with query parameters or the body of the request. Try { - $MessageCopyForSentAsEnabled = if ($request.query.MessageCopyForSentAsEnabled -eq 'false') { "false" } else { "true" } - $MessageResult = Set-CIPPMessageCopy -userid $Request.query.id -tenantFilter $Request.query.TenantFilter -APIName $APINAME -ExecutingUser $request.headers.'x-ms-client-principal' -MessageCopyForSentAsEnabled $MessageCopyForSentAsEnabled - $Results = [pscustomobject]@{"Results" = "$MessageResult" } + $MessageResult = New-CIPPOneDriveShortCut -userid $Request.query.id -TenantFilter $Request.query.TenantFilter -URL $Request.query.URL -ExecutingUser $request.headers.'x-ms-client-principal' + $Results = [pscustomobject]@{ "Results" = "$MessageResult" } } catch { - $Results = [pscustomobject]@{"Results" = "set MessageCopyForSentAsEnabled to $MessageCopyForSentAsEnabled failed - $($_.Exception.Message)" } + $Results = [pscustomobject]@{"Results" = "Onedrive Shortcut creation failed: $($_.Exception.Message)" } } # Associate values to output bindings by calling 'Push-OutputBinding'. From 0698ea81cf1a4f1f41b900e206dd4f6ebe8b219c Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Fri, 8 Sep 2023 00:29:21 +0200 Subject: [PATCH 28/30] add username --- ExecOneDriveShortCut/run.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ExecOneDriveShortCut/run.ps1 b/ExecOneDriveShortCut/run.ps1 index 439909c1182c..becfeaafcd7c 100644 --- a/ExecOneDriveShortCut/run.ps1 +++ b/ExecOneDriveShortCut/run.ps1 @@ -7,7 +7,7 @@ $APIName = $TriggerMetadata.FunctionName Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Accessed this API" -Sev "Debug" Try { - $MessageResult = New-CIPPOneDriveShortCut -userid $Request.query.id -TenantFilter $Request.query.TenantFilter -URL $Request.query.URL -ExecutingUser $request.headers.'x-ms-client-principal' + $MessageResult = New-CIPPOneDriveShortCut -username $username -userid $Request.query.id -TenantFilter $Request.query.TenantFilter -URL $Request.query.URL -ExecutingUser $request.headers.'x-ms-client-principal' $Results = [pscustomobject]@{ "Results" = "$MessageResult" } } catch { From 3da9c57dce858d33695dd02700e50a80e79655cb Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Fri, 8 Sep 2023 01:06:35 +0200 Subject: [PATCH 29/30] sharepoint url stuff --- ExecOneDriveShortCut/run.ps1 | 2 +- Modules/CIPPCore/Public/New-CIPPOneDriveShortCut.ps1 | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ExecOneDriveShortCut/run.ps1 b/ExecOneDriveShortCut/run.ps1 index becfeaafcd7c..ddb4566d600a 100644 --- a/ExecOneDriveShortCut/run.ps1 +++ b/ExecOneDriveShortCut/run.ps1 @@ -7,7 +7,7 @@ $APIName = $TriggerMetadata.FunctionName Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Accessed this API" -Sev "Debug" Try { - $MessageResult = New-CIPPOneDriveShortCut -username $username -userid $Request.query.id -TenantFilter $Request.query.TenantFilter -URL $Request.query.URL -ExecutingUser $request.headers.'x-ms-client-principal' + $MessageResult = New-CIPPOneDriveShortCut -username $Request.body.username -userid $Request.body.userid -TenantFilter $Request.Body.TenantFilter -URL $Request.body.input -ExecutingUser $request.headers.'x-ms-client-principal' $Results = [pscustomobject]@{ "Results" = "$MessageResult" } } catch { diff --git a/Modules/CIPPCore/Public/New-CIPPOneDriveShortCut.ps1 b/Modules/CIPPCore/Public/New-CIPPOneDriveShortCut.ps1 index 6a5539ff598f..e140b1e6052c 100644 --- a/Modules/CIPPCore/Public/New-CIPPOneDriveShortCut.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPOneDriveShortCut.ps1 @@ -9,7 +9,7 @@ function New-CIPPOneDriveShortCut { $APIName = "Create OneDrive shortcut", $ExecutingUser ) - + Write-Host "Received $username and $userid. We're using $url and $TenantFilter" try { $SiteInfo = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/sites/" -tenantid $TenantFilter -asapp $true | Where-Object -Property weburl -EQ $url $ListItemUniqueId = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/sites/$($siteInfo.id)/drive?`$select=SharepointIds" -tenantid $TenantFilter -asapp $true).SharePointIds @@ -26,12 +26,13 @@ function New-CIPPOneDriveShortCut { } '@microsoft.graph.conflictBehavior' = "rename" } | ConvertTo-Json -Depth 10 - New-GraphPOSTRequest -method POST "https://graph.microsoft.com/beta/users/$userid/drive/root/children" -body $body -tenantid $TenantFilter -asapp $true - return "Succesfully created Shortcut in OneDrive for $username using $url" + New-GraphPOSTRequest -method POST "https://graph.microsoft.com/beta/users/$username/drive/root/children" -body $body -tenantid $TenantFilter -asapp $true + Write-LogMessage -message "Created OneDrive shortcut called $($SiteInfo.displayName) for $($username)" -Sev 'info' -API $APIName -user $ExecutingUser + return "Created OneDrive Shortcut for $username called $($SiteInfo.displayName) " } catch { Write-LogMessage -message "Could not add Onedrive shortcut to $username : $($_.Exception.Message)" -Sev 'error' -API $APIName -user $ExecutingUser - return $false + return "Could not add Onedrive shortcut to $username : $($_.Exception.Message)" } } From 66d3ee86ce339893e6187e727d014d22d55db2db Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Fri, 8 Sep 2023 16:52:35 +0200 Subject: [PATCH 30/30] latest release --- version_latest.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version_latest.txt b/version_latest.txt index 2582dddfd549..ef8d7569d677 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -4.1.1 \ No newline at end of file +4.2.0 \ No newline at end of file