diff --git a/CIPP-Permissions.json b/CIPP-Permissions.json index 95c4e7dea816c..ad1e52cb5388f 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 13f1f68a5fde0..2bd5e177c57cf 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 931fcd0af54bf..80c782083a28b 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 0000000000000..b6325f7a21afb --- /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 e1281bff1c28f..86f5ef44c70ca 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 0f31743a02bfa..b3bcef9fb5d35 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 74fbcebe7cbe9..d4abc5200b3de 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 0000000000000..7630e7ce05adb --- /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 12702b4c6bebd..22969cbb9fcfb 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 a2f28f43be339..e7fdef7e2e635 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -8.4.0 +8.4.2