modules/ISC.EntraSourceSetup.psm1
|
#Requires -Version 5.1 Set-StrictMode -Version Latest function Import-SourceSetupModule { param( [Parameter(Mandatory)][string]$ModuleRoot, [Parameter(Mandatory)][string]$Name, [Parameter(Mandatory)][string]$FileName ) if (-not (Get-Module -Name $Name)) { Import-Module (Join-Path $ModuleRoot $FileName) -Force -Global -WarningAction SilentlyContinue } } function Import-AgentAdapterModule { if (-not (Get-Module -Name 'ISC.AgentAdapter')) { Import-Module (Join-Path $PSScriptRoot 'ISC.AgentAdapter.psm1') -Force -WarningAction SilentlyContinue } } function Initialize-EntraSourceSetup { param( [string]$ModuleRoot, [switch]$NonInteractive ) if (-not $ModuleRoot) { $ModuleRoot = $PSScriptRoot } Import-SourceSetupModule -ModuleRoot $ModuleRoot -Name 'ISC.OperatorConsole' -FileName 'ISC.OperatorConsole.psm1' Import-SourceSetupModule -ModuleRoot $ModuleRoot -Name 'ISC.OperatorToolchain' -FileName 'ISC.OperatorToolchain.psm1' Import-SourceSetupModule -ModuleRoot $ModuleRoot -Name 'ISC.EntraConnector' -FileName 'ISC.EntraConnector.psm1' Import-SourceSetupModule -ModuleRoot $ModuleRoot -Name 'ISC.EntraCiemConnector' -FileName 'ISC.EntraCiemConnector.psm1' Import-SourceSetupModule -ModuleRoot $ModuleRoot -Name 'ISC.PowerPlatformConnector' -FileName 'ISC.PowerPlatformConnector.psm1' Initialize-OperatorConsole -NonInteractive:$NonInteractive Initialize-EntraConnectorData Initialize-EntraCiemConnectorData } function Get-EntraAgentCatalog { Initialize-EntraSourceSetup $catalog = Get-EntraConnectorCatalog return [ordered]@{ permissionModes = @('Granular', 'Directory') featurePacks = @($catalog.FeaturePacks.Keys) directoryRoles = @($catalog.DirectoryRoleMap.Keys) requiredConfig = @('applicationName') optionalConfig = @('tenantId', 'permissionMode', 'features', 'directoryRole', 'rotateSecret', 'outputDirectory', 'agent365RedirectUri', 'powerPlatformEnvironmentUrl') secretFields = @('clientSecret', 'agent365RefreshToken') } } function Test-Agent365Selected { param([string[]]$FeatureNames) return @($FeatureNames) -contains 'Agent365' } # CopilotDiscovery is the pack that turns on Copilot Studio agent aggregation, and Copilot Studio is # the only part of it that needs anything outside Entra. function Test-CopilotStudioSelected { param([string[]]$FeatureNames) return @($FeatureNames) -contains 'CopilotDiscovery' } function Get-EntraResolvedConfig { param($Request) Import-AgentAdapterModule $config = Get-AgentRequestValue -Object $Request -Name 'config' -Default @{} $decisions = Get-AgentRequestValue -Object $Request -Name 'decisions' -Default @{} $secretRefs = Get-AgentRequestValue -Object $Request -Name 'secretRefs' -Default @{} return [PSCustomObject]@{ TenantId = $(if (Get-AgentRequestValue -Object $config -Name 'tenantId') { [string](Get-AgentRequestValue -Object $config -Name 'tenantId') } else { $null }) ApplicationName = [string](Get-AgentRequestValue -Object $config -Name 'applicationName') PermissionMode = if (Get-AgentRequestValue -Object $config -Name 'permissionMode') { [string](Get-AgentRequestValue -Object $config -Name 'permissionMode') } else { 'Granular' } Feature = @($(Get-AgentRequestValue -Object $config -Name 'features' -Default @())) DirectoryRole = if (Get-AgentRequestValue -Object $config -Name 'directoryRole') { [string](Get-AgentRequestValue -Object $config -Name 'directoryRole') } else { 'UserAdministrator' } SecretDisplayName = if (Get-AgentRequestValue -Object $config -Name 'secretDisplayName') { [string](Get-AgentRequestValue -Object $config -Name 'secretDisplayName') } else { 'ISC' } SecretValidityMonths = if (Get-AgentRequestValue -Object $config -Name 'secretValidityMonths') { [int](Get-AgentRequestValue -Object $config -Name 'secretValidityMonths') } else { 24 } RotateSecret = [bool](Get-AgentRequestValue -Object $config -Name 'rotateSecret' -Default $false) OutputDirectory = if (Get-AgentRequestValue -Object $config -Name 'outputDirectory') { [string](Get-AgentRequestValue -Object $config -Name 'outputDirectory') } else { (Join-Path (Get-Location) (Join-Path 'wizard-output' 'entra-id-isc')) } Agent365RedirectUri = if (Get-AgentRequestValue -Object $config -Name 'agent365RedirectUri') { [string](Get-AgentRequestValue -Object $config -Name 'agent365RedirectUri') } else { 'http://localhost:8400/' } PowerPlatformEnvironmentUrl = $(if (Get-AgentRequestValue -Object $config -Name 'powerPlatformEnvironmentUrl') { [string](Get-AgentRequestValue -Object $config -Name 'powerPlatformEnvironmentUrl') } else { $null }) ClientSecretRef = $(if (Get-AgentRequestValue -Object $secretRefs -Name 'clientSecret') { [string](Get-AgentRequestValue -Object $secretRefs -Name 'clientSecret') } else { $null }) Agent365RefreshTokenRef = $(if (Get-AgentRequestValue -Object $secretRefs -Name 'agent365RefreshToken') { [string](Get-AgentRequestValue -Object $secretRefs -Name 'agent365RefreshToken') } else { $null }) TargetAppObjectId = $(if (Get-AgentRequestValue -Object $decisions -Name 'targetAppObjectId') { [string](Get-AgentRequestValue -Object $decisions -Name 'targetAppObjectId') } else { $null }) CreateNewApplication = $(if ($null -ne (Get-AgentRequestValue -Object $decisions -Name 'createNewApplication')) { [bool](Get-AgentRequestValue -Object $decisions -Name 'createNewApplication') } else { $null }) AllowGlobalAdmin = [bool](Get-AgentRequestValue -Object $decisions -Name 'allowGlobalAdministrator' -Default $false) CreateSecret = $(if ($null -ne (Get-AgentRequestValue -Object $decisions -Name 'createSecret')) { [bool](Get-AgentRequestValue -Object $decisions -Name 'createSecret') } else { -not [bool](Get-AgentRequestValue -Object $config -Name 'skipSecret' -Default $false) }) } } function New-EntraAgentPlan { param([Parameter(Mandatory)]$Request) Initialize-EntraSourceSetup $resolved = Get-EntraResolvedConfig -Request $Request $needsInput = [System.Collections.Generic.List[object]]::new() $warnings = [System.Collections.Generic.List[string]]::new() $manualSteps = [System.Collections.Generic.List[string]]::new() if ([string]::IsNullOrWhiteSpace($resolved.ApplicationName)) { $needsInput.Add([ordered]@{ field = 'applicationName'; reason = 'Application display name is required.' }) } if ($resolved.DirectoryRole -eq 'GlobalAdministrator' -and -not $resolved.AllowGlobalAdmin) { $needsInput.Add([ordered]@{ field = 'decisions.allowGlobalAdministrator'; reason = 'Global Administrator is high risk; set true to proceed.' }) } $discoveries = [ordered]@{} $mutations = @('connect-graph', 'resolve-permissions', 'create-or-update-app', 'grant-consent', 'assign-directory-roles') if ($resolved.CreateSecret -or $resolved.RotateSecret) { $mutations += 'create-client-secret' } if (Test-Agent365Selected -FeatureNames $resolved.Feature) { $mutations += @('register-redirect-uri', 'grant-delegated-consent', 'issue-agent365-refresh-token') if (-not $resolved.Agent365RefreshTokenRef) { if (-not ($resolved.CreateSecret -or $resolved.RotateSecret) -and -not $resolved.ClientSecretRef) { $needsInput.Add([ordered]@{ field = 'secretRefs.clientSecret' reason = 'Minting the Agent 365 refresh token needs the client secret. Supply it, rotate the secret, or pass secretRefs.agent365RefreshToken.' }) } $manualSteps.Add('Agent 365: a browser sign-in as an AI Administrator with a Microsoft Agent 365 license is required during Apply.') } } if (Test-CopilotStudioSelected -FeatureNames $resolved.Feature) { $mutations += 'configure-copilot-studio-dataverse-access' $manualSteps.Add('Copilot Studio: the Azure CLI must be signed in (az login) as a Power Platform administrator so the Dataverse application user can be created.') } if (-not [string]::IsNullOrWhiteSpace($resolved.ApplicationName)) { try { Ensure-MicrosoftGraphModules $session = Connect-EntraGraph -RequestedTenantId $resolved.TenantId $discoveries.tenantId = $session.TenantId $discoveries.domainName = $session.DomainName $existingApps = @(Find-EntraApplicationByName -DisplayName $resolved.ApplicationName) $discoveries.existingApplications = @($existingApps | ForEach-Object { [ordered]@{ objectId = $_.Id; appId = $_.AppId; displayName = $_.DisplayName } }) if ($existingApps.Count -eq 1 -and -not $resolved.TargetAppObjectId -and $null -eq $resolved.CreateNewApplication) { $needsInput.Add([ordered]@{ field = 'decisions' reason = "Application '$($resolved.ApplicationName)' already exists." options = @( [ordered]@{ decision = 'targetAppObjectId'; value = $existingApps[0].Id; label = 'Update existing application' } [ordered]@{ decision = 'createNewApplication'; value = $true; label = 'Create new application with a different name' } ) }) } if ($existingApps.Count -gt 1 -and -not $resolved.TargetAppObjectId -and $null -eq $resolved.CreateNewApplication) { $needsInput.Add([ordered]@{ field = 'decisions.targetAppObjectId' reason = 'Multiple applications share this display name.' options = @($existingApps | ForEach-Object { [ordered]@{ value = $_.Id; appId = $_.AppId; displayName = $_.DisplayName } }) }) } } catch { $warnings.Add("Live discovery skipped: $($_.Exception.Message)") $manualSteps.Add('Sign in with Microsoft Graph (Connect-MgGraph) before Apply.') } } $manualSteps.Add('Grant admin consent in Entra portal if automated consent fails.') $manualSteps.Add('Assign Reader role at tenant root management group for read-only Azure cloud object access.') $status = if ($needsInput.Count -gt 0) { 'needsInput' } else { 'ready' } return [ordered]@{ status = $status requestHash = (Get-AgentRequestHash -Request $Request) resolved = $resolved discoveries = $discoveries mutations = $mutations prerequisites = @('Microsoft Graph PowerShell modules', 'Entra admin rights') warnings = @($warnings) manualSteps = @($manualSteps) needsInput = @($needsInput) } } function Invoke-EntraSourceApply { param( [Parameter(Mandatory)]$Resolved, [string]$TargetAppObjectId, [bool]$CreateSecret, [switch]$WhatIf ) $catalog = Get-EntraConnectorCatalog $needsAgent365 = Test-Agent365Selected -FeatureNames $Resolved.Feature $access = if ($Resolved.PSObject.Properties['Access']) { [string]$Resolved.Access } else { '' } $delegated = @(Get-SelectedDelegatedPermissions -FeatureNames $Resolved.Feature -Access $access) # Writing an oauth2PermissionGrant needs a scope the base sign-in does not request, so it is only # asked for when delegated permissions are actually in play. $extraScopes = if ($delegated.Count -gt 0) { @('DelegatedPermissionGrant.ReadWrite.All') } else { @() } $session = Connect-EntraGraph -RequestedTenantId $Resolved.TenantId -AdditionalScopes $extraScopes $TenantId = $session.TenantId $isUpdate = [bool]$TargetAppObjectId $permissions = @(Get-SelectedPermissions -Mode $Resolved.PermissionMode -FeatureNames $Resolved.Feature -Access $access) $roleNames = @($catalog.DirectoryRoleMap[$Resolved.DirectoryRole]) if ($WhatIf) { return [PSCustomObject]@{ Session = $session IsUpdate = $isUpdate Permissions = $permissions DelegatedPermissions = $delegated RoleNames = $roleNames ConsentFailures = @() Pair = $null SecretValue = $null SecretExpires = $null AssignedRoles = @() Agent365RefreshToken = $null Agent365Pending = $null CopilotStudioAccess = @() } } Write-Step 'Resolving API permissions' $rolesByResource = @{} $resourceServicePrincipals = @{} foreach ($group in ($permissions | Group-Object Resource)) { $resourceKey = $group.Name $resource = $catalog.Resources[$resourceKey] $resourceSp = Get-ApiServicePrincipal -AppId $resource.AppId -Name $resource.Name if (-not $resourceSp) { if ($resource.Required) { throw "The $($resource.Name) service principal was not found in tenant $TenantId." } continue } $roles = @(Resolve-AppRoles -ServicePrincipal $resourceSp -PermissionValues @($group.Group.Value) -ResourceName $resource.Name) if ($roles.Count -eq 0) { if ($resource.Required) { throw "No $($resource.Name) application permissions could be resolved." } continue } $rolesByResource[$resourceKey] = $roles $resourceServicePrincipals[$resourceKey] = $resourceSp } $scopesByResource = @{} if ($delegated.Count -gt 0) { Write-Step 'Resolving delegated permissions' foreach ($group in ($delegated | Group-Object Resource)) { $resourceKey = $group.Name $resource = $catalog.Resources[$resourceKey] if (-not $resourceServicePrincipals.ContainsKey($resourceKey)) { $resourceSp = Get-ApiServicePrincipal -AppId $resource.AppId -Name $resource.Name if (-not $resourceSp) { Write-Warning "The $($resource.Name) service principal was not found; skipping its delegated permissions." continue } $resourceServicePrincipals[$resourceKey] = $resourceSp } $scopes = @(Resolve-DelegatedScopes -ServicePrincipal $resourceServicePrincipals[$resourceKey] ` -PermissionValues @($group.Group.Value) -ResourceName $resource.Name) if ($scopes.Count -gt 0) { $scopesByResource[$resourceKey] = $scopes } } } Write-Step $(if ($isUpdate) { 'Updating application' } else { 'Creating application' }) if ($isUpdate) { $pair = Get-EntraConnectorApplication -ApplicationObjectId $TargetAppObjectId Write-Ok "Using application $($pair.Application.AppId)" } else { $pair = New-EntraConnectorApplication -DisplayName $Resolved.ApplicationName Write-Ok "Created application $($pair.Application.AppId)" } Write-Step 'Updating requested API permissions' Set-RequiredResourceAccess -Application $pair.Application -RolesByResource $rolesByResource -ScopesByResource $scopesByResource Write-Ok 'Application manifest updated' if ($needsAgent365) { Write-Step 'Registering the Agent 365 redirect URI' Set-EntraRedirectUri -Application $pair.Application -RedirectUri $Resolved.Agent365RedirectUri | Out-Null } Write-Step 'Granting admin consent' $consentFailures = [System.Collections.Generic.List[string]]::new() foreach ($resourceKey in $rolesByResource.Keys) { $result = Grant-AppRoleConsent -ServicePrincipal $pair.ServicePrincipal ` -ResourceServicePrincipal $resourceServicePrincipals[$resourceKey] ` -AppRoles $rolesByResource[$resourceKey] foreach ($failure in $result.Failed) { $consentFailures.Add($failure) } } foreach ($resourceKey in $scopesByResource.Keys) { $result = Grant-DelegatedConsent -ServicePrincipal $pair.ServicePrincipal ` -ResourceServicePrincipal $resourceServicePrincipals[$resourceKey] ` -Scopes $scopesByResource[$resourceKey] foreach ($failure in $result.Failed) { $consentFailures.Add($failure) } } $assignedRoles = @() if ($roleNames.Count -gt 0) { Write-Step 'Assigning directory roles' $assignedRoles = @(Add-EntraDirectoryRoles -ServicePrincipal $pair.ServicePrincipal -RoleDisplayNames $roleNames) } $secretValue = $null $secretExpires = $null if ($CreateSecret) { Write-Step 'Creating client secret' $secret = New-EntraClientSecret -Application $pair.Application -DisplayName $Resolved.SecretDisplayName -ValidityMonths $Resolved.SecretValidityMonths Write-Ok "Secret '$($Resolved.SecretDisplayName)' created, expires $($secret.EndDateTime)" $secretValue = $secret.SecretText $secretExpires = $secret.EndDateTime } $copilotStudioAccess = @() if (Test-CopilotStudioSelected -FeatureNames $Resolved.Feature) { Write-Step 'Copilot Studio Dataverse access' $copilotStudioAccess = @(Set-CopilotStudioAccessForApply -Resolved $Resolved -ClientId $pair.Application.AppId -WhatIf:$WhatIf) } $agent365Token = $null $agent365Pending = $null if ($needsAgent365) { Write-Step 'Microsoft Agent 365 refresh token' $agent365 = Get-Agent365RefreshTokenForApply -Resolved $Resolved -TenantId $TenantId -ClientId $pair.Application.AppId -SecretValue $secretValue $agent365Token = $agent365.Token $agent365Pending = $agent365.Pending } return [PSCustomObject]@{ Session = $session IsUpdate = $isUpdate Permissions = $permissions DelegatedPermissions = $delegated RoleNames = $roleNames ConsentFailures = @($consentFailures) Pair = $pair SecretValue = $secretValue SecretExpires = $secretExpires AssignedRoles = $assignedRoles Agent365RefreshToken = $agent365Token Agent365Pending = $agent365Pending CopilotStudioAccess = @($copilotStudioAccess) } } # The refresh token is redeemed by ISC with the same client credentials, so it has to be minted as a # confidential client: without the secret value there is nothing to exchange the authorization code # with, and Entra never shows an existing secret again. function Get-Agent365RefreshTokenForApply { param( [Parameter(Mandatory)]$Resolved, [Parameter(Mandatory)][string]$TenantId, [Parameter(Mandatory)][string]$ClientId, [string]$SecretValue ) if ($Resolved.Agent365RefreshTokenRef) { Import-AgentAdapterModule Write-Ok 'Using the supplied refresh token' return [PSCustomObject]@{ Token = (Resolve-AgentSecretReference -Reference $Resolved.Agent365RefreshTokenRef); Pending = $null } } $clientSecret = $SecretValue if (-not $clientSecret -and $Resolved.ClientSecretRef) { Import-AgentAdapterModule $clientSecret = Resolve-AgentSecretReference -Reference $Resolved.ClientSecretRef } if (-not $clientSecret -and -not (Test-OperatorNonInteractive)) { Write-Info 'Minting the token needs the client secret of this application, which Entra displays only once.' $clientSecret = Read-InputString -Prompt 'Existing client secret (blank to skip the refresh token)' -Mask } if (-not $clientSecret) { $pending = 'no client secret was available to mint it. Re-run with -RotateSecret' Write-Warning "Agent 365 refresh token skipped: $pending" return [PSCustomObject]@{ Token = $null; Pending = $pending } } try { $token = Get-EntraAgent365RefreshToken -TenantId $TenantId -ClientId $ClientId ` -ClientSecret $clientSecret -RedirectUri $Resolved.Agent365RedirectUri Write-Ok 'Refresh token issued' return [PSCustomObject]@{ Token = $token; Pending = $null } } catch { $pending = $_.Exception.Message Write-Warning "Agent 365 refresh token not issued: $pending" return [PSCustomObject]@{ Token = $null; Pending = $pending } } } # Copilot Studio agents are read out of Dataverse, so the application needs an application user in # every Power Platform environment that holds agents. Environments are discovered rather than asked # for, because an operator rarely knows the Dataverse org URL by heart. function Set-CopilotStudioAccessForApply { param( [Parameter(Mandatory)]$Resolved, [Parameter(Mandatory)][string]$ClientId, [switch]$WhatIf ) $results = [System.Collections.Generic.List[object]]::new() try { $azPath = Ensure-AzCliCommand } catch { Write-Warning "Skipping the Copilot Studio Dataverse setup: $($_.Exception.Message)" return @($results) } $environments = @() if ($Resolved.PowerPlatformEnvironmentUrl) { $environments = @([PSCustomObject]@{ Name = $Resolved.PowerPlatformEnvironmentUrl; Url = $Resolved.PowerPlatformEnvironmentUrl }) } else { try { $environments = @(Get-PowerPlatformEnvironment -AzPath $azPath) } catch { Write-Warning "Could not discover Power Platform environments: $($_.Exception.Message)" return @($results) } } if ($environments.Count -eq 0) { Write-Warning 'No Power Platform environments were found for the signed-in account.' return @($results) } $selected = $environments if ($environments.Count -gt 1 -and -not (Test-OperatorNonInteractive)) { $labels = @($environments | ForEach-Object { "$($_.Name) - $($_.Url)" }) $chosen = @(Read-MultiChoice -Options @($environments.Url) -Labels $labels ` -Prompt 'Power Platform environments holding Copilot Studio agents') if ($chosen.Count -gt 0) { $selected = @($environments | Where-Object { $chosen -contains $_.Url }) } } foreach ($environment in $selected) { $results.Add((Set-CopilotStudioDataverseAccess -EnvironmentUrl $environment.Url -ClientId $ClientId ` -EnvironmentName $environment.Name -WhatIf:$WhatIf -AzPath $azPath)) } return @($results) } function Build-EntraSourceResult { param( [Parameter(Mandatory)]$Resolved, [Parameter(Mandatory)]$ApplyResult, [string]$RunDirectory ) # Secrets are written through the agent adapter, which the wizard does not load on its own. Import-AgentAdapterModule $pair = $ApplyResult.Pair $domainName = if ($ApplyResult.Session.DomainName) { $ApplyResult.Session.DomainName } else { $ApplyResult.Session.TenantId } $situation = [System.Collections.Generic.List[string]]::new() $situation.Add('Entra ID application setup is complete. Paste the Connection Settings values into ISC.') if ($ApplyResult.ConsentFailures.Count -gt 0) { $situation.Add("Pending: grant admin consent in the Entra portal for: $($ApplyResult.ConsentFailures -join ', ').") } if ($ApplyResult.RoleNames.Count -gt 0) { $missingRoles = @($ApplyResult.RoleNames | Where-Object { $ApplyResult.AssignedRoles -notcontains $_ }) if ($missingRoles.Count -gt 0) { $situation.Add("Pending: assign directory role(s) to the service principal: $($missingRoles -join ', ').") } } if ($ApplyResult.SecretValue) { $situation.Add('Store the client secret now — Microsoft will not display it again.') } $copilotAccess = @() if ($ApplyResult.PSObject.Properties['CopilotStudioAccess']) { $copilotAccess = @($ApplyResult.CopilotStudioAccess) } foreach ($environment in $copilotAccess) { if ($environment.Error) { $situation.Add("Pending: Copilot Studio access in $($environment.Environment) could not be configured ($($environment.Error)).") } elseif ($environment.RolesMissing.Count -gt 0) { $situation.Add("Pending: assign $($environment.RolesMissing -join ', ') to the application user in $($environment.Environment).") } } $fields = [ordered]@{ 'Grant Type' = 'Client Credentials' 'Client ID' = [string]$pair.Application.AppId 'Domain Name' = $domainName } $secretArtifacts = @() if ($ApplyResult.SecretValue) { $secretPath = Join-Path $RunDirectory 'client-secret.txt' Set-AgentRestrictedFile -Path $secretPath -Content $ApplyResult.SecretValue $fields['Client Secret'] = $ApplyResult.SecretValue $secretArtifacts += [ordered]@{ label = 'Client Secret'; path = $secretPath } } if ($ApplyResult.Agent365RefreshToken) { $tokenPath = Join-Path $RunDirectory 'agent365-refresh-token.txt' Set-AgentRestrictedFile -Path $tokenPath -Content $ApplyResult.Agent365RefreshToken $fields['Agent 365 Refresh Token'] = $ApplyResult.Agent365RefreshToken $secretArtifacts += [ordered]@{ label = 'Agent 365 Refresh Token'; path = $tokenPath } $situation.Add('The Agent 365 refresh token belongs in Machine Identity Governance Settings, not Connection Settings.') } elseif ($ApplyResult.Agent365Pending) { $situation.Add("Pending: the Agent 365 refresh token was not issued ($($ApplyResult.Agent365Pending)). Aggregation fails until it is set.") } if (-not (Test-Path -LiteralPath $Resolved.OutputDirectory)) { New-Item -ItemType Directory -Path $Resolved.OutputDirectory -Force | Out-Null } $settingsPath = Join-Path $Resolved.OutputDirectory 'sailpoint-entra-connection-settings.txt' Write-ConnectionSettings -Fields $fields -Title 'ISC source Connection Settings' -Path $settingsPath $publicSettings = [ordered]@{ 'Grant Type' = 'Client Credentials' 'Client ID' = [string]$pair.Application.AppId 'Domain Name' = $domainName } $portalUrl = "https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Overview/appId/$($pair.Application.AppId)" $permissionsUrl = "https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/CallAnAPI/appId/$($pair.Application.AppId)" foreach ($checklistItem in @(Get-EntraIscFeatureChecklist -FeatureNames $Resolved.Feature)) { $situation.Add("ISC: $checklistItem") } $ciemCtx = @{ FeatureNames = $Resolved.Feature } Apply-EmbeddedCiemPrerequisites -Context $ciemCtx foreach ($entry in (Build-EmbeddedCiemConnectionSettings -Context $ciemCtx).GetEnumerator()) { $situation.Add("ISC: $($entry.Key) — $($entry.Value)") } $completionItems = @( [PSCustomObject]@{ Label = 'Grant Type'; Value = 'Client Credentials'; Kind = 'Copy'; Mask = $false } [PSCustomObject]@{ Label = 'Client ID'; Value = [string]$pair.Application.AppId; Kind = 'Copy'; Mask = $false } [PSCustomObject]@{ Label = 'Domain Name'; Value = $domainName; Kind = 'Copy'; Mask = $false } ) if ($ApplyResult.SecretValue) { $completionItems += [PSCustomObject]@{ Label = 'Client Secret'; Value = $ApplyResult.SecretValue; Kind = 'Copy'; Mask = $true } } if ($ApplyResult.Agent365RefreshToken) { $completionItems += [PSCustomObject]@{ Label = 'Agent 365 Refresh Token (Machine Identity Governance Settings)' Value = $ApplyResult.Agent365RefreshToken Kind = 'Copy' Mask = $true } } $completionItems += [PSCustomObject]@{ Label = 'Entra app overview'; Value = $portalUrl; Kind = 'Open'; Mask = $false } if ($ApplyResult.ConsentFailures.Count -gt 0) { $completionItems += [PSCustomObject]@{ Label = 'API permissions (grant consent)'; Value = $permissionsUrl; Kind = 'Open'; Mask = $false } } return [ordered]@{ status = 'ok' connectionSettings = $publicSettings artifacts = @([ordered]@{ label = 'Connection settings'; path = $settingsPath }) secretArtifacts = $secretArtifacts manualSteps = @($situation) verification = [ordered]@{ appId = $pair.Application.AppId; tenantId = $ApplyResult.Session.TenantId } situation = @($situation) completionItems = $completionItems } } function Invoke-EntraAgentApply { param( [Parameter(Mandatory)]$Request, [Parameter(Mandatory)]$Plan, [switch]$WhatIf ) Initialize-EntraSourceSetup Ensure-MicrosoftGraphModules $resolved = Get-EntraResolvedConfig -Request $Request if ($Plan.resolved) { foreach ($prop in $Plan.resolved.PSObject.Properties) { $resolved.$($prop.Name) = $prop.Value } } $targetAppObjectId = $resolved.TargetAppObjectId $createSecret = if ($resolved.CreateSecret) { $true } elseif ($resolved.RotateSecret) { $true } else { -not $targetAppObjectId } $applyResult = Invoke-EntraSourceApply -Resolved $resolved -TargetAppObjectId $targetAppObjectId -CreateSecret $createSecret -WhatIf:$WhatIf if ($WhatIf) { return [ordered]@{ status = 'whatIf'; mutations = $Plan.mutations; resolved = $resolved } } $run = New-AgentRunDirectory -ConnectorSlug 'entra-id-isc' -RunId (Get-AgentRequestValue -Object $Request -Name 'runId') return Build-EntraSourceResult -Resolved $resolved -ApplyResult $applyResult -RunDirectory $run.Path } Export-ModuleMember -Function @( 'Initialize-EntraSourceSetup' 'Test-Agent365Selected' 'Test-CopilotStudioSelected' 'Set-CopilotStudioAccessForApply' 'Get-EntraAgentCatalog' 'Get-EntraResolvedConfig' 'New-EntraAgentPlan' 'Invoke-EntraSourceApply' 'Build-EntraSourceResult' 'Invoke-EntraAgentApply' ) |