From 64cd24079b48b68af86c65da42dc312d2bfb2ef8 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 5 Jul 2024 09:54:07 -0400 Subject: [PATCH 1/3] Update Set-CIPPSPOTenant.ps1 --- Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 b/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 index 383a65a5b854..ad6a9b115321 100644 --- a/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 @@ -57,10 +57,14 @@ function Set-CIPPSPOTenant { # Get property type $PropertyType = $Properties[$Property].GetType().Name if ($PropertyType -in $AllowedTypes) { - if ($PropertyType -eq 'Boolean') { $Properties[$Property] = $Properties[$Property].ToString().ToLower() } + if ($PropertyType -eq 'Boolean') { + $PropertyToSet = $Properties[$Property].ToString().ToLower() + } else { + $PropertyToSet = $Properties[$Property] + } $xml = @" - $($Properties[$Property]) + $($PropertyToSet) "@ $SetProperty.Add($xml) @@ -85,4 +89,4 @@ function Set-CIPPSPOTenant { New-GraphPostRequest -scope "$AdminURL/.default" -tenantid $TenantFilter -Uri "$AdminURL/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' -AddedHeaders $AdditionalHeaders } } -} \ No newline at end of file +} From ed2378f5708599714c201f9885f976cc88c54710 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 5 Jul 2024 12:57:49 -0400 Subject: [PATCH 2/3] CPV tweaks --- .../Public/Add-CIPPApplicationPermission.ps1 | 3 ++- .../Public/Add-CIPPDelegatedPermission.ps1 | 16 +++++++++++++--- .../CIPPCore/Public/AdditionalPermissions.json | 15 +++++++++++++++ Modules/CIPPCore/Public/SAMManifest.json | 3 +-- 4 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 Modules/CIPPCore/Public/AdditionalPermissions.json diff --git a/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 b/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 index f0f4c6badf6d..8fb1f23c3537 100644 --- a/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 @@ -6,7 +6,8 @@ function Add-CIPPApplicationPermission { $Tenantfilter ) if ($ApplicationId -eq $ENV:ApplicationID -and $Tenantfilter -eq $env:TenantID) { - return @('Cannot modify application permissions for CIPP-SAM on partner tenant') + #return @('Cannot modify application permissions for CIPP-SAM on partner tenant') + $RequiredResourceAccess = 'CIPPDefaults' } Set-Location (Get-Item $PSScriptRoot).FullName if ($RequiredResourceAccess -eq 'CIPPDefaults') { diff --git a/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 b/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 index 921488c45f08..4ac1639877de 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 @@ -9,11 +9,14 @@ function Add-CIPPDelegatedPermission { Set-Location (Get-Item $PSScriptRoot).FullName if ($ApplicationId -eq $ENV:ApplicationID -and $Tenantfilter -eq $env:TenantID) { - return @('Cannot modify delgated permissions for CIPP-SAM on partner tenant') + #return @('Cannot modify delgated permissions for CIPP-SAM on partner tenant') + $RequiredResourceAccess = 'CIPPDefaults' } if ($RequiredResourceAccess -eq 'CIPPDefaults') { $RequiredResourceAccess = (Get-Content '.\SAMManifest.json' | ConvertFrom-Json).requiredResourceAccess + $AdditionalPermissions = Get-Content '.\AdditionalPermissions.json' | ConvertFrom-Json + $RequiredResourceAccess = $RequiredResourceAccess + ($AdditionalPermissions | Where-Object { $RequiredResourceAccess.resourceAppId -notcontains $_.resourceAppId }) } $Translator = Get-Content '.\PermissionsTranslator.json' | ConvertFrom-Json $ServicePrincipalList = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=AppId,id,displayName&`$top=999" -tenantid $Tenantfilter -skipTokenCache $true @@ -22,10 +25,17 @@ function Add-CIPPDelegatedPermission { $CurrentDelegatedScopes = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals/$($ourSVCPrincipal.id)/oauth2PermissionGrants" -skipTokenCache $true -tenantid $Tenantfilter - foreach ($App in $requiredResourceAccess) { + foreach ($App in $RequiredResourceAccess) { $svcPrincipalId = $ServicePrincipalList | Where-Object -Property AppId -EQ $App.resourceAppId + $AdditionalScopes = ($AdditionalPermissions | Where-Object -Property resourceAppId -EQ $App.resourceAppId).resourceAccess if (!$svcPrincipalId) { continue } - $NewScope = ($Translator | Where-Object { $_.id -in $App.ResourceAccess.id }).value -join ' ' + if ($AdditionalScopes) { + $NewScope = (($Translator | Where-Object { $_.id -in $App.ResourceAccess.id }).value + $AdditionalScopes.id | Select-Object -Unique) -join ' ' + Write-Host "NEW SCOPE: $NewScope" + } else { + $NewScope = ($Translator | Where-Object { $_.id -in $App.ResourceAccess.id }).value -join ' ' + } + $OldScope = ($CurrentDelegatedScopes | Where-Object -Property Resourceid -EQ $svcPrincipalId.id) if (!$OldScope) { diff --git a/Modules/CIPPCore/Public/AdditionalPermissions.json b/Modules/CIPPCore/Public/AdditionalPermissions.json new file mode 100644 index 000000000000..4983c6f5fd03 --- /dev/null +++ b/Modules/CIPPCore/Public/AdditionalPermissions.json @@ -0,0 +1,15 @@ +[ + { + "resourceAppId": "00000003-0000-0ff1-ce00-000000000000", + "resourceAccess": [{ "id": "AllProfiles.Manage", "type": "Scope" }] + }, + { + "resourceAppId": "fb78d390-0c51-40cd-8e17-fdbfab77341b", + "resourceAccess": [ + { "id": "AdminApi.AccessAsUser.All", "type": "Scope" }, + { "id": "FfoPowerShell.AccessAsUser.All", "type": "Scope" }, + { "id": "RemotePowerShell.AccessAsUser.All", "type": "Scope" }, + { "id": "VivaFeatureAccessPolicy.Manage.All", "type": "Scope" } + ] + } +] diff --git a/Modules/CIPPCore/Public/SAMManifest.json b/Modules/CIPPCore/Public/SAMManifest.json index 3d52dfeadcb1..fc75cb8b8644 100644 --- a/Modules/CIPPCore/Public/SAMManifest.json +++ b/Modules/CIPPCore/Public/SAMManifest.json @@ -172,8 +172,7 @@ { "resourceAppId": "00000003-0000-0ff1-ce00-000000000000", "resourceAccess": [ - { "id": "56680e0d-d2a3-4ae1-80d8-3c4f2100e3d0", "type": "Scope" }, - { "id": "ec4fc4c8-872e-442b-a2a2-d095575807b3", "type": "Scope" } + { "id": "56680e0d-d2a3-4ae1-80d8-3c4f2100e3d0", "type": "Scope" } ] }, { From c355a54d572ffb626a52f70f7757bde5128746d4 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 5 Jul 2024 20:14:03 -0400 Subject: [PATCH 3/3] Extension data sync --- .../CIPPCore/Public/Add-CIPPScheduledTask.ps1 | 8 +- .../Push-ExtensionSyncData.ps1 | 6 + .../Register-CippExtensionScheduledTasks.ps1 | 71 +++++++ .../Sync-CippExtensionData.ps1 | 178 ++++++++++++++++++ .../Public/Hudu/Get-HuduFieldMapping.ps1 | 12 +- 5 files changed, 266 insertions(+), 9 deletions(-) create mode 100644 Modules/CippExtensions/Public/Extension Functions/Push-ExtensionSyncData.ps1 create mode 100644 Modules/CippExtensions/Public/Extension Functions/Register-CippExtensionScheduledTasks.ps1 create mode 100644 Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 diff --git a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 index 952e3a68bc93..7e629e038798 100644 --- a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 @@ -2,7 +2,8 @@ function Add-CIPPScheduledTask { [CmdletBinding()] param( [pscustomobject]$Task, - [bool]$Hidden + [bool]$Hidden, + [string]$SyncType = $null ) $Table = Get-CIPPTable -TableName 'ScheduledTasks' @@ -49,10 +50,13 @@ function Add-CIPPScheduledTask { Hidden = [bool]$Hidden Results = 'Planned' } + if ($SyncType) { + $entity.SyncType = $SyncType + } try { Add-CIPPAzDataTableEntity @Table -Entity $entity -Force } catch { return "Could not add task: $($_.Exception.Message)" } return "Successfully added task: $($entity.Name)" -} \ No newline at end of file +} diff --git a/Modules/CippExtensions/Public/Extension Functions/Push-ExtensionSyncData.ps1 b/Modules/CippExtensions/Public/Extension Functions/Push-ExtensionSyncData.ps1 new file mode 100644 index 000000000000..ebee9385b07a --- /dev/null +++ b/Modules/CippExtensions/Public/Extension Functions/Push-ExtensionSyncData.ps1 @@ -0,0 +1,6 @@ +function Push-ExtensionSyncData { + param( + $TenantFilter, + $Extension + ) +} diff --git a/Modules/CippExtensions/Public/Extension Functions/Register-CippExtensionScheduledTasks.ps1 b/Modules/CippExtensions/Public/Extension Functions/Register-CippExtensionScheduledTasks.ps1 new file mode 100644 index 000000000000..32aff8ef8ff7 --- /dev/null +++ b/Modules/CippExtensions/Public/Extension Functions/Register-CippExtensionScheduledTasks.ps1 @@ -0,0 +1,71 @@ +function Register-CIPPExtensionScheduledTasks { + Param( + [switch]$Reschedule + ) + + # get extension configuration and mappings table + $Table = Get-CIPPTable -TableName Extensionsconfig + $Config = ((Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json -ea stop) + $MappingsTable = Get-CIPPTable -TableName CippMapping + + # Get existing scheduled usertasks + $ScheduledTasksTable = Get-CIPPTable -TableName ScheduledTasks + $ScheduledTasks = Get-CIPPAzDataTableEntity @ScheduledTasksTable -Filter 'Hidden eq true' | Where-Object { $_.Command -match 'Sync-CippExtensionData' } + $Tenants = Get-Tenants -IncludeErrors + + $Extensions = @('Hudu') + + foreach ($Extension in $Extensions) { + $ExtensionConfig = $Config.$Extension + if ($ExtensionConfig.Enabled -eq $true) { + $Mappings = Get-CIPPAzDataTableEntity @MappingsTable -Filter "PartitionKey eq '$($Extension)Mapping'" + $FieldMapping = Get-CIPPAzDataTableEntity @MappingsTable -Filter "PartitionKey eq '$($Extension)FieldMapping'" + $FieldSync = @{} + $SyncTypes = [System.Collections.Generic.List[string]]::new() + + foreach ($Mapping in $FieldMapping) { + $FieldSync[$Mapping.RowKey] = !([string]::IsNullOrEmpty($Mapping.IntegrationId)) + } + + $SyncTypes.Add('Overview') + + if ($FieldSync.Users) { + $SyncTypes.Add('Users') + $SyncTypes.Add('Mailboxes') + } + if ($FieldSync.Devices) { + $SyncTypes.Add('Devices') + } + + foreach ($Mapping in $Mappings) { + $Tenant = $Tenants | Where-Object { $_.customerId -eq $Mapping.RowKey } + + foreach ($SyncType in $SyncTypes) { + $ExistingTask = $ScheduledTasks | Where-Object { $_.Tenant -eq $Tenant.defaultDomainName -and $_.SyncType -eq $SyncType } + if (!$ExistingTask -or $Reschedule.IsPresent) { + $unixtime = [int64](([datetime]::UtcNow) - (Get-Date '1/1/1970')).TotalSeconds + $Task = @{ + Name = "Extension Sync - $SyncType" + Command = @{ + value = 'Sync-CippExtensionData' + label = 'Sync-CippExtensionData' + } + Parameters = @{ + TenantFilter = $Tenant.defaultDomainName + SyncType = $SyncType + } + Recurrence = '1d' + ScheduledTime = $unixtime + TenantFilter = $Tenant.defaultDomainName + } + if ($ExistingTask) { + $Task.RowKey = $ExistingTask.RowKey + } + $null = Add-CIPPScheduledTask -Task $Task -hidden $true -SyncType $SyncType + } + } + } + } + } + +} diff --git a/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 b/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 new file mode 100644 index 000000000000..7cb1c9017a78 --- /dev/null +++ b/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 @@ -0,0 +1,178 @@ +function Sync-CippExtensionData { + <# + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + $TenantFilter, + $SyncType + ) + + $Table = Get-CIPPTable -TableName ExtensionSync + $Extensions = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq '$($SyncType)'" + $LastSync = $Extensions | Where-Object { $_.RowKey -eq $TenantFilter } + $CacheTable = Get-CIPPTable -tablename 'CacheExtensionSync' + + if (!$LastSync) { + $LastSync = @{ + PartitionKey = $SyncType + RowKey = $TenantFilter + Status = 'Not Synced' + Error = '' + LastSync = 'Never' + } + Add-CIPPAzDataTableEntity @Table -Entity $LastSync + } + + try { + switch ($SyncType) { + 'Overview' { + # Build bulk requests array. + [System.Collections.Generic.List[PSCustomObject]]$TenantRequests = @( + @{ + id = 'TenantDetails' + method = 'GET' + url = '/organization' + }, + @{ + id = 'AllRoles' + method = 'GET' + url = '/directoryRoles?$top=999' + }, + @{ + id = 'Domains' + method = 'GET' + url = '/domains$top=999' + }, + @{ + id = 'Licenses' + method = 'GET' + url = '/subscribedSkus?$top=999' + }, + @{ + id = 'Groups' + method = 'GET' + url = '/groups?$top=999&$select=id,createdDateTime,displayName,description,mail,mailEnabled,mailNickname,resourceProvisioningOptions,securityEnabled,visibility,organizationId,onPremisesSamAccountName,membershipRule,grouptypes,onPremisesSyncEnabled,resourceProvisioningOptions,userPrincipalName' + }, + @{ + id = 'ConditionalAccess' + method = 'GET' + url = '/identity/conditionalAccess/policies' + }, + @{ + id = 'SecureScoreControlProfiles' + method = 'GET' + url = '/security/secureScoreControlProfiles?$top=999' + }, + @{ + id = 'Subscriptions' + method = 'GET' + url = '/directory/subscriptions?$top=999' + } + ) + + $SingleGraphQueries = @(@{ + id = 'SecureScore' + graphRequest = @{ + uri = 'https://graph.microsoft.com/beta/security/secureScores?$top=1' + noPagination = $true + } + }) + } + 'Users' { + [System.Collections.Generic.List[PSCustomObject]]$TenantRequests = @( + @{ + id = 'Users' + method = 'GET' + url = '/users?$top=999&$select=id,accountEnabled,businessPhones,city,createdDateTime,companyName,country,department,displayName,faxNumber,givenName,isResourceAccount,jobTitle,mail,mailNickname,mobilePhone,onPremisesDistinguishedName,officeLocation,onPremisesLastSyncDateTime,otherMails,postalCode,preferredDataLocation,preferredLanguage,proxyAddresses,showInAddressList,state,streetAddress,surname,usageLocation,userPrincipalName,userType,assignedLicenses,onPremisesSyncEnabled' + } + ) + } + 'Devices' { + [System.Collections.Generic.List[PSCustomObject]]$TenantRequests = @( + @{ + id = 'Devices' + method = 'GET' + url = '/deviceManagement/managedDevices?$top=999' + }, + @{ + id = 'DeviceCompliancePolicies' + method = 'GET' + url = '/deviceManagement/deviceCompliancePolicies' + }, + @{ + id = 'DeviceApps' + method = 'GET' + url = '/deviceAppManagement/mobileApps' + } + ) + } + 'Mailboxes' { + $Select = 'id,ExchangeGuid,ArchiveGuid,UserPrincipalName,DisplayName,PrimarySMTPAddress,RecipientType,RecipientTypeDetails,EmailAddresses,WhenSoftDeleted,IsInactiveMailbox' + $ExoRequest = @{ + tenantid = $TenantFilter + cmdlet = 'Get-Mailbox' + cmdParams = @{} + Select = $Select + } + $Mailboxes = (New-ExoRequest @ExoRequest) | Select-Object id, ExchangeGuid, ArchiveGuid, WhenSoftDeleted, @{ Name = 'UPN'; Expression = { $_.'UserPrincipalName' } }, + + @{ Name = 'displayName'; Expression = { $_.'DisplayName' } }, + @{ Name = 'primarySmtpAddress'; Expression = { $_.'PrimarySMTPAddress' } }, + @{ Name = 'recipientType'; Expression = { $_.'RecipientType' } }, + @{ Name = 'recipientTypeDetails'; Expression = { $_.'RecipientTypeDetails' } }, + @{ Name = 'AdditionalEmailAddresses'; Expression = { ($_.'EmailAddresses' | Where-Object { $_ -clike 'smtp:*' }).Replace('smtp:', '') -join ', ' } } + + $Entity = @{ + PartitionKey = $TenantFilter + SyncType = 'Mailboxes' + RowKey = 'Mailboxes' + Data = [string]($Mailboxes | ConvertTo-Json -Depth 10 -Compress) + } + Add-CIPPAzDataTableEntity @CacheTable -Entity $Entity -Force + } + } + + if ($TenantRequests) { + try { + $TenantResults = New-GraphBulkRequest -Requests $TenantRequests -tenantid $TenantFilter + } catch { + Throw "Failed to fetch bulk company data: $_" + } + + if ($SingleGraphQueries) { + foreach ($SingleGraphQuery in $SingleGraphQueries) { + $Request = $SingleGraphQuery.graphRequest + $Data = New-GraphGetRequest @Request -tenantid $TenantFilter + $Entity = @{ + PartitionKey = $TenantFilter + SyncType = $SyncType + RowKey = $SingleGraphQuery.id + Data = [string]($Data | ConvertTo-Json -Depth 10 -Compress) + } + Add-CIPPAzDataTableEntity @CacheTable -Entity $Entity -Force + } + } + + $TenantResults | Select-Object id, body | ForEach-Object { + $Entity = @{ + PartitionKey = $TenantFilter + RowKey = $_.id + SyncType = $SyncType + Data = [string]($_.body.value | ConvertTo-Json -Depth 10 -Compress) + } + Add-CIPPAzDataTableEntity @CacheTable -Entity $Entity -Force + } + } + $LastSync.LastSync = [datetime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ') + $LastSync.Status = 'Completed' + $LastSync.Error = '' + } catch { + $LastSync.Status = 'Failed' + $LastSync.Error = [string](Get-CippException -Exception $_ | ConvertTo-Json -Compress) + throw "Failed to sync data: $($_.Exception.Message)" + } finally { + Add-CIPPAzDataTableEntity @Table -Entity $LastSync -Force + } +} diff --git a/Modules/CippExtensions/Public/Hudu/Get-HuduFieldMapping.ps1 b/Modules/CippExtensions/Public/Hudu/Get-HuduFieldMapping.ps1 index 64b4700af62f..bb12d561104a 100644 --- a/Modules/CippExtensions/Public/Hudu/Get-HuduFieldMapping.ps1 +++ b/Modules/CippExtensions/Public/Hudu/Get-HuduFieldMapping.ps1 @@ -4,13 +4,13 @@ function Get-HuduFieldMapping { $CIPPMapping ) - $Mappings = Get-ExtensionMapping -Extension 'HuduFields' + $Mappings = Get-ExtensionMapping -Extension 'HuduField' $CIPPFieldHeaders = @( [PSCustomObject]@{ Title = 'Hudu Asset Layouts' FieldType = 'Layouts' - Description = 'Use the table below to map your Hudu Asset Layouts to the correct CIPP Field' + Description = 'Use the table below to map your Hudu Asset Layouts to the correct CIPP Data Type. A new Rich Text asset layout field will be created if it does not exist.' } ) $CIPPFields = @( @@ -31,8 +31,6 @@ function Get-HuduFieldMapping { } ) - - $Tenants = Get-Tenants -IncludeErrors $Table = Get-CIPPTable -TableName Extensionsconfig try { $Configuration = ((Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json -ea stop).Hudu @@ -46,8 +44,8 @@ function Get-HuduFieldMapping { $_.Exception.message } - Write-LogMessage -Message "Could not get Hudu Companies, error: $Message " -Level Error -tenant 'CIPP' -API 'HuduMapping' - $HuduCompanies = @(@{name = "Could not get Hudu Companies, error: $Message"; value = '-1' }) + Write-LogMessage -Message "Could not get Hudu Asset Layouts, error: $Message " -Level Error -tenant 'CIPP' -API 'HuduMapping' + $AssetLayouts = @(@{name = "Could not get Hudu Asset Layouts, error: $Message"; value = '-1' }) } $Unset = [PSCustomObject]@{ @@ -65,4 +63,4 @@ function Get-HuduFieldMapping { return $MappingObj -} \ No newline at end of file +}