diff --git a/CIPP-Permissions.json b/CIPP-Permissions.json index 95c4e7dea816..ad1e52cb5388 100644 --- a/CIPP-Permissions.json +++ b/CIPP-Permissions.json @@ -425,6 +425,11 @@ "Name": "UserAuthenticationMethod.ReadWrite", "Description": "Allows the app to read and write your authentication methods, including phone numbers and Authenticator app settings.This does not allow the app to see secret information like your passwords, or to sign-in or otherwise use your authentication methods." }, + { + "Id": "424b07a8-1209-4d17-9fe4-9018a93a1024", + "Name": "TeamsTelephoneNumber.ReadWrite.All", + "Description": "Allows the app to read and modify your tenant's acquired telephone number details on behalf of the signed-in admin user. Acquired telephone numbers may include attributes related to assigned object, emergency location, network site, etc." + }, { "Id": "b7887744-6746-4312-813d-72daeaee7e2d", "Name": "UserAuthenticationMethod.ReadWrite.All", @@ -697,6 +702,11 @@ "Name": "User.ReadWrite.All", "Description": "Allows the app to read and update user profiles without a signed in user." }, + { + "Id": "0a42382f-155c-4eb1-9bdc-21548ccaa387", + "Name": "TeamsTelephoneNumber.ReadWrite.All", + "Description": "Allows the app to read your tenant's acquired telephone number details, without a signed-in user. Acquired telephone numbers may include attributes related to assigned object, emergency location, network site, etc." + }, { "Id": "50483e42-d915-4231-9639-7fdb7fd190e5", "Name": "UserAuthenticationMethod.ReadWrite.All", diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListMessageTrace.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListMessageTrace.ps1 index 13f1f68a5fde..2bd5e177c57c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListMessageTrace.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListMessageTrace.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ListMessageTrace { +function Invoke-ListMessageTrace { <# .FUNCTIONALITY Entrypoint @@ -65,11 +65,11 @@ Function Invoke-ListMessageTrace { MessageTraceId = $Request.Body.ID RecipientAddress = $Request.Body.recipient } - New-ExoRequest -TenantId $TenantFilter -Cmdlet 'Get-MessageTraceDetail' -CmdParams $CmdParams | Select-Object @{ Name = 'Date'; Expression = { $_.Date.ToString('u') } }, Event, Action, Detail + New-ExoRequest -TenantId $TenantFilter -Cmdlet 'Get-MessageTraceDetailV2' -CmdParams $CmdParams | Select-Object @{ Name = 'Date'; Expression = { $_.Date.ToString('u') } }, Event, Action, Detail } else { Write-Information ($SearchParams | ConvertTo-Json) - New-ExoRequest -TenantId $TenantFilter -Cmdlet 'Get-MessageTrace' -CmdParams $SearchParams | Select-Object MessageTraceId, Status, Subject, RecipientAddress, SenderAddress, @{ Name = 'Received'; Expression = { $_.Received.ToString('u') } }, FromIP, ToIP + New-ExoRequest -TenantId $TenantFilter -Cmdlet 'Get-MessageTraceV2' -CmdParams $SearchParams | Select-Object MessageTraceId, Status, Subject, RecipientAddress, SenderAddress, @{ Name = 'Received'; Expression = { $_.Received.ToString('u') } }, FromIP, ToIP Write-LogMessage -headers $Request.Headers -API $APIName -tenant $($TenantFilter) -message 'Executed message trace' -Sev 'Info' } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 index 931fcd0af54b..80c782083a28 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 @@ -111,6 +111,13 @@ function Invoke-CIPPOffboardingJob { $_.Exception.Message } } + { $_.RemoveTeamsPhoneDID } { + try { + Remove-CIPPUserTeamsPhoneDIDs -userid $userid -username $username -tenantFilter $TenantFilter -Headers $Headers -APIName $APIName + } catch { + $_.Exception.Message + } + } { $_.RemoveLicenses -eq $true } { Remove-CIPPLicense -userid $userid -username $Username -tenantFilter $TenantFilter -Headers $Headers -APIName $APIName -Schedule } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-DeleteSharepointSite.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-DeleteSharepointSite.ps1 new file mode 100644 index 000000000000..b6325f7a21af --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-DeleteSharepointSite.ps1 @@ -0,0 +1,91 @@ +using namespace System.Net + +function Invoke-DeleteSharepointSite { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Sharepoint.Site.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Body.tenantFilter + $SiteId = $Request.Body.SiteId + + try { + # Validate required parameters + if (-not $SiteId) { + throw "SiteId is required" + } + if (-not $TenantFilter) { + throw "TenantFilter is required" + } + + # Validate SiteId format (GUID) + if ($SiteId -notmatch '^(\{)?[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(\})?$') { + throw "SiteId must be a valid GUID" + } + + $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter + + # Get site information using SharePoint admin API + $SiteInfoUri = "$($SharePointInfo.AdminUrl)/_api/SPO.Tenant/sites('$SiteId')" + + # Add the headers that SharePoint REST API expects + $ExtraHeaders = @{ + 'accept' = 'application/json' + 'content-type' = 'application/json' + 'odata-version' = '4.0' + } + + $SiteInfo = New-GraphGETRequest -scope "$($SharePointInfo.AdminUrl)/.default" -uri $SiteInfoUri -tenantid $TenantFilter -extraHeaders $ExtraHeaders + + if (-not $SiteInfo) { + throw "Could not retrieve site information from SharePoint Admin API" + } + + # Determine if site is group-connected based on GroupId + $IsGroupConnected = $SiteInfo.GroupId -and $SiteInfo.GroupId -ne "00000000-0000-0000-0000-000000000000" + + if ($IsGroupConnected) { + # Use GroupSiteManager/Delete for group-connected sites + $body = @{ + siteUrl = $SiteInfo.Url + } + $DeleteUri = "$($SharePointInfo.AdminUrl)/_api/GroupSiteManager/Delete" + } else { + # Use SPSiteManager/delete for regular sites + $body = @{ + siteId = $SiteId + } + $DeleteUri = "$($SharePointInfo.AdminUrl)/_api/SPSiteManager/delete" + } + + # Execute the deletion + $DeleteResult = New-GraphPOSTRequest -scope "$($SharePointInfo.AdminUrl)/.default" -uri $DeleteUri -body (ConvertTo-Json -Depth 10 -InputObject $body) -tenantid $TenantFilter -extraHeaders $ExtraHeaders + + $SiteTypeMsg = if ($IsGroupConnected) { "group-connected" } else { "regular" } + $Results = "Successfully initiated deletion of $SiteTypeMsg SharePoint site with ID $SiteId, this process can take some time to complete in the background" + + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -sev Info + $StatusCode = [HttpStatusCode]::OK + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Results = "Failed to delete SharePoint site with ID $SiteId. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + } + + # Associate values to output bindings + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ 'Results' = $Results } + }) +} diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 index e1281bff1c28..86f5ef44c70c 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 @@ -59,7 +59,7 @@ function New-GraphGetRequest { $RetryCount = 0 $MaxRetries = 3 $RequestSuccessful = $false - Write-Host "This is attempt $($RetryCount + 1) of $MaxRetries" + Write-Information "GET [ $nextURL ] | tenant: $tenantid | attempt: $($RetryCount + 1) of $MaxRetries" do { try { $GraphRequest = @{ @@ -117,11 +117,24 @@ function New-GraphGetRequest { } catch { $ShouldRetry = $false $WaitTime = 0 - try { - $Message = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.message + $MessageObj = $_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue + if ($MessageObj.error) { + $MessageObj | Add-Member -NotePropertyName 'url' -NotePropertyValue $nextURL -Force + $Message = $MessageObj.error.message -ne '' ? $MessageObj.error.message : $MessageObj.error.code + } } catch { $Message = $null } - if ($Message -eq $null) { $Message = $($_.Exception.Message) } + + if ([string]::IsNullOrEmpty($Message)) { + $Message = $($_.Exception.Message) + $MessageObj = @{ + error = @{ + code = $_.Exception.GetType().FullName + message = $Message + url = $nextURL + } + } + } # Check for 429 Too Many Requests if ($_.Exception.Response.StatusCode -eq 429) { @@ -147,7 +160,7 @@ function New-GraphGetRequest { } else { # Final failure - update tenant error tracking and throw if ($Message -ne 'Request not applicable to target tenant.' -and $Tenant) { - $Tenant.LastGraphError = $Message + $Tenant.LastGraphError = [string]($MessageObj | ConvertTo-Json -Compress) if ($Tenant.PSObject.Properties.Name -notcontains 'GraphErrorCount') { $Tenant | Add-Member -MemberType NoteProperty -Name 'GraphErrorCount' -Value 0 -Force } diff --git a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 index 0f31743a02bf..b3bcef9fb5d3 100644 --- a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 +++ b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 @@ -86,7 +86,6 @@ function Get-GraphRequestList { $SingleTenantThreshold = 8000 Write-Information "Tenant: $TenantFilter" $TableName = ('cache{0}' -f ($Endpoint -replace '[^A-Za-z0-9]'))[0..62] -join '' - Write-Information "Table: $TableName" $Endpoint = $Endpoint -replace '^/', '' $DisplayName = ($Endpoint -split '/')[0] @@ -123,7 +122,6 @@ function Get-GraphRequestList { } $GraphQuery.Query = $ParamCollection.ToString() $PartitionKey = Get-StringHash -String (@($Endpoint, $ParamCollection.ToString(), 'v2') -join '-') - Write-Information "PK: $PartitionKey" # Perform $count check before caching $Count = 0 @@ -174,7 +172,7 @@ function Get-GraphRequestList { Write-Information "Total results (`$count): $Count" } } - Write-Information ( 'GET [ {0} ]' -f $GraphQuery.ToString()) + #Write-Information ( 'GET [ {0} ]' -f $GraphQuery.ToString()) try { if ($QueueId) { @@ -196,7 +194,7 @@ function Get-GraphRequestList { } $Rows = Get-CIPPAzDataTableEntity @Table -Filter $Filter $Type = 'Cache' - Write-Information "Cached: $(($Rows | Measure-Object).Count) rows (Type: $($Type))" + Write-Information "Table: $TableName | PK: $PartitionKey | Cached: $(($Rows | Measure-Object).Count) rows (Type: $($Type))" $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } } diff --git a/Modules/CIPPCore/Public/PermissionsTranslator.json b/Modules/CIPPCore/Public/PermissionsTranslator.json index 74fbcebe7cbe..d4abc5200b3d 100644 --- a/Modules/CIPPCore/Public/PermissionsTranslator.json +++ b/Modules/CIPPCore/Public/PermissionsTranslator.json @@ -5353,5 +5353,23 @@ "userConsentDescription": "Access Microsoft Teams and Skype for Business data as the signed in user", "userConsentDisplayName": "Access Microsoft Teams and Skype for Business data based on the user's role membership", "value": "OnPremDirectorySynchronization.ReadWrite.All" + }, + { + "description": "Read and Modify Tenant-Acquired Telephone Number Details", + "displayName": "Read and Modify Tenant-Acquired Telephone Number Details", + "id": "424b07a8-1209-4d17-9fe4-9018a93a1024", + "Origin": "Delegated", + "userConsentDescription": "Allows the app to read and modify your tenant's acquired telephone number details on behalf of the signed-in admin user. Acquired telephone numbers may include attributes related to assigned object, emergency location, network site, etc.", + "userConsentDisplayName": "Allows the app to read and modify your tenant's acquired telephone number details on behalf of the signed-in admin user. Acquired telephone numbers may include attributes related to assigned object, emergency location, network site, etc.", + "value": "TeamsTelephoneNumber.ReadWrite.All" + }, + { + "description": "Read and Modify Tenant-Acquired Telephone Number Details", + "displayName": "Read and Modify Tenant-Acquired Telephone Number Details", + "id": "0a42382f-155c-4eb1-9bdc-21548ccaa387", + "Origin": "Application", + "userConsentDescription": "Allows the app to read your tenant's acquired telephone number details, without a signed-in user. Acquired telephone numbers may include attributes related to assigned object, emergency location, network site, etc.", + "userConsentDisplayName": "Allows the app to read your tenant's acquired telephone number details, without a signed-in user. Acquired telephone numbers may include attributes related to assigned object, emergency location, network site, etc.", + "value": "TeamsTelephoneNumber.ReadWrite.All" } ] diff --git a/Modules/CIPPCore/Public/Remove-CIPPUserTeamsPhoneDIDs.ps1 b/Modules/CIPPCore/Public/Remove-CIPPUserTeamsPhoneDIDs.ps1 new file mode 100644 index 000000000000..7630e7ce05ad --- /dev/null +++ b/Modules/CIPPCore/Public/Remove-CIPPUserTeamsPhoneDIDs.ps1 @@ -0,0 +1,98 @@ +using namespace System.Net +using namespace System.Collections.Generic + +function Remove-CIPPUserTeamsPhoneDIDs { + [CmdletBinding()] + param ( + $Headers, + [parameter(Mandatory = $true)] + [string]$UserID, + [string]$Username, + $APIName = 'Remove User Teams Phone DIDs', + [parameter(Mandatory = $true)] + $TenantFilter + ) + + try { + + # Set Username to UserID if not provided + if ([string]::IsNullOrEmpty($Username)) { + $Username = $UserID + } + + # Initialize collections for results + $Results = [List[string]]::new() + $SuccessCount = 0 + $ErrorCount = 0 + + # Get all tenant DIDs + $TeamsPhoneDIDs = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/admin/teams/telephoneNumberManagement/numberAssignments" -tenant $TenantFilter + + if (-not $TeamsPhoneDIDs -or $TeamsPhoneDIDs.Count -eq 0) { + $Result = "No Teams Phone DIDs found in tenant" + $Results.Add($Result) + return $Results.ToArray() + } + + # Filter DIDs assigned to the specific user + $UserDIDs = $TeamsPhoneDIDs | Where-Object { $_.assignmentTargetId -eq $UserID -and $_.assignmentStatus -ne 'unassigned' } + + if (-not $UserDIDs -or $UserDIDs.Count -eq 0) { + $Result = "No Teams Phone DIDs found assigned to user: '$Username' - '$UserID'" + $Results.Add($Result) + return $Results.ToArray() + } + + # Prepare bulk requests for all DIDs + $RemoveRequests = foreach ($DID in $UserDIDs) { + @{ + id = $DID.telephoneNumber + method = 'POST' + url = "admin/teams/telephoneNumberManagement/numberAssignments/unassignNumber" + body = @{ + telephoneNumber = $DID.telephoneNumber + numberType = $DID.numberType + } + } + } + + # Execute bulk request + $RemoveResults = New-GraphBulkRequest -tenantid $TenantFilter -requests @($RemoveRequests) + + # Process results + $RemoveResults | ForEach-Object { + $PhoneNumber = $_.id + + if ($_.status -eq 204) { + $SuccessResult = "Successfully removed Teams Phone DID: '$PhoneNumber' from: '$Username' - '$UserID'" + Write-LogMessage -headers $Headers -API $APIName -message $SuccessResult -Sev 'Info' -tenant $TenantFilter + $Results.Add($SuccessResult) + $SuccessCount++ + } else { + $ErrorMessage = if ($_.body.error.message) { + $_.body.error.message + } else { + "HTTP Status: $($_.status)" + } + + $ErrorResult = "Failed to remove Teams Phone DID: '$PhoneNumber' from: '$Username' - '$UserID'. Error: $ErrorMessage" + Write-LogMessage -headers $Headers -API $APIName -message $ErrorResult -Sev 'Error' -tenant $TenantFilter + $Results.Add($ErrorResult) + $ErrorCount++ + } + } + + # Add summary result + $SummaryResult = "Completed processing $($UserDIDs.Count) DIDs for user '$Username': $SuccessCount successful, $ErrorCount failed" + Write-LogMessage -headers $Headers -API $APIName -message $SummaryResult -Sev 'Info' -tenant $TenantFilter + $Results.Add($SummaryResult) + + return $Results.ToArray() + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to process Teams Phone DIDs removal for: '$Username' - '$UserID'. 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/SAMManifest.json b/Modules/CIPPCore/Public/SAMManifest.json index 12702b4c6beb..22969cbb9fcf 100644 --- a/Modules/CIPPCore/Public/SAMManifest.json +++ b/Modules/CIPPCore/Public/SAMManifest.json @@ -570,6 +570,14 @@ { "id": "b7887744-6746-4312-813d-72daeaee7e2d", "type": "Scope" + }, + { + "id": "424b07a8-1209-4d17-9fe4-9018a93a1024", + "type": "Scope" + }, + { + "id": "0a42382f-155c-4eb1-9bdc-21548ccaa387", + "type": "Role" } ] }, @@ -643,4 +651,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/version_latest.txt b/version_latest.txt index a2f28f43be33..e7fdef7e2e63 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -8.4.0 +8.4.2