diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 index 092c1d37857b..4395e899a8fe 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 @@ -9,22 +9,25 @@ function Invoke-AddOfficeApp { param($Request, $TriggerMetadata) # Input bindings are passed in via param block. - $Tenants = $Request.body.selectedTenants.defaultDomainName + $Tenants = $Request.Body.selectedTenants.defaultDomainName + $Headers = $Request.Headers + $APIName = $Request.Params.CIPPEndpoint if ('AllTenants' -in $Tenants) { $Tenants = (Get-Tenants).defaultDomainName } - $AssignTo = if ($request.body.Assignto -ne 'on') { $request.body.Assignto } + $AssignTo = if ($Request.Body.AssignTo -ne 'on') { $Request.Body.AssignTo } - $results = foreach ($Tenant in $tenants) { + $Results = foreach ($Tenant in $Tenants) { try { - $ExistingO365 = New-graphGetRequest -Uri 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps' -tenantid $tenant | Where-Object { $_.displayname -eq 'Microsoft 365 Apps for Windows 10 and later' } + $ExistingO365 = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps' -tenantid $Tenant | Where-Object { $_.displayName -eq 'Microsoft 365 Apps for Windows 10 and later' } if (!$ExistingO365) { # Check if custom XML is provided - if ($request.body.useCustomXml -and $request.body.customXml) { + if ($Request.Body.useCustomXml -and $Request.Body.customXml) { # Use custom XML configuration $ObjBody = [pscustomobject]@{ '@odata.type' = '#microsoft.graph.officeSuiteApp' 'displayName' = 'Microsoft 365 Apps for Windows 10 and later' 'description' = 'Microsoft 365 Apps for Windows 10 and later' 'informationUrl' = 'https://products.office.com/en-us/explore-office-for-home' + 'privacyInformationUrl' = 'https://privacy.microsoft.com/en-us/privacystatement' 'isFeatured' = $true 'publisher' = 'Microsoft' 'notes' = '' @@ -38,7 +41,7 @@ function Invoke-AddOfficeApp { } } else { # Use standard configuration - $Arch = if ($request.body.arch) { 'x64' } else { 'x86' } + $Arch = if ($Request.Body.arch) { 'x64' } else { 'x86' } $products = @('o365ProPlusRetail') $ExcludedApps = [pscustomobject]@{ infoPath = $true @@ -54,26 +57,27 @@ function Invoke-AddOfficeApp { access = $false bing = $false } - foreach ($ExcludedApp in $request.body.excludedApps.value) { - $ExcludedApps.$excludedapp = $true + foreach ($ExcludedApp in $Request.Body.excludedApps.value) { + $ExcludedApps.$ExcludedApp = $true } $ObjBody = [pscustomobject]@{ '@odata.type' = '#microsoft.graph.officeSuiteApp' 'displayName' = 'Microsoft 365 Apps for Windows 10 and later' 'description' = 'Microsoft 365 Apps for Windows 10 and later' 'informationUrl' = 'https://products.office.com/en-us/explore-office-for-home' + 'privacyInformationUrl' = 'https://privacy.microsoft.com/en-us/privacystatement' 'isFeatured' = $true 'publisher' = 'Microsoft' 'notes' = '' 'owner' = 'Microsoft' - 'autoAcceptEula' = [bool]$request.body.AcceptLicense + 'autoAcceptEula' = [bool]$Request.Body.AcceptLicense 'excludedApps' = $ExcludedApps 'officePlatformArchitecture' = $Arch 'officeSuiteAppDefaultFileFormat' = 'OfficeOpenXMLFormat' - 'localesToInstall' = @($request.body.languages.value) - 'shouldUninstallOlderVersionsOfOffice' = [bool]$request.body.RemoveVersions - 'updateChannel' = $request.body.updateChannel.value - 'useSharedComputerActivation' = [bool]$request.body.SharedComputerActivation + 'localesToInstall' = @($Request.Body.languages.value) + 'shouldUninstallOlderVersionsOfOffice' = [bool]$Request.Body.RemoveVersions + 'updateChannel' = $Request.Body.updateChannel.value + 'useSharedComputerActivation' = [bool]$Request.Body.SharedComputerActivation 'productIds' = $products 'largeIcon' = @{ '@odata.type' = 'microsoft.graph.mimeContent' @@ -88,25 +92,24 @@ function Invoke-AddOfficeApp { "Office deployment already exists for $($Tenant)" continue } - Write-LogMessage -headers $Request.Headers -API $APIName -tenant $($tenant) -message "Added Office profile to $($tenant)" -Sev 'Info' + Write-LogMessage -headers $Headers -API $APIName -tenant $($Tenant) -message "Added Office profile to $($Tenant)" -Sev 'Info' if ($AssignTo) { $AssignO365 = if ($AssignTo -ne 'AllDevicesAndUsers') { '{"mobileAppAssignments":[{"@odata.type":"#microsoft.graph.mobileAppAssignment","target":{"@odata.type":"#microsoft.graph.' + $($AssignTo) + 'AssignmentTarget"},"intent":"Required"}]}' } else { '{"mobileAppAssignments":[{"@odata.type":"#microsoft.graph.mobileAppAssignment","target":{"@odata.type":"#microsoft.graph.allDevicesAssignmentTarget"},"intent":"Required"},{"@odata.type":"#microsoft.graph.mobileAppAssignment","target":{"@odata.type":"#microsoft.graph.allLicensedUsersAssignmentTarget"},"intent":"Required"}]}' } Write-Host ($AssignO365) - New-graphPostRequest -Uri "https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/$($OfficeAppID.id)/assign" -tenantid $tenant -Body $AssignO365 -type POST - Write-LogMessage -headers $Request.Headers -API $APIName -tenant $($tenant) -message "Assigned Office to $AssignTo" -Sev 'Info' + New-GraphPOSTRequest -Uri "https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/$($OfficeAppID.id)/assign" -tenantid $Tenant -Body $AssignO365 -type POST + Write-LogMessage -headers $Headers -API $APIName -tenant $($Tenant) -message "Assigned Office to $AssignTo" -Sev 'Info' } "Successfully added Office App for $($Tenant)" } catch { - "Failed to add Office App for $($Tenant): $($_.Exception.Message)" - Write-LogMessage -headers $Request.Headers -API $APIName -tenant $($tenant) -message "Failed to add Office App. Error: $($_.Exception.Message)" -Sev 'Error' + $ErrorMessage = Get-CippException -Exception $_ + "Failed to add Office App for $($Tenant): $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $($Tenant) -message "Failed to add Office App. Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -Logdata $ErrorMessage continue } } - $body = [pscustomobject]@{'Results' = $results } - return ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK - Body = $body + Body = @{'Results' = $Results } }) } diff --git a/Modules/CIPPCore/Public/Invoke-RemoveQueuedApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-RemoveQueuedApp.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Invoke-RemoveQueuedApp.ps1 rename to Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-RemoveQueuedApp.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecGetRecoveryKey.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecGetRecoveryKey.ps1 index cc9e747b9787..a87941bfd8e4 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecGetRecoveryKey.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecGetRecoveryKey.ps1 @@ -15,9 +15,14 @@ function Invoke-ExecGetRecoveryKey { # Interact with query parameters or the body of the request. $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter $GUID = $Request.Query.GUID ?? $Request.Body.GUID + $RecoveryKeyType = $Request.Body.RecoveryKeyType ?? 'BitLocker' try { - $Result = Get-CIPPBitLockerKey -Device $GUID -TenantFilter $TenantFilter -APIName $APIName -Headers $Headers + switch ($RecoveryKeyType) { + 'BitLocker' { $Result = Get-CIPPBitLockerKey -Device $GUID -TenantFilter $TenantFilter -APIName $APIName -Headers $Headers } + 'FileVault' { $Result = Get-CIPPFileVaultKey -Device $GUID -TenantFilter $TenantFilter -APIName $APIName -Headers $Headers } + default { throw "Invalid RecoveryKeyType specified: $RecoveryKeyType." } + } $StatusCode = [HttpStatusCode]::OK } catch { $Result = $_.Exception.Message diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 index 73d260965f23..aeb79dddc16a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 @@ -88,10 +88,10 @@ function Invoke-ListGroups { groupInfo = ($RawGraphRequest | Where-Object { $_.id -eq 1 }).body | Select-Object *, @{ Name = 'primDomain'; Expression = { $_.mail -split '@' | Select-Object -Last 1 } }, @{Name = 'teamsEnabled'; Expression = { if ($_.resourceProvisioningOptions -like '*Team*') { $true } else { $false } } }, @{Name = 'calculatedGroupType'; Expression = { - if ($_.mailEnabled -and $_.securityEnabled) { 'Mail-Enabled Security' } - if (!$_.mailEnabled -and $_.securityEnabled) { 'Security' } if ($_.groupTypes -contains 'Unified') { 'Microsoft 365' } - if (([string]::isNullOrEmpty($_.groupTypes)) -and ($_.mailEnabled) -and (!$_.securityEnabled)) { 'Distribution List' } + elseif ($_.mailEnabled -and $_.securityEnabled) { 'Mail-Enabled Security' } + elseif (-not $_.mailEnabled -and $_.securityEnabled) { 'Security' } + elseif (([string]::isNullOrEmpty($_.groupTypes)) -and ($_.mailEnabled) -and (-not $_.securityEnabled)) { 'Distribution List' } } }, @{Name = 'dynamicGroupBool'; Expression = { if ($_.groupTypes -contains 'DynamicMembership') { $true } else { $false } } } members = ($RawGraphRequest | Where-Object { $_.id -eq 2 }).body.value @@ -105,11 +105,10 @@ function Invoke-ListGroups { @{Name = 'membersCsv'; Expression = { $_.members.userPrincipalName -join ',' } }, @{Name = 'teamsEnabled'; Expression = { if ($_.resourceProvisioningOptions -like '*Team*') { $true }else { $false } } }, @{Name = 'calculatedGroupType'; Expression = { - - if ($_.mailEnabled -and $_.securityEnabled) { 'Mail-Enabled Security' } - if (!$_.mailEnabled -and $_.securityEnabled) { 'Security' } if ($_.groupTypes -contains 'Unified') { 'Microsoft 365' } - if (([string]::isNullOrEmpty($_.groupTypes)) -and ($_.mailEnabled) -and (!$_.securityEnabled)) { 'Distribution List' } + elseif ($_.mailEnabled -and $_.securityEnabled) { 'Mail-Enabled Security' } + elseif (-not $_.mailEnabled -and $_.securityEnabled) { 'Security' } + elseif (([string]::isNullOrEmpty($_.groupTypes)) -and ($_.mailEnabled) -and (-not $_.securityEnabled)) { 'Distribution List' } } }, @{Name = 'dynamicGroupBool'; Expression = { if ($_.groupTypes -contains 'DynamicMembership') { $true } else { $false } } } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserGroups.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserGroups.ps1 index d0c533708320..a223aef2c597 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserGroups.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserGroups.ps1 @@ -1,4 +1,4 @@ -Function Invoke-ListUserGroups { +function Invoke-ListUserGroups { <# .FUNCTIONALITY Entrypoint @@ -22,10 +22,10 @@ Function Invoke-ListUserGroups { @{ Name = 'OnPremisesSync'; Expression = { $_.onPremisesSyncEnabled } }, @{ Name = 'IsAssignableToRole'; Expression = { $_.isAssignableToRole } }, @{ Name = 'calculatedGroupType'; Expression = { - if ($_.mailEnabled -and $_.securityEnabled) { 'Mail-Enabled Security' } - if (!$_.mailEnabled -and $_.securityEnabled) { 'Security' } if ($_.groupTypes -contains 'Unified') { 'Microsoft 365' } - if (([string]::isNullOrEmpty($_.groupTypes)) -and ($_.mailEnabled) -and (!$_.securityEnabled)) { 'Distribution List' } + elseif ($_.mailEnabled -and $_.securityEnabled) { 'Mail-Enabled Security' } + elseif (-not $_.mailEnabled -and $_.securityEnabled) { 'Security' } + elseif (([string]::isNullOrEmpty($_.groupTypes)) -and ($_.mailEnabled) -and (-not $_.securityEnabled)) { 'Distribution List' } } } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 index 4fd95ff2dba2..dd112c3f16ec 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 @@ -1,4 +1,4 @@ -Function Invoke-ListSites { +function Invoke-ListSites { <# .FUNCTIONALITY Entrypoint @@ -11,15 +11,14 @@ Function Invoke-ListSites { $TenantFilter = $Request.Query.TenantFilter - $Type = $request.query.Type - $UserUPN = $request.query.UserUPN + $Type = $Request.Query.Type + $UserUPN = $Request.Query.UserUPN if (!$TenantFilter) { return ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::BadRequest Body = 'TenantFilter is required' }) - return } if (!$Type) { @@ -27,7 +26,6 @@ Function Invoke-ListSites { StatusCode = [HttpStatusCode]::BadRequest Body = 'Type is required' }) - return } $Tenant = Get-Tenants -TenantFilter $TenantFilter diff --git a/Modules/CIPPCore/Public/Get-CIPPBitlockerKey.ps1 b/Modules/CIPPCore/Public/Get-CIPPBitlockerKey.ps1 index 6035975ad1d2..0b159e9807be 100644 --- a/Modules/CIPPCore/Public/Get-CIPPBitlockerKey.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPBitlockerKey.ps1 @@ -1,3 +1,31 @@ +<# +.SYNOPSIS + Retrieves BitLocker recovery keys for a managed device from Microsoft Graph API. + +.DESCRIPTION + This function queries the Microsoft Graph API to retrieve all BitLocker recovery keys + associated with a specified device. It handles cases where no key is found and provides appropriate + logging and error handling. +.PARAMETER Device + The ID of the device for which to retrieve BitLocker recovery keys. + +.PARAMETER TenantFilter + The tenant ID to filter the request to the appropriate tenant. + +.PARAMETER APIName + The name of the API operation for logging purposes. Defaults to 'Get BitLocker key'. + +.PARAMETER Headers + The headers to include in the request, typically used for authentication and logging. + +.OUTPUTS + Array of PSCustomObject with properties: + - resultText: Formatted string containing the key ID and key value + - copyField: The raw key value + - state: Status of the operation ('success') + + Or a string message if no keys are found. +#> function Get-CIPPBitLockerKey { [CmdletBinding()] diff --git a/Modules/CIPPCore/Public/Get-CIPPFileVaultKey.ps1 b/Modules/CIPPCore/Public/Get-CIPPFileVaultKey.ps1 new file mode 100644 index 000000000000..bc1933d3bc2f --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPFileVaultKey.ps1 @@ -0,0 +1,63 @@ +<# +.SYNOPSIS + Retrieves the FileVault recovery key for a managed device from Microsoft Graph API. + +.DESCRIPTION + This function makes a request to the Microsoft Graph API to retrieve the FileVault recovery key + for a specified managed device. It handles cases where no key is found and provides appropriate + logging and error handling. + +.PARAMETER Device + The GUID of the managed device for which to retrieve the FileVault key. + +.PARAMETER TenantFilter + The tenant ID to filter the request to the appropriate tenant. + +.PARAMETER APIName + The name of the API operation for logging purposes. Defaults to 'Get FileVault key'. + +.PARAMETER Headers + The headers to include in the request, typically used for authentication and logging. + +.OUTPUTS + PSCustomObject with properties: + - resultText: Formatted string containing the key + - copyField: The raw key value + - state: Status of the operation ('success') + + Or a string message if no key is found. + +#> + +function Get-CIPPFileVaultKey { + [CmdletBinding()] + param ( + $Device, + $TenantFilter, + $APIName = 'Get FileVault key', + $Headers + ) + + try { + $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices/$Device/getFileVaultKey" -tenantid $TenantFilter + + if ([string]::IsNullOrEmpty($GraphRequest)) { + $Result = "No FileVault recovery key found for $($Device)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev Info -tenant $TenantFilter + return $Result + } + + Write-LogMessage -headers $Headers -API $APIName -message "Retrieved FileVault recovery key for $($Device)" -Sev Info -tenant $TenantFilter + return [PSCustomObject]@{ + resultText = "Key: $($GraphRequest)" + copyField = $GraphRequest + state = 'success' + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Could not retrieve FileVault recovery key for $($Device). Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev Error -tenant $TenantFilter -LogData $ErrorMessage + throw $Result + } + +} diff --git a/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 b/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 index 8a5cddf7a400..9ef67b5e673f 100644 --- a/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 @@ -18,6 +18,22 @@ function Get-CIPPLicenseOverview { url = 'directory/subscriptions' method = 'GET' } + @{ + id = 'licensedUsers' + url = "users?`$select=id,displayName,userPrincipalName,assignedLicenses&`$filter=assignedLicenses/`$count ne 0&`$count=true" + method = 'GET' + headers = @{ + 'ConsistencyLevel' = 'eventual' + } + } + @{ + id = 'licensedGroups' + url = "groups?`$select=id,displayName,assignedLicenses,mailEnabled,securityEnabled,groupTypes,onPremisesSyncEnabled&`$filter=assignedLicenses/`$count ne 0&`$count=true" + method = 'GET' + headers = @{ + 'ConsistencyLevel' = 'eventual' + } + } ) try { @@ -38,9 +54,54 @@ function Get-CIPPLicenseOverview { $ConvertTable = Import-Csv ConversionTable.csv $LicenseTable = Get-CIPPTable -TableName ExcludedLicenses $ExcludedSkuList = Get-CIPPAzDataTableEntity @LicenseTable - $GraphRequest = foreach ($singlereq in $RawGraphRequest) { - $skuid = $singlereq.Licenses - foreach ($sku in $skuid) { + + $AllLicensedUsers = @(($Results | Where-Object { $_.id -eq 'licensedUsers' }).body.value) + $UsersBySku = @{} + foreach ($User in $AllLicensedUsers) { + if (-not $User.assignedLicenses) { continue } # Skip users with no assigned licenses. Should not happens as the filter is applied, but just in case + $UserInfo = [PSCustomObject]@{ + displayName = [string]$User.displayName + userPrincipalName = [string]$User.userPrincipalName + id = [string]$User.id + } + + foreach ($AssignedLicense in $User.assignedLicenses) { + $LicenseSkuId = ([string]$AssignedLicense.skuId).ToLowerInvariant() + if ([string]::IsNullOrWhiteSpace($LicenseSkuId)) { continue } # Skip if SKU ID is null or whitespace. Should not happen but just in case + if (-not $UsersBySku.ContainsKey($LicenseSkuId)) { + $UsersBySku[$LicenseSkuId] = [System.Collections.Generic.List[object]]::new() + } + $UsersBySku[$LicenseSkuId].Add($UserInfo) + } + + } + + $AllLicensedGroups = @(($Results | Where-Object { $_.id -eq 'licensedGroups' }).body.value) + $GroupsBySku = @{} + foreach ($Group in $AllLicensedGroups) { + if (-not $Group.assignedLicenses) { continue } + $GroupInfo = [PSCustomObject]@{ + displayName = [string]$Group.displayName + calculatedGroupType = if ($Group.groupTypes -contains 'Unified') { 'Microsoft 365' } + elseif ($Group.mailEnabled -and $Group.securityEnabled) { 'Mail-Enabled Security' } + elseif (-not $Group.mailEnabled -and $Group.securityEnabled) { 'Security' } + elseif (([string]::isNullOrEmpty($Group.groupTypes)) -and ($Group.mailEnabled) -and (-not $Group.securityEnabled)) { 'Distribution List' } + id = [string]$Group.id + onPremisesSyncEnabled = [bool]$Group.onPremisesSyncEnabled + + } + foreach ($AssignedLicense in $Group.assignedLicenses) { + $LicenseSkuId = ([string]$AssignedLicense.skuId).ToLowerInvariant() + if ([string]::IsNullOrWhiteSpace($LicenseSkuId)) { continue } + if (-not $GroupsBySku.ContainsKey($LicenseSkuId)) { + $GroupsBySku[$LicenseSkuId] = [System.Collections.Generic.List[object]]::new() + } + $GroupsBySku[$LicenseSkuId].Add($GroupInfo) + } + } + $GraphRequest = foreach ($singleReq in $RawGraphRequest) { + $skuId = $singleReq.Licenses + foreach ($sku in $skuId) { if ($sku.skuId -in $ExcludedSkuList.GUID) { continue } $PrettyNameAdmin = $AdminPortalLicenses | Where-Object { $_.SkuId -eq $sku.skuId } | Select-Object -ExpandProperty Name $PrettyNameCSV = ($ConvertTable | Where-Object { $_.guid -eq $sku.skuid }).'Product_Display_Name' | Select-Object -Last 1 @@ -71,8 +132,9 @@ function Get-CIPPLicenseOverview { OCPSubscriptionId = $SubInfo.ocpSubscriptionId } } + $SkuKey = ([string]$sku.skuId).ToLowerInvariant() [pscustomobject]@{ - Tenant = [string]$singlereq.Tenant + Tenant = [string]$singleReq.Tenant License = [string]$PrettyName CountUsed = [string]"$($sku.consumedUnits)" CountAvailable = [string]$sku.prepaidUnits.enabled - $sku.consumedUnits @@ -81,11 +143,12 @@ function Get-CIPPLicenseOverview { skuPartNumber = [string]$PrettyName availableUnits = [string]$sku.prepaidUnits.enabled - $sku.consumedUnits TermInfo = [string]($TermInfo | ConvertTo-Json -Depth 10 -Compress) + AssignedUsers = ($UsersBySku.ContainsKey($SkuKey) ? @(($UsersBySku[$SkuKey])) : $null) + AssignedGroups = ($GroupsBySku.ContainsKey($SkuKey) ? @(($GroupsBySku[$SkuKey])) : $null) 'PartitionKey' = 'License' - 'RowKey' = "$($singlereq.Tenant) - $($sku.skuid)" + 'RowKey' = "$($singleReq.Tenant) - $($sku.skuid)" } } } return $GraphRequest } - diff --git a/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 b/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 index d08a46c09027..3ddeb244de00 100644 --- a/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 +++ b/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 @@ -228,6 +228,15 @@ function Sync-CippExtensionData { $Data = $Data.Value } + # Filter out excluded licenses to respect the ExcludedLicenses table + if ($_.id -eq 'Licenses') { + $LicenseTable = Get-CIPPTable -TableName ExcludedLicenses + $ExcludedSkuList = Get-CIPPAzDataTableEntity @LicenseTable + if ($ExcludedSkuList) { + $Data = $Data | Where-Object { $_.skuId -notin $ExcludedSkuList.GUID } + } + } + $Entity = @{ PartitionKey = $TenantFilter RowKey = $_.id diff --git a/Tools/Initialize-DevEnvironment.ps1 b/Tools/Initialize-DevEnvironment.ps1 index bf9641461a77..bc8fd6193f91 100644 --- a/Tools/Initialize-DevEnvironment.ps1 +++ b/Tools/Initialize-DevEnvironment.ps1 @@ -15,6 +15,14 @@ if ((Test-Path $PowerShellWorkerRoot) -and !('Microsoft.Azure.Functions.PowerShe Add-Type -Path $PowerShellWorkerRoot } +# Remove previously loaded modules to force reloading if new code changes were made +$LoadedModules = Get-Module | Select-Object -ExpandProperty Name +switch ($LoadedModules) { + 'CIPPCore' { Remove-Module CIPPCore -Force } + 'CippExtensions' { Remove-Module CippExtensions -Force } + 'MicrosoftTeams' { Remove-Module MicrosoftTeams -Force } +} + Import-Module ( Join-Path $CippRoot 'Modules\AzBobbyTables' ) Import-Module ( Join-Path $CippRoot 'Modules\DNSHealth' ) Import-Module ( Join-Path $CippRoot 'Modules\CIPPCore' )