modules/Definitions/Google.Connector.psm1

Set-StrictMode -Version Latest

function Get-GoogleContextFeatureNames {
    param($Value)
    $items = [System.Collections.Generic.List[string]]::new()
    if ($null -eq $Value) { return ,$items.ToArray() }
    foreach ($entry in @($Value)) {
        foreach ($part in ([string]$entry -split ',')) {
            $name = $part.Trim()
            if ($name) { $items.Add($name) }
        }
    }
    return ,$items.ToArray()
}

function Get-GoogleLegacyArguments {
    param(
        [Parameter(Mandatory)][ValidateSet('Saas', 'Ciem')]
        [string]$Mode,
        [Parameter(Mandatory)][hashtable]$Context
    )
    # Context is a hashtable of optional keys: indexer access (see Aws.Connector).
    # CIEM is the Workspace connector with the Gcp and Ciem packs. Mode selects those
    # packs; a SaaS run leaves Feature untouched so the operator can still choose none.
    $feature = $Context['Feature']
    if ($Mode -eq 'Ciem') {
        $names = [System.Collections.Generic.List[string]]::new()
        foreach ($required in @('Gcp', 'Ciem')) { $names.Add($required) }
        $extras = Get-GoogleContextFeatureNames $Context['Feature']
        foreach ($extra in $extras) {
            if (-not $names.Contains([string]$extra)) { $names.Add([string]$extra) }
        }
        $feature = $names.ToArray()
    }
    $parameters = @{
        GrantType       = if ($Context['GrantType']) { $Context['GrantType'] } else { 'ServiceAccount' }
        NonInteractive  = [bool]$Context['NonInteractive']
        RotateKey       = [bool]$Context['RotateKey']
    }
    foreach ($name in @('OrganizationId', 'ProjectId', 'ServiceAccountId', 'DisplayName', 'ImpersonateUser', 'OutputDirectory', 'Access')) {
        if (-not [string]::IsNullOrWhiteSpace([string]$Context[$name])) { $parameters[$name] = $Context[$name] }
    }
    if ($null -ne $feature) { $parameters.Feature = $feature }
    return $parameters
}

function Invoke-GoogleLegacyFromContext {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory)][ValidateSet('Saas', 'Ciem')]
        [string]$Mode,
        [Parameter(Mandatory)][hashtable]$Context
    )
    $impl = Join-Path $PSScriptRoot '../Legacy/GoogleCloud.Implementation.ps1'
    $splat = Get-GoogleLegacyArguments -Mode $Mode -Context $Context
    if ($Context['Access'] -eq 'ReadOnly') { $splat.AggregationOnly = $true }
    & $impl @splat -WhatIf:$WhatIfPreference
}

function Register-GoogleConnectorDefinitions {
    Register-IntegrationWizardDefinition @{
        Id           = 'google-saas-connector'
        Platform     = 'GoogleCloud'
        Product      = 'ISC'
        CoreSwitch   = 'SaaSConnector'
        CliName      = 'gcloud'
        Title        = 'Google Workspace connector'
        FolderName   = 'Google-Workspace'
        AccessLevels = @('ReadOnly', 'ReadWrite')
        AdminRoles   = @('Organization admin or project owner for IAM and service accounts')
        OutputsHelp  = 'Paste service account email, key, and admin email into ISC Connection Settings.'
        Inputs       = @(
            @{ Name = 'OrganizationId'; Source = 'Discover'; Prompt = 'Organization ID'; Purpose = 'Google Cloud organization'; Required = $true }
            @{ Name = 'ProjectId'; Source = 'Discover'; Prompt = 'Project ID'; Purpose = 'Host project'; Required = $true }
            @{ Name = 'ImpersonateUser'; Source = 'Supply'; Prompt = 'Admin user to impersonate'; Purpose = 'Workspace domain-wide delegation'; Required = $true }
            @{ Name = 'OutputDirectory'; Source = 'Supply'; Prompt = 'Output directory'; Purpose = 'Keys and connection settings'; Required = $false }
        )
        Steps        = { param($ctx) @() }
        BuildScript  = {
            param($c)
            $orgId = if ($c['OrganizationId']) { [string]$c['OrganizationId'] } else { '' }
            $projId = if ($c['ProjectId']) { [string]$c['ProjectId'] } else { 'YOUR_PROJECT_ID' }
            $user = if ($c['ImpersonateUser']) { [string]$c['ImpersonateUser'] } else { 'admin@yourdomain.com' }
            $access = if ($c['Access']) { [string]$c['Access'] } else { 'ReadOnly' }
            $saId = if ($c['ServiceAccountId']) { [string]$c['ServiceAccountId'] } else { 'sailpoint-isc-gws' }
            $saEmail = "$saId@$projId.iam.gserviceaccount.com"
            $displayName = if ($c['DisplayName']) { [string]$c['DisplayName'] } else { 'SailPoint ISC Google Workspace' }

            $scopes = if ($access -eq 'ReadOnly') {
                @('https://www.googleapis.com/auth/admin.directory.group.readonly', 'https://www.googleapis.com/auth/admin.directory.user.readonly', 'https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly')
            } else {
                @('https://www.googleapis.com/auth/admin.directory.group', 'https://www.googleapis.com/auth/admin.directory.user', 'https://www.googleapis.com/auth/apps.groups.settings', 'https://www.googleapis.com/auth/admin.directory.rolemanagement', 'https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly')
            }
            $scopeCsv = $scopes -join ','

            @"
# -----------------------------------------------------------------------------
# Google Workspace SaaS Connector Setup
# -----------------------------------------------------------------------------
`$projectId = '$projId'
`$serviceAccountId = '$saId'
`$serviceAccountEmail = '$saEmail'
`$displayName = '$displayName'
`$impersonateUser = '$user'
`$scopes = '$scopeCsv'
`$outputDir = if (`$OutputDirectory) { `$OutputDirectory } else { `$PSScriptRoot }

Write-Host "Configuring Google Cloud project: `$projectId" -ForegroundColor Cyan
& gcloud config set project `$projectId --quiet

Write-Host "Enabling Google Workspace APIs..." -ForegroundColor Cyan
& gcloud services enable admin.googleapis.com groupssettings.googleapis.com --project=`$projectId --quiet

Write-Host "Ensuring service account `$serviceAccountEmail..." -ForegroundColor Cyan
`$existingSa = & gcloud iam service-accounts list --project=`$projectId --filter="email=`$serviceAccountEmail" --format="value(email)" 2>`$null
if (-not `$existingSa) {
    Write-Host "Creating service account `$serviceAccountId..."
    & gcloud iam service-accounts create `$serviceAccountId --display-name="`$displayName" --description="SailPoint ISC Google Workspace SaaS connector" --project=`$projectId --quiet
} else {
    Write-Host "Service account `$serviceAccountEmail already exists."
}

`$uniqueId = (& gcloud iam service-accounts describe `$serviceAccountEmail --project=`$projectId --format="value(uniqueId)" 2>`$null).Trim()
Write-Host "Service Account Client ID: `$uniqueId" -ForegroundColor Green

# Generate service account key and convert to RSA PEM
`$keyPath = Join-Path `$outputDir 'sailpoint-gws-key.json'
`$pemPath = Join-Path `$outputDir 'sailpoint-gws-rsa.pem'
Write-Host "Creating service account key at `$keyPath..." -ForegroundColor Cyan
& gcloud iam service-accounts keys create `$keyPath --iam-account=`$serviceAccountEmail --project=`$projectId --quiet

if (Get-Command openssl -ErrorAction SilentlyContinue) {
    Write-Host "Converting JSON private key to traditional encrypted RSA PEM..." -ForegroundColor Cyan
    `$keyJson = Get-Content -LiteralPath `$keyPath -Raw | ConvertFrom-Json
    `$pkcs8Path = Join-Path `$outputDir 'sailpoint-gws-temp.pkcs8'
    [System.IO.File]::WriteAllText(`$pkcs8Path, (`$keyJson.private_key -replace '\\n', "`n").Trim() + "`n")
    
    # Generate random passphrase
    `$passBytes = New-Object byte[] 24
    `$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
    `$rng.GetBytes(`$passBytes)
    `$rng.Dispose()
    `$keyPassword = [Convert]::ToBase64String(`$passBytes)
    `$env:SP_GWS_PEM_PASS = `$keyPassword

    try {
        & openssl rsa -aes-256-cbc -in `$pkcs8Path -out `$pemPath -passout env:SP_GWS_PEM_PASS
        Write-Host "Generated RSA PEM at `$pemPath" -ForegroundColor Green
        Write-Host "Private Key Password: `$keyPassword" -ForegroundColor Yellow
    } finally {
        Remove-Item Env:SP_GWS_PEM_PASS -ErrorAction SilentlyContinue
        Remove-Item -LiteralPath `$pkcs8Path -Force -ErrorAction SilentlyContinue
    }
}

Write-Host ''
Write-Host '========================================================================' -ForegroundColor Cyan
Write-Host 'Google Workspace Admin Console Action Required (Domain-Wide Delegation):' -ForegroundColor Yellow
Write-Host "1. Go to: https://admin.google.com -> Security -> Access and data control -> API controls"
Write-Host "2. Under 'Domain-wide delegation', click 'Manage Domain Wide Delegation' -> 'Add new'"
Write-Host "3. Client ID: `$uniqueId"
Write-Host "4. OAuth Scopes: `$scopes"
Write-Host "========================================================================" -ForegroundColor Cyan
"@

        }
        LegacyRun    = { [CmdletBinding(SupportsShouldProcess = $true)] param($ctx) Invoke-GoogleLegacyFromContext -Mode 'Saas' -Context $ctx -WhatIf:$WhatIfPreference }
    }
    Register-IntegrationWizardDefinition @{
        Id           = 'google-ciem-connector'
        Platform     = 'GoogleCloud'
        Product      = 'ISC'
        CoreSwitch   = 'CiemConnector'
        CliName      = 'gcloud'
        Title        = 'GCP CIEM connector feature packs'
        FolderName   = 'GCP-CIEM'
        AccessLevels = @('ReadOnly', 'ReadWrite')
        AdminRoles   = @('Organization admin for custom org roles')
        OutputsHelp  = 'Enable CIEM/NHI packs in ISC using the GCP org role from connection settings.'
        Inputs       = @(
            @{ Name = 'OrganizationId'; Source = 'Discover'; Prompt = 'Organization ID'; Purpose = 'GCP organization'; Required = $true }
            @{ Name = 'ProjectId'; Source = 'Discover'; Prompt = 'Project ID'; Purpose = 'Host project'; Required = $true }
            @{ Name = 'ImpersonateUser'; Source = 'Supply'; Prompt = 'Admin user to impersonate'; Purpose = 'Workspace domain-wide delegation'; Required = $true }
            @{ Name = 'Feature'; Source = 'Supply'; Prompt = 'Extra feature packs (comma-separated)'; Purpose = 'Optional packs merged with Gcp and Ciem'; Required = $false }
            @{ Name = 'OutputDirectory'; Source = 'Supply'; Prompt = 'Output directory'; Purpose = 'Artifacts'; Required = $false }
        )
        Steps        = { param($ctx) @() }
        BuildScript  = {
            param($c)
            $orgId = if ($c['OrganizationId']) { [string]$c['OrganizationId'] } else { 'YOUR_ORG_ID' }
            $projId = if ($c['ProjectId']) { [string]$c['ProjectId'] } else { 'YOUR_PROJECT_ID' }
            $user = if ($c['ImpersonateUser']) { [string]$c['ImpersonateUser'] } else { 'admin@yourdomain.com' }
            $access = if ($c['Access']) { [string]$c['Access'] } else { 'ReadOnly' }
            $saId = if ($c['ServiceAccountId']) { [string]$c['ServiceAccountId'] } else { 'sailpoint-isc-gws' }
            $saEmail = "$saId@$projId.iam.gserviceaccount.com"

            $features = @('Gcp', 'Ciem')
            if ($c['Feature']) {
                foreach ($f in ($c['Feature'] -split ',')) {
                    $trim = $f.Trim()
                    if ($trim -and -not ($features -contains $trim)) { $features += $trim }
                }
            }

            $perms = @(
                'cloudasset.assets.searchAllIamPolicies',
                'cloudasset.assets.searchAllResources',
                'iam.roles.get',
                'iam.roles.list',
                'iam.serviceAccounts.get',
                'iam.serviceAccounts.getIamPolicy',
                'iam.serviceAccounts.list',
                'logging.logEntries.list',
                'resourcemanager.folders.getIamPolicy',
                'resourcemanager.folders.list',
                'resourcemanager.organizations.get',
                'resourcemanager.organizations.getIamPolicy',
                'resourcemanager.projects.get',
                'resourcemanager.projects.getIamPolicy',
                'resourcemanager.projects.list'
            )
            if ($access -ne 'ReadOnly') {
                $perms += @(
                    'iam.roles.create',
                    'iam.roles.delete',
                    'iam.roles.update',
                    'iam.serviceAccounts.create',
                    'iam.serviceAccounts.setIamPolicy',
                    'resourcemanager.folders.setIamPolicy',
                    'resourcemanager.organizations.setIamPolicy',
                    'resourcemanager.projects.setIamPolicy'
                )
            }
            if ($features -contains 'AgentDiscovery') {
                $perms += @('aiplatform.agents.get', 'aiplatform.agents.list')
            }
            $permYamlList = ($perms | ForEach-Object { "- $_" }) -join "`n"

            @"
# -----------------------------------------------------------------------------
# Google Cloud CIEM Connector Setup
# -----------------------------------------------------------------------------
`$orgId = '$orgId'
`$projectId = '$projId'
`$serviceAccountEmail = '$saEmail'
`$impersonateUser = '$user'
`$customRoleTitle = 'SailPoint Google Workspace CIEM'
`$customRoleId = 'sailpointGoogleWorkspace'

Write-Host "Configuring Google Cloud project: `$projectId" -ForegroundColor Cyan
& gcloud config set project `$projectId --quiet

Write-Host "Enabling GCP & CIEM APIs..." -ForegroundColor Cyan
& gcloud services enable iam.googleapis.com cloudresourcemanager.googleapis.com cloudasset.googleapis.com logging.googleapis.com --project=`$projectId --quiet

# Create or update organization custom role
`$roleYaml = @(
    "title: `$customRoleTitle"
    "description: SailPoint Identity Security Cloud GCP CIEM custom role"
    "stage: GA"
    "includedPermissions:"
    "$permYamlList"
) -join "`n"

`$tempRoleFile = [System.IO.Path]::GetTempFileName()
Set-Content -Path `$tempRoleFile -Value `$roleYaml -Encoding utf8

try {
    Write-Host "Checking organization custom role `$customRoleId..." -ForegroundColor Cyan
    `$roleExists = & gcloud iam roles describe `$customRoleId --organization=`$orgId 2>`$null
    if (`$LASTEXITCODE -eq 0) {
        Write-Host "Updating custom role `$customRoleId at organization `$orgId..."
        & gcloud iam roles update `$customRoleId --organization=`$orgId --file=`$tempRoleFile --quiet
    } else {
        Write-Host "Creating custom role `$customRoleId at organization `$orgId..."
        & gcloud iam roles create `$customRoleId --organization=`$orgId --file=`$tempRoleFile --quiet
    }
} finally {
    Remove-Item -LiteralPath `$tempRoleFile -Force -ErrorAction SilentlyContinue
}

Write-Host "Binding custom role to service account and delegated admin user..." -ForegroundColor Cyan
& gcloud organizations add-iam-policy-binding `$orgId --member="serviceAccount:`$serviceAccountEmail" --role="organizations/`$orgId/roles/`$customRoleId" --quiet
& gcloud organizations add-iam-policy-binding `$orgId --member="user:`$impersonateUser" --role="organizations/`$orgId/roles/`$customRoleId" --quiet

Write-Host "CIEM organization IAM bindings applied successfully." -ForegroundColor Green
"@

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

Export-ModuleMember -Function Register-GoogleConnectorDefinitions, Invoke-GoogleLegacyFromContext