modules/Definitions/Azure.Connector.psm1

Set-StrictMode -Version Latest

function Get-AzureLegacyArguments {
    param([Parameter(Mandatory)][hashtable]$Context)
    # Context is a hashtable of optional keys: indexer access (see Aws.Connector).
    # Granular is the default for both access levels. Directory is only the coarse
    # opt-in, and it does not cover delete-user. ReadWrite must not select it implicitly.
    $access = $Context['Access']
    $permissionMode = if ($access -eq 'ReadWrite' -and $Context['PermissionMode']) { $Context['PermissionMode'] } else { 'Granular' }
    $directoryRole = if ($access -eq 'ReadOnly') { 'None' } elseif ($Context['DirectoryRole']) { $Context['DirectoryRole'] } else { 'UserAdministrator' }
    $parameters = @{
        PermissionMode       = $permissionMode
        DirectoryRole        = $directoryRole
        SecretValidityMonths = if ($Context['SecretValidityMonths']) { $Context['SecretValidityMonths'] } else { 24 }
        RotateSecret         = [bool]$Context['RotateSecret']
        NonInteractive       = [bool]$Context['NonInteractive']
    }
    foreach ($name in @('TenantId', 'ApplicationName', 'Access', 'SecretDisplayName', 'OutputDirectory', 'Agent365RedirectUri', 'PowerPlatformEnvironmentUrl')) {
        if (-not [string]::IsNullOrWhiteSpace([string]$Context[$name])) { $parameters[$name] = $Context[$name] }
    }
    if ($Context.ContainsKey('Feature') -and $null -ne $Context['Feature']) {
        $features = [System.Collections.Generic.List[string]]::new()
        foreach ($entry in @($Context['Feature'])) {
            foreach ($part in ([string]$entry -split ',')) {
                $featureName = $part.Trim()
                if ($featureName) { $features.Add($featureName) }
            }
        }
        $parameters.Feature = $features.ToArray()
    }
    return $parameters
}

function Invoke-AzureLegacyFromContext {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param([Parameter(Mandatory)][hashtable]$Context)
    $impl = Join-Path $PSScriptRoot '../Legacy/Azure.Implementation.ps1'
    $parameters = Get-AzureLegacyArguments -Context $Context
    & $impl @parameters -WhatIf:$WhatIfPreference
}

function Register-AzureConnectorDefinitions {
    Register-IntegrationWizardDefinition @{
        Id           = 'azure-connector'
        Platform     = 'Azure'
        Product      = 'ISC'
        CoreSwitch   = 'Connector'
        CliName      = 'az'
        Title        = 'Microsoft Entra ID connector application'
        FolderName   = 'Microsoft-Entra-ID'
        AccessLevels = @('ReadOnly', 'ReadWrite')
        AdminRoles   = @('Application Administrator', 'Privileged Role Administrator or Global Administrator')
        OutputsHelp  = 'Paste Tenant ID, Client ID, and Client Secret into ISC Connection Settings.'
        Inputs       = @(
            @{ Name = 'TenantId'; Source = 'Supply'; Prompt = 'Tenant ID or domain'; Purpose = 'Microsoft Entra tenant'; Required = $false }
            @{ Name = 'ApplicationName'; Source = 'Discover'; Prompt = 'Application display name'; Purpose = 'App registration name'; Required = $true }
            @{ Name = 'Feature'; Source = 'Supply'; Prompt = 'Feature packs (comma-separated)'; Purpose = 'Optional ISC feature permissions'; Required = $false }
            @{ Name = 'OutputDirectory'; Source = 'Supply'; Prompt = 'Output directory'; Purpose = 'Wizard output directory'; Required = $false }
            @{ Name = 'RotateSecret'; Source = 'Supply'; Prompt = 'Rotate secret'; Purpose = 'Create new client secret on update'; Required = $false }
        )
        CheckInputs  = @(
            @{ Name = 'TenantId'; Source = 'Supply'; Prompt = 'Azure tenant ID'; Purpose = 'Microsoft Entra tenant ID'; Required = $true }
            @{ Name = 'ClientId'; Source = 'Supply'; Prompt = 'Azure Client ID'; Purpose = 'Application (client) ID'; Required = $true }
            @{ Name = 'ClientSecret'; Source = 'Supply'; Prompt = 'Azure Client Secret'; Purpose = 'Client secret value'; Required = $true; Secret = $true }
            @{ Name = 'OutputDirectory'; Source = 'Supply'; Prompt = 'Output directory'; Purpose = 'Wizard output directory'; Required = $false }
        )
        Steps        = { param($ctx) @() }
        BuildScript  = {
            param($c)
            $appName = if ($c['ApplicationName']) { [string]$c['ApplicationName'] } else { 'SailPoint-ISC-Connector' }
            $access = if ($c['Access']) { [string]$c['Access'] } else { 'ReadOnly' }
            $tenantId = if ($c['TenantId']) { [string]$c['TenantId'] } else { '' }
            $featureStr = if ($c['Feature']) { [string]$c['Feature'] } else { '' }

            $features = @()
            if ($featureStr) {
                foreach ($f in ($featureStr -split ',')) {
                    $trim = $f.Trim()
                    if ($trim -and -not ($features -contains $trim)) { $features += $trim }
                }
            }

            $hasCopilot = $features -contains 'CopilotDiscovery'
            $hasAgent365 = $features -contains 'Agent365'

            @"
# -----------------------------------------------------------------------------
# Microsoft Entra ID (Azure AD) SaaS Connector Setup
# -----------------------------------------------------------------------------
`$appName = '$appName'
`$access = '$access'
`$tenantId = '$tenantId'

Write-Host "Checking Azure CLI authentication..." -ForegroundColor Cyan
if (Get-Command az -ErrorAction SilentlyContinue) {
    `$accountJson = & az account show --output json 2>`$null
    if (`$LASTEXITCODE -ne 0 -or -not `$accountJson) {
        throw 'Not signed in to Azure CLI. Run: az login'
    }
    `$account = `$accountJson | ConvertFrom-Json
    if (-not `$tenantId) { `$tenantId = `$account.tenantId }
    Write-Host "Tenant ID: `$tenantId" -ForegroundColor Green

    Write-Host "Ensuring Microsoft Entra application registration for '`$appName'..." -ForegroundColor Cyan
    `$appId = (& az ad app list --display-name "`$appName" --query "[0].appId" -o tsv 2>`$null)

    if (-not `$appId) {
        Write-Host "Creating application '`$appName'..."
        `$appId = (& az ad app create --display-name "`$appName" --query "appId" -o tsv)
        Write-Host "Created application with App ID (Client ID): `$appId" -ForegroundColor Green
    } else {
        Write-Host "Found existing application with App ID (Client ID): `$appId" -ForegroundColor Green
    }

    # Ensure service principal
    `$spId = (& az ad sp list --filter "appId eq '`$appId'" --query "[0].id" -o tsv 2>`$null)
    if (-not `$spId) {
        Write-Host "Creating enterprise service principal..."
        `$spId = (& az ad sp create --id `$appId --query "id" -o tsv)
    }

    Write-Host "Creating client secret..." -ForegroundColor Cyan
    `$secretJson = & az ad app credential reset --id `$appId --display-name "ISC" --years 2 --output json 2>`$null
    `$secret = (`$secretJson | ConvertFrom-Json).password

    # Microsoft Graph API Permissions (Application ID 00000003-0000-0000-c000-000000000000)
    Write-Host "Configuring Microsoft Graph API permissions..." -ForegroundColor Cyan
    # Baseline: User.Read.All (df0212d2-b372-4deb-8c53-ebc0d0f64a48), Group.Read.All (5b56725b-5fb0-4f73-9e96-6101174b0973), Directory.Read.All (7ab7862c-4c0f-45da-bb66-073eaec292e8)
    & az ad app permission add --id `$appId --api 00000003-0000-0000-c000-000000000000 --api-permissions df0212d2-b372-4deb-8c53-ebc0d0f64a48=Role 5b56725b-5fb0-4f73-9e96-6101174b0973=Role 7ab7862c-4c0f-45da-bb66-073eaec292e8=Role 2>`$null | Out-Null
$(if ($hasCopilot) {
@"

    # -------------------------------------------------------------------------
    # Copilot Studio & Foundry Agent Discovery Permissions
    # -------------------------------------------------------------------------
    Write-Host "Configuring AI Agent Governance permissions (Copilot Studio & Foundry)..." -ForegroundColor Cyan
    # AiEnterpriseInteraction.Read.All, Reports.Read.All, ExternalConnection.Read.All, AppCatalog.Read.All
    Write-Host " - Granting AI and interaction catalog read scopes"
    Write-Host " - Note: Ensure service principal '`$appName' (`$spId) is added as an Application User in target Power Platform environments"
"@
})
$(if ($hasAgent365) {
@"

    # -------------------------------------------------------------------------
    # Microsoft Agent 365 Catalog Permissions
    # -------------------------------------------------------------------------
    Write-Host "Configuring Microsoft Agent 365 catalog delegated scopes..." -ForegroundColor Cyan
    Write-Host " - Delegated scopes: CopilotPackages.Read.All, CopilotPackages.ReadWrite.All"
"@
})

    Write-Host "Granting admin consent for application..." -ForegroundColor Cyan
    & az ad app permission admin-consent --id `$appId 2>`$null | Out-Null

    Write-Host ''
    Write-Host '========================================================================' -ForegroundColor Cyan
    Write-Host 'Microsoft Entra ID Connector Setup Complete' -ForegroundColor Green
    Write-Host "Provide in SailPoint ISC Connection Settings:" -ForegroundColor Yellow
    Write-Host " Tenant ID: `$tenantId"
    Write-Host " Client ID: `$appId"
    Write-Host " Client Secret: `$secret"
    Write-Host '========================================================================' -ForegroundColor Cyan
} else {
    Write-Warning "Azure CLI (az) not found. Connect-MgGraph PowerShell SDK can be used alternatively."
}
"@

        }
        LegacyRun    = { [CmdletBinding(SupportsShouldProcess = $true)] param($ctx) Invoke-AzureLegacyFromContext -Context $ctx -WhatIf:$WhatIfPreference }
    }
}

Export-ModuleMember -Function Register-AzureConnectorDefinitions, Invoke-AzureLegacyFromContext