diff --git a/.github/workflows/dev_cippb2p4g.yml b/.github/workflows/dev_cippb2p4g.yml new file mode 100644 index 000000000000..e82ec8c1ba86 --- /dev/null +++ b/.github/workflows/dev_cippb2p4g.yml @@ -0,0 +1,29 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/azure/functions-action +# More GitHub Actions for Azure: https://github.com/Azure/actions + +name: Build and deploy Powershell project to Azure Function App - cippb2p4g + +on: + push: + branches: + - dev + workflow_dispatch: + +env: + AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root + +jobs: + build-and-deploy: + runs-on: windows-latest + steps: + - name: 'Checkout GitHub Action' + uses: actions/checkout@v4 + + - name: 'Run Azure Functions Action' + uses: Azure/functions-action@v1 + id: fa + with: + app-name: 'cippb2p4g' + slot-name: 'Production' + package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }} + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_C106B398356B4EFCB81F779ED5806A0D }} diff --git a/ExecAccessChecks/run.ps1 b/ExecAccessChecks/run.ps1 index ae45d23e6230..0cc71645208e 100644 --- a/ExecAccessChecks/run.ps1 +++ b/ExecAccessChecks/run.ps1 @@ -10,258 +10,14 @@ Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -m # Write to the Azure Functions log stream. Write-Host 'PowerShell HTTP trigger function processed a request.' if ($Request.query.Permissions -eq 'true') { - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Started permissions check' -Sev 'Debug' - $Messages = [System.Collections.Generic.List[string]]::new() - $MissingPermissions = [System.Collections.Generic.List[string]]::new() - $Links = [System.Collections.Generic.List[object]]::new() - $AccessTokenDetails = [PSCustomObject]@{ - AppId = '' - AppName = '' - Audience = '' - AuthMethods = '' - IPAddress = '' - Name = '' - Scope = '' - TenantId = '' - UserPrincipalName = '' - } - $Success = $true - try { - Set-Location (Get-Item $PSScriptRoot).Parent.FullName - $ExpectedPermissions = Get-Content '.\Cache_SAMSetup\SAMManifest.json' | ConvertFrom-Json - - $GraphToken = Get-GraphToken -returnRefresh $true - if ($GraphToken) { - $GraphPermissions = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/myorganization/applications?`$filter=appId eq '$env:ApplicationID'" -NoAuthCheck $true - } - if ($env:MSI_SECRET) { - try { - Disable-AzContextAutosave -Scope Process | Out-Null - $AzSession = Connect-AzAccount -Identity - - $KV = $ENV:WEBSITE_DEPLOYMENT_ID - $KeyVaultRefresh = Get-AzKeyVaultSecret -VaultName $kv -Name 'RefreshToken' -AsPlainText - if ($ENV:RefreshToken -ne $KeyVaultRefresh) { - $Success = $false - $Messages.Add('Your refresh token does not match key vault, clear your cache or wait 30 minutes.') | Out-Null - $Links.Add([PSCustomObject]@{ - Text = 'Clear Token Cache' - Href = 'https://cipp.app/docs/general/troubleshooting/#clear-token-cache' - } - ) | Out-Null - } else { - $Messages.Add('Your refresh token matches key vault.') | Out-Null - } - } catch { - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message "Key vault exception: $($_) " -Sev 'Error' - } - } - - try { - $AccessTokenDetails = Read-JwtAccessDetails -Token $GraphToken.access_token -erroraction SilentlyContinue - } catch { - $AccessTokenDetails = [PSCustomObject]@{ - Name = '' - AuthMethods = @() - } - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message "Token exception: $($_) " -Sev 'Error' - $Success = $false - } - - if ($AccessTokenDetails.Name -eq '') { - $Messages.Add('Your refresh token is invalid, check for line breaks or missing characters.') | Out-Null - $Success = $false - } else { - if ($AccessTokenDetails.AuthMethods -contains 'mfa') { - $Messages.Add('Your access token contains the MFA claim.') | Out-Null - } else { - $Messages.Add('Your access token does not contain the MFA claim, Refresh your SAM tokens.') | Out-Null - $Success = $false - $Links.Add([PSCustomObject]@{ - Text = 'MFA Troubleshooting' - Href = 'https://cipp.app/docs/general/troubleshooting/#multi-factor-authentication-troubleshooting' - } - ) | Out-Null - } - } - - $MissingPermissions = $ExpectedPermissions.requiredResourceAccess.ResourceAccess.id | Where-Object { $_ -notin $GraphPermissions.requiredResourceAccess.ResourceAccess.id } - if ($MissingPermissions) { - $Translator = Get-Content '.\Cache_SAMSetup\PermissionsTranslator.json' | ConvertFrom-Json - $TranslatedPermissions = $Translator | Where-Object id -In $MissingPermissions | ForEach-Object { "$($_.value) - $($_.Origin)" } - $MissingPermissions = @($TranslatedPermissions) - $Success = $false - $Links.Add([PSCustomObject]@{ - Text = 'Permissions' - Href = 'https://cipp.app/docs/user/gettingstarted/postinstall/permissions/' - } - ) | Out-Null - } else { - $Messages.Add('Your Secure Application Model has all required permissions') | Out-Null - } - $CIPPGroupCount = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups/`$count?`$filter=startsWith(displayName,'M365 GDAP')" -NoAuthCheck $true -ComplexFilter - $SAMUserMemberships = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/me/memberOf?$select=id,displayName,isAssignableToRole' -NoAuthCheck $true - $ExpectedGroups = @( - 'AdminAgents', - 'M365 GDAP Application Administrator', - 'M365 GDAP User Administrator', - 'M365 GDAP Intune Administrator', - 'M365 GDAP Exchange Administrator', - 'M365 GDAP Security Administrator', - 'M365 GDAP Cloud App Security Administrator', - 'M365 GDAP Cloud Device Administrator', - 'M365 GDAP Teams Administrator', - 'M365 GDAP Sharepoint Administrator', - 'M365 GDAP Authentication Policy Administrator', - 'M365 GDAP Privileged Role Administrator', - 'M365 GDAP Privileged Authentication Administrator' - ) - $RoleAssignableGroups = $SAMUserMemberships | Where-Object { $_.isAssignableToRole } - $NestedGroups = foreach ($Group in $RoleAssignableGroups) { - New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups/$($Group.id)/memberOf?`$select=id,displayName" -NoAuthCheck $true - } - - $MissingGroups = [System.Collections.Generic.List[string]]::new() - foreach ($Group in $ExpectedGroups) { - $GroupFound = $false - foreach ($Membership in ($SAMUserMemberships + $NestedGroups)) { - if ($Membership.displayName -match $Group -and (($CIPPGroupCount -gt 0 -and $Group -match 'M365 GDAP') -or $Group -notmatch 'M365 GDAP')) { - $GroupFound = $true - } - } - if (-not $GroupFound) { - $MissingGroups.Add($Group) - } - } - if (($MissingGroups | Measure-Object).Count -eq 0) { - $Messages.Add('The SAM user has all the required groups') - } else { - $Success = $false - } - } catch { - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Permissions check failed: $($_) " -Sev 'Error' - $Messages.Add("We could not connect to the API to retrieve the permissions. There might be a problem with the secure application model configuration. The returned error is: $(Get-NormalizedError -message $_)") | Out-Null - $Success = $false - } - - $Results = [PSCustomObject]@{ - AccessTokenDetails = $AccessTokenDetails - Messages = @($Messages) - MissingPermissions = @($MissingPermissions) - MissingGroups = @($MissingGroups) - Memberships = @($SAMUserMemberships) - CIPPGroupCount = $CIPPGroupCount - Links = @($Links) - Success = $Success - } + $Results = Test-CIPPAccessPermissions -tenantfilter $ENV:tenantid -APIName $APINAME -ExecutingUser $request.headers.'x-ms-client-principal' } if ($Request.query.Tenants -eq 'true') { - $ExpectedRoles = @( - @{ Name = 'Application Administrator'; Id = '9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3' }, - @{ Name = 'User Administrator'; Id = 'fe930be7-5e62-47db-91af-98c3a49a38b1' }, - @{ Name = 'Intune Administrator'; Id = '3a2c62db-5318-420d-8d74-23affee5d9d5' }, - @{ Name = 'Exchange Administrator'; Id = '29232cdf-9323-42fd-ade2-1d097af3e4de' }, - @{ Name = 'Security Administrator'; Id = '194ae4cb-b126-40b2-bd5b-6091b380977d' }, - @{ Name = 'Cloud App Security Administrator'; Id = '892c5842-a9a6-463a-8041-72aa08ca3cf6' }, - @{ Name = 'Cloud Device Administrator'; Id = '7698a772-787b-4ac8-901f-60d6b08affd2' }, - @{ Name = 'Teams Administrator'; Id = '69091246-20e8-4a56-aa4d-066075b2a7a8' }, - @{ Name = 'Sharepoint Administrator'; Id = 'f28a1f50-f6e7-4571-818b-6a12f2af6b6c' }, - @{ Name = 'Authentication Policy Administrator'; Id = '0526716b-113d-4c15-b2c8-68e3c22b9f80' }, - @{ Name = 'Privileged Role Administrator'; Id = 'e8611ab8-c189-46e8-94e1-60213ab1f814' }, - @{ Name = 'Privileged Authentication Administrator'; Id = '7be44c8a-adaf-4e2a-84d6-ab2649e08a13' } - ) - $Tenants = ($Request.body.tenantid).split(',') - if (!$Tenants) { $results = 'Could not load the tenants list from cache. Please run permissions check first, or visit the tenants page.' } - $TenantList = Get-Tenants - $TenantIds = foreach ($Tenant in $Tenants) { - ($TenantList | Where-Object { $_.defaultDomainName -eq $Tenant }).customerId - } - $MyRoles = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/managedTenants/myRoles?`$filter=tenantId in ('$($TenantIds -join "','")')" - $results = foreach ($tenant in $Tenants) { - $AddedText = '' - try { - $TenantId = ($TenantList | Where-Object { $_.defaultDomainName -eq $tenant }).customerId - $Assignments = ($MyRoles | Where-Object { $_.tenantId -eq $TenantId }).assignments - $SAMUserRoles = ($Assignments | Where-Object { $_.assignmentType -eq 'granularDelegatedAdminPrivileges' }).roles - - $BulkRequests = $ExpectedRoles | ForEach-Object { @( - @{ - id = "roleManagement_$($_.id)" - method = 'GET' - url = "roleManagement/directory/roleAssignments?`$filter=roleDefinitionId eq '$($_.id)'&`$expand=principal" - } - ) - } - $GDAPRolesGraph = New-GraphBulkRequest -tenantid $tenant -Requests $BulkRequests - $GDAPRoles = [System.Collections.Generic.List[object]]::new() - $MissingRoles = [System.Collections.Generic.List[object]]::new() - foreach ($RoleId in $ExpectedRoles) { - $GraphRole = $GDAPRolesGraph.body.value | Where-Object -Property roleDefinitionId -EQ $RoleId.Id - $Role = $GraphRole.principal | Where-Object -Property organizationId -EQ $ENV:tenantid - $SAMRole = $SAMUserRoles | Where-Object -Property templateId -EQ $RoleId.Id - if (!$Role) { - $MissingRoles.Add( - [PSCustomObject]@{ - Name = $RoleId.Name - Type = 'Tenant' - } - ) - $AddedText = 'but missing GDAP roles' - } else { - $GDAPRoles.Add([PSCustomObject]$RoleId) - } - if (!$SAMRole) { - $MissingRoles.Add( - [PSCustomObject]@{ - Name = $RoleId.Name - Type = 'SAM User' - } - ) - $AddedText = 'but missing GDAP roles' - } - } - if (!($MissingRoles | Measure-Object).Count -gt 0) { - $MissingRoles = $true - } - @{ - TenantName = "$($Tenant)" - Status = "Successfully connected $($AddedText)" - GDAPRoles = $GDAPRoles - MissingRoles = $MissingRoles - SAMUserRoles = $SAMUserRoles - } - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message 'Tenant access check executed successfully' -Sev 'Info' - - } catch { - @{ - TenantName = "$($tenant)" - Status = "Failed to connect: $(Get-NormalizedError -message $_.Exception.Message)" - GDAP = '' - } - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message "Tenant access check failed: $(Get-NormalizedError -message $_) " -Sev 'Error' - - } - - try { - $GraphRequest = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig' -ErrorAction Stop - @{ - TenantName = "$($Tenant)" - Status = 'Successfully connected to Exchange' - } - - } catch { - $ReportedError = ($_.ErrorDetails | ConvertFrom-Json -ErrorAction SilentlyContinue) - $Message = if ($ReportedError.error.details.message) { $ReportedError.error.details.message } else { $ReportedError.error.innererror.internalException.message } - if ($null -eq $Message) { $Message = $($_.Exception.Message) } - @{ - TenantName = "$($Tenant)" - Status = "Failed to connect to Exchange: $(Get-NormalizedError -message $Message)" - } - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message "Tenant access check for Exchange failed: $(Get-NormalizedError -message $Message) " -Sev 'Error' - } - } - if (!$Tenants) { $results = 'Could not load the tenants list from cache. Please run permissions check first, or visit the tenants page.' } + $Results = Test-CIPPAccessTenant -Tenantcsv $Request.body.TenantId +} +if ($Request.query.GDAP -eq 'true') { + $Results = Test-CIPPGDAPRelationships } $body = [pscustomobject]@{'Results' = $Results } diff --git a/ExecGDAPInvite/run.ps1 b/ExecGDAPInvite/run.ps1 index ff966b6e8a75..2e043db68286 100644 --- a/ExecGDAPInvite/run.ps1 +++ b/ExecGDAPInvite/run.ps1 @@ -7,19 +7,18 @@ $APIName = $TriggerMetadata.FunctionName Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' $RoleMappings = $Request.body.gdapRoles -$Results = [System.Collections.ArrayList]@() +$Results = [System.Collections.Generic.List[string]]::new() +$InviteUrls = [System.Collections.Generic.List[string]]::new() $Table = Get-CIPPTable -TableName 'GDAPInvites' try { $JSONBody = @{ - 'displayName' = "$((New-Guid).GUID)" - 'partner' = @{ - 'tenantId' = "$env:tenantid" - } - 'accessDetails' = @{ + 'displayName' = "$((New-Guid).GUID)" + 'accessDetails' = @{ 'unifiedRoles' = @($RoleMappings | Select-Object roleDefinitionId) } - 'duration' = 'P730D' + 'autoExtendDuration' = 'P180D' + 'duration' = 'P730D' } | ConvertTo-Json -Depth 5 -Compress $NewRelationship = New-GraphPostRequest -NoAuthCheck $True -uri 'https://graph.microsoft.com/beta/tenantRelationships/delegatedAdminRelationships' -type POST -body $JSONBody -verbose -tenantid $env:TenantID @@ -40,6 +39,7 @@ try { if ($NewRelationshipRequest.action -eq 'lockForApproval') { $InviteUrl = "https://admin.microsoft.com/AdminPortal/Home#/partners/invitation/granularAdminRelationships/$($NewRelationship.id)" + $InviteUrls.Add($InviteUrl) $InviteEntity = [PSCustomObject]@{ 'PartitionKey' = 'invite' @@ -58,9 +58,12 @@ try { $Results.add('Error creating GDAP relationship') } -Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Created GDAP Invite - $InviteUrl" -Sev 'Debug' +Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Created GDAP Invite - $InviteUrl" -Sev 'Info' -$body = @{Results = @($Results) } +$body = @{ + Results = @($Results) + InviteUrls = @($InviteUrls) +} Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK Body = $body diff --git a/ExecGDAPInviteQueue/run.ps1 b/ExecGDAPInviteQueue/run.ps1 index 3d9fd22c68b6..78e43c118449 100644 --- a/ExecGDAPInviteQueue/run.ps1 +++ b/ExecGDAPInviteQueue/run.ps1 @@ -7,7 +7,7 @@ Write-Host "PowerShell queue trigger function processed work item: $QueueItem" $Table = Get-CIPPTable -TableName 'GDAPInvites' $Invite = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq '$QueueItem'" - +$APINAME = 'GDAPInvites' $RoleMappings = $Invite.RoleMappings | ConvertFrom-Json Write-Host ($Invite | ConvertTo-Json -Compress) @@ -27,9 +27,9 @@ foreach ($role in $RoleMappings) { New-GraphPostRequest -NoAuthCheck $True -uri "https://graph.microsoft.com/beta/tenantRelationships/delegatedAdminRelationships/$($QueueItem)/accessAssignments" -tenantid $env:TenantID -type POST -body $MappingBody -verbose Start-Sleep -Milliseconds 100 } catch { - Write-LogMessage -API $APINAME -message "GDAP Group mapping failed - $($role.GroupId): $($_.Exception.Message)" -Sev 'Debug' + Write-LogMessage -API $APINAME -message "GDAP Group mapping failed - $($role.GroupId): $($_.Exception.Message)" -Sev Error exit 1 } - Write-LogMessage -API $APINAME -message "Groups mapped for GDAP Relationship: $($GdapInvite.RowKey)" + Write-LogMessage -API $APINAME -message "Groups mapped for GDAP Relationship: $($GdapInvite.RowKey)" -Sev Info } Remove-AzDataTableEntity @Table -Entity $Invite diff --git a/ExecMailboxRestore/function.json b/ExecMailboxRestore/function.json new file mode 100644 index 000000000000..bf6c3ef0c49a --- /dev/null +++ b/ExecMailboxRestore/function.json @@ -0,0 +1,18 @@ +{ + "scriptFile": "../Modules/CippEntryPoints/CippEntryPoints.psm1", + "entryPoint": "Receive-CippHttpTrigger", + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "Request", + "methods": ["get", "post"] + }, + { + "type": "http", + "direction": "out", + "name": "Response" + } + ] +} diff --git a/ExecOffboardUser/run.ps1 b/ExecOffboardUser/run.ps1 index 6195792e60ec..5e701c996166 100644 --- a/ExecOffboardUser/run.ps1 +++ b/ExecOffboardUser/run.ps1 @@ -45,7 +45,7 @@ try { } { $_."OOO" -ne "" } { - Set-CIPPOutOfOffice -tenantFilter $tenantFilter -userid $username -OOO $request.body.OOO -ExecutingUser $request.headers.'x-ms-client-principal' -APIName "ExecOffboardUser" + Set-CIPPOutOfOffice -tenantFilter $tenantFilter -userid $username -InternalMessage $request.body.OOO -ExternalMessage $request.body.OOO -ExecutingUser $request.headers.'x-ms-client-principal' -APIName "ExecOffboardUser" } { $_."forward" -ne "" } { Set-CIPPForwarding -userid $userid -username $username -tenantFilter $Tenantfilter -Forward $request.body.forward -KeepCopy [bool]$request.body.keepCopy -ExecutingUser $request.headers.'x-ms-client-principal' -APIName "ExecOffboardUser" diff --git a/GraphHelper.psm1 b/GraphHelper.psm1 index bcbcdf24eb0f..1848c74ecb09 100644 --- a/GraphHelper.psm1 +++ b/GraphHelper.psm1 @@ -505,7 +505,7 @@ function Get-Tenants { }) | Out-Null } foreach ($Tenant in $TenantList) { - if ($Tenant.defaultDomainName -eq 'Invalid') { continue } + if ($Tenant.defaultDomainName -eq 'Invalid' -or !$Tenant.defaultDomainName) { continue } $IncludedTenantsCache.Add(@{ RowKey = [string]$Tenant.customerId PartitionKey = 'Tenants' @@ -528,7 +528,7 @@ function Get-Tenants { Add-CIPPAzDataTableEntity @TenantsTable -Entity $IncludedTenantsCache } } - return ($IncludedTenantsCache | Sort-Object -Property displayName) + return ($IncludedTenantsCache | Where-Object -Property defaultDomainName -ne $null | Sort-Object -Property displayName) } diff --git a/ListMailboxRestores/function.json b/ListMailboxRestores/function.json new file mode 100644 index 000000000000..bf6c3ef0c49a --- /dev/null +++ b/ListMailboxRestores/function.json @@ -0,0 +1,18 @@ +{ + "scriptFile": "../Modules/CippEntryPoints/CippEntryPoints.psm1", + "entryPoint": "Receive-CippHttpTrigger", + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "Request", + "methods": ["get", "post"] + }, + { + "type": "http", + "direction": "out", + "name": "Response" + } + ] +} diff --git a/ListMailboxes/run.ps1 b/ListMailboxes/run.ps1 index c492cf59fcec..8f617d95006b 100644 --- a/ListMailboxes/run.ps1 +++ b/ListMailboxes/run.ps1 @@ -4,33 +4,44 @@ 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 to the Azure Functions log stream. -Write-Host "PowerShell HTTP trigger function processed a request." +Write-Host 'PowerShell HTTP trigger function processed a request.' # Interact with query parameters or the body of the request. $TenantFilter = $Request.Query.TenantFilter try { $users = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/?`$top=999&`$select=id,userPrincipalName,assignedLicenses" -Tenantid $tenantfilter - $GraphRequest = (New-ExoRequest -tenantid $TenantFilter -cmdlet "Get-mailbox") | Select-Object id, @{ Name = 'UPN'; Expression = { $_.'UserPrincipalName' } }, + + $ExoRequest = @{ + tenantid = $TenantFilter + cmdlet = 'Get-Mailbox' + } + + if ([bool]$Request.Query.SoftDeletedMailbox -eq $true) { + $ExoRequest.cmdParams = @{ SoftDeletedMailbox = $true } + } + + Write-Host ($ExoRequest | ConvertTo-Json) + + $GraphRequest = (New-ExoRequest @ExoRequest) | Select-Object id, ExchangeGuid, ArchiveGuid, @{ Name = 'UPN'; Expression = { $_.'UserPrincipalName' } }, @{ Name = 'displayName'; Expression = { $_.'DisplayName' } }, - @{ Name = 'SharedMailboxWithLicense'; Expression = { + @{ Name = 'SharedMailboxWithLicense'; Expression = { $ID = $_.id $Shared = if ($_.'RecipientTypeDetails' -eq 'SharedMailbox') { $true } else { $false } - if (($users | Where-Object -Property ID -EQ $ID).assignedLicenses.skuid -and $Shared) { $true } else { $false } - } + if (($users | Where-Object -Property ID -EQ $ID).assignedLicenses.skuid -and $Shared) { $true } else { $false } + } }, @{ Name = 'primarySmtpAddress'; Expression = { $_.'PrimarySMTPAddress' } }, @{ Name = 'recipientType'; Expression = { $_.'RecipientType' } }, @{ Name = 'recipientTypeDetails'; Expression = { $_.'RecipientTypeDetails' } }, - @{ Name = 'AdditionalEmailAddresses'; Expression = { ($_.'EmailAddresses' | Where-Object { $_ -clike 'smtp:*' }).Replace('smtp:', '') -join ", " } } + @{ Name = 'AdditionalEmailAddresses'; Expression = { ($_.'EmailAddresses' | Where-Object { $_ -clike 'smtp:*' }).Replace('smtp:', '') -join ', ' } } $StatusCode = [HttpStatusCode]::OK -} -catch { +} catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message $StatusCode = [HttpStatusCode]::Forbidden $GraphRequest = $ErrorMessage diff --git a/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 b/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 index 2c0a3eaf0047..81c0bdd6761a 100644 --- a/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 @@ -13,7 +13,7 @@ function Add-CIPPApplicationPermission { $ourSVCPrincipal = $ServicePrincipalList | Where-Object -Property AppId -EQ $ApplicationId if(!$ourSVCPrincipal) { #Our Service Principal isn't available yet. We do a sleep and reexecute after 3 seconds. - Start-Sleep -Seconds 3 + Start-Sleep -Seconds 5 $ServicePrincipalList = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=AppId,id,displayName&`$top=999" -skipTokenCache $true -tenantid $Tenantfilter $ourSVCPrincipal = $ServicePrincipalList | Where-Object -Property AppId -EQ $ApplicationId } diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecMailboxRestore.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecMailboxRestore.ps1 new file mode 100644 index 000000000000..f483d40ae7a7 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecMailboxRestore.ps1 @@ -0,0 +1,54 @@ +function Invoke-ExecMailboxRestore { + Param($Request, $TriggerMetadata) + + $APIName = $TriggerMetadata.FunctionName + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' + + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Body.TenantFilter + $RequestName = $Request.Body.RequestName + $SourceMailbox = $Request.Body.SourceMailbox + $TargetMailbox = $Request.Body.TargetMailbox + + try { + $ExoRequest = @{ + tenantid = $TenantFilter + cmdlet = 'New-MailboxRestoreRequest' + cmdParams = @{ + Name = $RequestName + SourceMailbox = $SourceMailbox + TargetMailbox = $TargetMailbox + AllowLegacyDNMismatch = $true + } + } + if ([bool]$Request.Body.AcceptLargeDataLoss -eq $true) { + $ExoRequest.cmdParams.AcceptLargeDataLoss = $true + } + if ([int]$Request.Body.BadItemLimit -gt 0) { + $ExoRequest.cmdParams.BadItemLimit = $Request.Body.BadItemLimit + } + if ([int]$Request.Body.LargeItemLimit -gt 0) { + $ExoRequest.cmdParams.LargeItemLimit = $Request.Body.LargeItemLimit + } + + $GraphRequest = New-ExoRequest @ExoRequest + + $Body = @{ + RestoreRequest = $GraphRequest + Results = @('Mailbox restore request started successfully') + } + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::Forbidden + $Body = @{ + RestoreRequest = $null + Results = @($ErrorMessage) + } + } + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $Body + }) +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListMailboxRestores.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListMailboxRestores.ps1 new file mode 100644 index 000000000000..f47458585c09 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListMailboxRestores.ps1 @@ -0,0 +1,43 @@ +function Invoke-ListMailboxRestores { + param($Request, $TriggerMetadata) + + $APIName = $TriggerMetadata.FunctionName + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' + + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Query.TenantFilter + try { + if ([bool]$Request.Query.Statistics -eq $true -and $Request.Query.Identity) { + $ExoRequest = @{ + tenantid = $TenantFilter + cmdlet = 'Get-MailboxRestoreRequestStatistics' + cmdParams = @{ Identity = $Request.Query.Identity } + } + + if ([bool]$Request.Query.IncludeReport -eq $true) { + $ExoRequest.cmdParams.IncludeReport = $true + } + $GraphRequest = New-ExoRequest @ExoRequest + + } else { + $ExoRequest = @{ + tenantid = $TenantFilter + cmdlet = 'Get-MailboxRestoreRequest' + } + + $RestoreRequests = (New-ExoRequest @ExoRequest) + $GraphRequest = $RestoreRequests + } + + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::Forbidden + $GraphRequest = $ErrorMessage + } + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + }) +} diff --git a/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 b/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 index c51b4ee90fae..73a2295b3be1 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 @@ -25,7 +25,7 @@ function Get-CIPPMFAState { $MFARegistration = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/reports/credentialUserRegistrationDetails' -tenantid $TenantFilter) } catch { - $CAState.Add('Not Licensed for Conditional Access') + $CAState.Add('Not Licensed for Conditional Access') | Out-Null $MFARegistration = $null } @@ -41,12 +41,12 @@ function Get-CIPPMFAState { if ($Policy.conditions.applications.includeApplications -ne 'All') { Write-Host $Policy.conditions.applications.includeApplications $CAState.Add("$($policy.displayName) - Specific Applications - $($policy.state)") | Out-Null - $Policy.conditions.users.excludeUsers.foreach({ $ExcludeSpecific.Add($_) }) + $Policy.conditions.users.excludeUsers.foreach({ $ExcludeSpecific.Add($_) | Out-Null }) continue } if ($Policy.conditions.users.includeUsers -eq 'All') { $CAState.Add("$($policy.displayName) - All Users - $($policy.state)") | Out-Null - $Policy.conditions.users.excludeUsers.foreach({ $ExcludeAllUsers.Add($_) }) + $Policy.conditions.users.excludeUsers.foreach({ $ExcludeAllUsers.Add($_) | Out-Null }) continue } } diff --git a/Modules/CIPPCore/Public/Get-CIPPPartnerAzSubscriptions.ps1 b/Modules/CIPPCore/Public/Get-CIPPPartnerAzSubscriptions.ps1 new file mode 100644 index 000000000000..ea67ec1a3f9e --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPPartnerAzSubscriptions.ps1 @@ -0,0 +1,64 @@ +function Get-CIPPPartnerAzSubscriptions { + param ( + $TenantFilter, + $APIName = "Get-CIPPPartnerAzSubscriptions" + ) + + try { + if ($variable -notmatch '[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}') { + $TenantFilter = (Invoke-RestMethod -Method GET "https://login.windows.net/$TenantFilter/.well-known/openid-configuration").token_endpoint.Split('/')[3] + } + } catch { + throw "Tenant $($TenantFilter) could not be found" + } + + $subsCache = [system.collections.generic.list[hashtable]]::new() + try { + try { + $usageRecords = (New-GraphGETRequest -Uri "https://api.partnercenter.microsoft.com/v1/customers/$($TenantFilter)/subscriptions/usagerecords" -scope "https://api.partnercenter.microsoft.com/user_impersonation").items + } catch { + throw "Unable to retrieve usagerecord(s): $($_.Exception.Message)" + } + + foreach ($usageRecord in $usageRecords) { + # if condition probably needs more refining + if ($usageRecord.offerId -notlike "DZH318Z0BPS6*") { + # Legacy subscriptions are directly accessible + $subDetails = @{ + tenantId = $tenantFilter + subscriptionId = ($usageRecord.id).ToLower() + isLegacy = $true + POR = "Legacy subscription" + status = $usageRecord.status + } + + $subsCache.Add($subDetails) + } else { + # For modern subscriptions we need to dig a little deeper + try { + $subid = (New-GraphGETRequest -Uri "https://api.partnercenter.microsoft.com/v1/customers/$($TenantFilter)/subscriptions/$($usageRecord.id)/azureEntitlements" -scope "https://api.partnercenter.microsoft.com/user_impersonation").items #| Where-Object { $_.status -eq "active" } + + foreach ($id in $subid) { + $subDetails = @{ + tenantId = $tenantFilter + subscriptionId = ($id.id) + isLegacy = $false + POR = $id.partnerOnRecord + status = $id.status + } + + $subsCache.Add($subDetails) + } + } catch { + # what do we do here error wise? + #Write-LogMessage -message "Unable to retrieve subscriptions(s) from usagerecord $($usageRecord.id): $($_.Exception.Message)" -Sev 'ERROR' -API $APINAME + #Write-Error "Unable to retrieve sub(s) from usagerecord $($usageRecord.id) for tenant $($tenantFilter): $($_.Exception.Message)" + } + } + } + + return $subsCache + } catch { + Write-LogMessage -message "Unable to retrieve CSP Azure subscriptions for $($TenantFilter): $($_.Exception.Message)" -Sev 'ERROR' -API $APINAME + } +} diff --git a/Modules/CIPPCore/Public/PermissionsTranslator.json b/Modules/CIPPCore/Public/PermissionsTranslator.json index a38d0786d5f1..a0ba05d3dc02 100644 --- a/Modules/CIPPCore/Public/PermissionsTranslator.json +++ b/Modules/CIPPCore/Public/PermissionsTranslator.json @@ -5312,5 +5312,14 @@ "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": "user_impersonation" + }, + { + "description": "Read and write all on-premises directory synchronization information", + "displayName": "Read and write all on-premises directory synchronization information", + "id": "c2d95988-7604-4ba1-aaed-38a5f82a51c7", + "Origin": "Delegated", + "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" } ] diff --git a/Modules/CIPPCore/Public/Remove-CIPPMailboxPermissions.ps1 b/Modules/CIPPCore/Public/Remove-CIPPMailboxPermissions.ps1 index 5c6ffb25dc38..1602e9861665 100644 --- a/Modules/CIPPCore/Public/Remove-CIPPMailboxPermissions.ps1 +++ b/Modules/CIPPCore/Public/Remove-CIPPMailboxPermissions.ps1 @@ -24,8 +24,8 @@ function Remove-CIPPMailboxPermissions { } "FullAccess" { $permissions = New-ExoRequest -tenantid $TenantFilter -cmdlet "Remove-MailboxPermission" -cmdParams @{Identity = $userid; user = $AccessUser; accessRights = @("FullAccess") } -Anchor $userid - Write-LogMessage -user $ExecutingUser -API $APIName -message "Removed FullAcess permissions for $($AccessUser) from $($userid)'s mailbox." -Sev "Info" -tenant $TenantFilter - "Removed FullAcess permissions for $($AccessUser) from $($userid)'s mailbox." + Write-LogMessage -user $ExecutingUser -API $APIName -message "Removed FullAccess permissions for $($AccessUser) from $($userid)'s mailbox." -Sev "Info" -tenant $TenantFilter + "Removed FullAccess permissions for $($AccessUser) from $($userid)'s mailbox." } } } diff --git a/Modules/CIPPCore/Public/Test-CIPPAccessPermissions.ps1 b/Modules/CIPPCore/Public/Test-CIPPAccessPermissions.ps1 new file mode 100644 index 000000000000..971753f68d40 --- /dev/null +++ b/Modules/CIPPCore/Public/Test-CIPPAccessPermissions.ps1 @@ -0,0 +1,131 @@ +function Test-CIPPAccessPermissions { + [CmdletBinding()] + param ( + $TenantFilter, + $APIName = "Access Check", + $ExecutingUser + ) + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Started permissions check' -Sev 'Debug' + $Messages = [System.Collections.Generic.List[string]]::new() + $MissingPermissions = [System.Collections.Generic.List[string]]::new() + $Links = [System.Collections.Generic.List[object]]::new() + $AccessTokenDetails = [PSCustomObject]@{ + AppId = '' + AppName = '' + Audience = '' + AuthMethods = '' + IPAddress = '' + Name = '' + Scope = '' + TenantId = '' + UserPrincipalName = '' + } + Write-Host "Setting success to true by default." + $Success = $true + try { + Set-Location (Get-Item $PSScriptRoot).FullName + $ExpectedPermissions = Get-Content '.\SAMManifest.json' | ConvertFrom-Json + + $GraphToken = Get-GraphToken -returnRefresh $true + if ($GraphToken) { + $GraphPermissions = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/myorganization/applications?`$filter=appId eq '$env:ApplicationID'" -NoAuthCheck $true + } + if ($env:MSI_SECRET) { + try { + Disable-AzContextAutosave -Scope Process | Out-Null + $AzSession = Connect-AzAccount -Identity + + $KV = $ENV:WEBSITE_DEPLOYMENT_ID + $KeyVaultRefresh = Get-AzKeyVaultSecret -VaultName $kv -Name 'RefreshToken' -AsPlainText + if ($ENV:RefreshToken -ne $KeyVaultRefresh) { + Write-Host "Setting success to false due to nonmaching token." + + $Success = $false + $Messages.Add('Your refresh token does not match key vault, clear your cache or wait 30 minutes.') | Out-Null + $Links.Add([PSCustomObject]@{ + Text = 'Clear Token Cache' + Href = 'https://docs.cipp.app/setup/installation/cleartokencache' + } + ) | Out-Null + } + else { + $Messages.Add('Your refresh token matches key vault.') | Out-Null + } + } + catch { + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message "Key vault exception: $($_) " -Sev 'Error' + } + } + + try { + $AccessTokenDetails = Read-JwtAccessDetails -Token $GraphToken.access_token -erroraction SilentlyContinue + } + catch { + $AccessTokenDetails = [PSCustomObject]@{ + Name = '' + AuthMethods = @() + } + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message "Token exception: $($_) " -Sev 'Error' + $Success = $false + Write-Host "Setting success to false due to not able to decode token." + + } + + if ($AccessTokenDetails.Name -eq '') { + $Messages.Add('Your refresh token is invalid, check for line breaks or missing characters.') | Out-Null + Write-Host "Setting success to false invalid token." + + $Success = $false + } + else { + if ($AccessTokenDetails.AuthMethods -contains 'mfa') { + $Messages.Add('Your access token contains the MFA claim.') | Out-Null + } + else { + $Messages.Add('Your access token does not contain the MFA claim, Refresh your SAM tokens.') | Out-Null + Write-Host "Setting success to False due to invalid list of claims." + + $Success = $false + $Links.Add([PSCustomObject]@{ + Text = 'MFA Troubleshooting' + Href = 'https://docs.cipp.app/troubleshooting/troubleshooting#multi-factor-authentication-troubleshooting' + } + ) | Out-Null + } + } + + $MissingPermissions = $ExpectedPermissions.requiredResourceAccess.ResourceAccess.id | Where-Object { $_ -notin $GraphPermissions.requiredResourceAccess.ResourceAccess.id } + if ($MissingPermissions) { + Write-Host "Setting success to False due to permissions issues: $($MissingPermissions | ConvertTo-Json)" + + $Translator = Get-Content '.\PermissionsTranslator.json' | ConvertFrom-Json + $TranslatedPermissions = $Translator | Where-Object id -In $MissingPermissions | ForEach-Object { "$($_.value) - $($_.Origin)" } + $MissingPermissions = @($TranslatedPermissions) + $Success = $false + $Links.Add([PSCustomObject]@{ + Text = 'Permissions' + Href = 'https://docs.cipp.app/setup/installation/permissions' + } + ) | Out-Null + } + else { + $Messages.Add('Your Secure Application Model has all required permissions') | Out-Null + } + + } + catch { + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Permissions check failed: $($_) " -Sev 'Error' + $Messages.Add("We could not connect to the API to retrieve the permissions. There might be a problem with the secure application model configuration. The returned error is: $(Get-NormalizedError -message $_)") | Out-Null + Write-Host "Setting success to False due to not being able to connect." + + $Success = $false + } + + return [PSCustomObject]@{ + AccessTokenDetails = $AccessTokenDetails + Messages = @($Messages) + MissingPermissions = @($MissingPermissions) + Links = @($Links) + Success = $Success + } +} diff --git a/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 b/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 new file mode 100644 index 000000000000..677b642e24c0 --- /dev/null +++ b/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 @@ -0,0 +1,118 @@ +function Test-CIPPAccessTenant { + [CmdletBinding()] + param ( + $TenantCSV, + $APIName = "Access Check", + $ExecutingUser + ) + $ExpectedRoles = @( + @{ Name = 'Application Administrator'; Id = '9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3' }, + @{ Name = 'User Administrator'; Id = 'fe930be7-5e62-47db-91af-98c3a49a38b1' }, + @{ Name = 'Intune Administrator'; Id = '3a2c62db-5318-420d-8d74-23affee5d9d5' }, + @{ Name = 'Exchange Administrator'; Id = '29232cdf-9323-42fd-ade2-1d097af3e4de' }, + @{ Name = 'Security Administrator'; Id = '194ae4cb-b126-40b2-bd5b-6091b380977d' }, + @{ Name = 'Cloud App Security Administrator'; Id = '892c5842-a9a6-463a-8041-72aa08ca3cf6' }, + @{ Name = 'Cloud Device Administrator'; Id = '7698a772-787b-4ac8-901f-60d6b08affd2' }, + @{ Name = 'Teams Administrator'; Id = '69091246-20e8-4a56-aa4d-066075b2a7a8' }, + @{ Name = 'Sharepoint Administrator'; Id = 'f28a1f50-f6e7-4571-818b-6a12f2af6b6c' }, + @{ Name = 'Authentication Policy Administrator'; Id = '0526716b-113d-4c15-b2c8-68e3c22b9f80' }, + @{ Name = 'Privileged Role Administrator'; Id = 'e8611ab8-c189-46e8-94e1-60213ab1f814' }, + @{ Name = 'Privileged Authentication Administrator'; Id = '7be44c8a-adaf-4e2a-84d6-ab2649e08a13' } + ) + $Tenants = ($TenantCSV).split(',') + if (!$Tenants) { $results = 'Could not load the tenants list from cache. Please run permissions check first, or visit the tenants page.' } + $TenantList = Get-Tenants + $TenantIds = foreach ($Tenant in $Tenants) { + ($TenantList | Where-Object { $_.defaultDomainName -eq $Tenant }).customerId + } + $MyRoles = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/managedTenants/myRoles?`$filter=tenantId in ('$($TenantIds -join "','")')" + $results = foreach ($tenant in $Tenants) { + $AddedText = '' + try { + $TenantId = ($TenantList | Where-Object { $_.defaultDomainName -eq $tenant }).customerId + $Assignments = ($MyRoles | Where-Object { $_.tenantId -eq $TenantId }).assignments + $SAMUserRoles = ($Assignments | Where-Object { $_.assignmentType -eq 'granularDelegatedAdminPrivileges' }).roles + + $BulkRequests = $ExpectedRoles | ForEach-Object { @( + @{ + id = "roleManagement_$($_.id)" + method = 'GET' + url = "roleManagement/directory/roleAssignments?`$filter=roleDefinitionId eq '$($_.id)'&`$expand=principal" + } + ) + } + $GDAPRolesGraph = New-GraphBulkRequest -tenantid $tenant -Requests $BulkRequests + $GDAPRoles = [System.Collections.Generic.List[object]]::new() + $MissingRoles = [System.Collections.Generic.List[object]]::new() + foreach ($RoleId in $ExpectedRoles) { + $GraphRole = $GDAPRolesGraph.body.value | Where-Object -Property roleDefinitionId -EQ $RoleId.Id + $Role = $GraphRole.principal | Where-Object -Property organizationId -EQ $ENV:tenantid + $SAMRole = $SAMUserRoles | Where-Object -Property templateId -EQ $RoleId.Id + if (!$Role) { + $MissingRoles.Add( + [PSCustomObject]@{ + Name = $RoleId.Name + Type = 'Tenant' + } + ) + $AddedText = 'but missing GDAP roles' + } + else { + $GDAPRoles.Add([PSCustomObject]$RoleId) + } + if (!$SAMRole) { + $MissingRoles.Add( + [PSCustomObject]@{ + Name = $RoleId.Name + Type = 'SAM User' + } + ) + $AddedText = 'but missing GDAP roles' + } + } + if (!($MissingRoles | Measure-Object).Count -gt 0) { + $MissingRoles = $true + } + @{ + TenantName = "$($Tenant)" + Status = "Successfully connected $($AddedText)" + GDAPRoles = $GDAPRoles + MissingRoles = $MissingRoles + SAMUserRoles = $SAMUserRoles + } + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message 'Tenant access check executed successfully' -Sev 'Info' + + } + catch { + @{ + TenantName = "$($tenant)" + Status = "Failed to connect: $(Get-NormalizedError -message $_.Exception.Message)" + GDAP = '' + } + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message "Tenant access check failed: $(Get-NormalizedError -message $_) " -Sev 'Error' + + } + + try { + $GraphRequest = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig' -ErrorAction Stop + @{ + TenantName = "$($Tenant)" + Status = 'Successfully connected to Exchange' + } + + } + catch { + $ReportedError = ($_.ErrorDetails | ConvertFrom-Json -ErrorAction SilentlyContinue) + $Message = if ($ReportedError.error.details.message) { $ReportedError.error.details.message } else { $ReportedError.error.innererror.internalException.message } + if ($null -eq $Message) { $Message = $($_.Exception.Message) } + @{ + TenantName = "$($Tenant)" + Status = "Failed to connect to Exchange: $(Get-NormalizedError -message $Message)" + } + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message "Tenant access check for Exchange failed: $(Get-NormalizedError -message $Message) " -Sev 'Error' + } + } + if (!$Tenants) { $results = 'Could not load the tenants list from cache. Please run permissions check first, or visit the tenants page.' } + + return $results +} diff --git a/Modules/CIPPCore/Public/Test-CIPPGDAPRelationships.ps1 b/Modules/CIPPCore/Public/Test-CIPPGDAPRelationships.ps1 new file mode 100644 index 000000000000..cf2bacb49e68 --- /dev/null +++ b/Modules/CIPPCore/Public/Test-CIPPGDAPRelationships.ps1 @@ -0,0 +1,101 @@ +function Test-CIPPGDAPRelationships { + [CmdletBinding()] + param ( + $TenantFilter, + $APIName = "Access Check", + $ExecutingUser + ) + + $GDAPissues = [System.Collections.ArrayList]@() + try { + #Get graph request to list all relationships. + $Relationships = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/delegatedAdminRelationships?`$filter=status eq 'active'" -tenantid $ENV:TenantID -NoAuthCheck $true + #Group relationships by tenant. The tenant information is in $relationships.customer.TenantId. + $RelationshipsByTenant = $Relationships | Group-Object -Property { $_.customer.TenantId } + foreach ($Tenant in $RelationshipsByTenant) { + if ($Tenant.Group.displayName.count -le 1 -and $Tenant.Group.displayName -like 'MLT_*') { + $GDAPissues.add([PSCustomObject]@{ + Type = "Error" + Issue = "This tenant only has a MLT(Microsoft Led Transition) relationship. This is a read-only relationship. You must migrate this tenant to GDAP." + Tenant = $Tenant.Group.customer.displayName + Relationship = $Tenant.Group.displayName + Link = "https://docs.cipp.app/setup/gdap/index" + }) | Out-Null + } + foreach ($Group in $Tenant.Group) { + if ("62e90394-69f5-4237-9190-012177145e10" -in $Group.accessDetails.unifiedRoles.roleDefinitionId) { + $GDAPissues.add([PSCustomObject]@{ + Type = "Warning" + Issue = "The relationship has global administrator access. Auto-Extend is not available." + Tenant = $Tenant.Group.customer.displayName | Out-String + Relationship = $group.displayName | Out-String + Link = "https://docs.cipp.app/setup/gdap/troubleshooting#autoextend" + + }) | Out-Null + } + } + + } + $me = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/me?$select=UserPrincipalName' -NoAuthCheck $true).UserPrincipalName + $CIPPGroupCount = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups/`$count?`$filter=startsWith(displayName,'M365 GDAP')" -NoAuthCheck $true -ComplexFilter + $SAMUserMemberships = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/me/memberOf?$select=id,displayName,isAssignableToRole' -NoAuthCheck $true + $ExpectedGroups = @( + 'AdminAgents', + 'M365 GDAP Application Administrator', + 'M365 GDAP User Administrator', + 'M365 GDAP Intune Administrator', + 'M365 GDAP Exchange Administrator', + 'M365 GDAP Security Administrator', + 'M365 GDAP Cloud App Security Administrator', + 'M365 GDAP Cloud Device Administrator', + 'M365 GDAP Teams Administrator', + 'M365 GDAP Sharepoint Administrator', + 'M365 GDAP Authentication Policy Administrator', + 'M365 GDAP Privileged Role Administrator', + 'M365 GDAP Privileged Authentication Administrator' + ) + $RoleAssignableGroups = $SAMUserMemberships | Where-Object { $_.isAssignableToRole } + $NestedGroups = foreach ($Group in $RoleAssignableGroups) { + New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups/$($Group.id)/memberOf?`$select=id,displayName" -NoAuthCheck $true + } + foreach ($Group in $ExpectedGroups) { + $GroupFound = $false + foreach ($Membership in ($SAMUserMemberships + $NestedGroups)) { + if ($Membership.displayName -match $Group -and (($CIPPGroupCount -gt 0 -and $Group -match 'M365 GDAP') -or $Group -notmatch 'M365 GDAP')) { + $GroupFound = $true + } + } + if (-not $GroupFound) { + $GDAPissues.add([PSCustomObject]@{ + Type = "Warning" + Issue = "$($Group) is not assigned to the SAM user $me. If you have migrated outside of CIPP this is to be expected. Please perform an access check to make sure you have the correct set of permissions." + Tenant = "*Partner Tenant" + Relationship = "None" + Link = "https://docs.cipp.app/setup/gdap/troubleshooting#groups" + + }) | Out-Null + } + if ($CIPPGroupCount -lt 12) { + $GDAPissues.add([PSCustomObject]@{ + Type = "Warning" + Issue = "We only found $($CIPPGroupCount) of the 12 required groups. If you have migrated outside of CIPP this is to be expected. Please perform an access check to make sure you have the correct set of permissions." + Tenant = "*Partner Tenant" + Relationship = "None" + Link = "https://docs.cipp.app/setup/gdap/troubleshooting#groups" + + }) | Out-Null + } + } + + } + catch { + Write-LogMessage -user $ExecutingUser -API $APINAME -message "Failed to run GDAP check for $($TenantFilter): $($_.Exception.Message)" -Sev "Error" + } + + return [PSCustomObject]@{ + GDAPIssues = @($GDAPissues) + MissingGroups = @($MissingGroups) + Memberships = @($SAMUserMemberships) + CIPPGroupCount = $CIPPGroupCount + } +} diff --git a/version_latest.txt b/version_latest.txt index ae153944ee8b..50021202769b 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -4.5.0 \ No newline at end of file +4.5.5 \ No newline at end of file