PowerOps.psm1

#requires -Modules Az.Accounts

function Register-PowerOpsAdminApplication {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [String]
        $ClientId
    )
    Invoke-PowerOpsRequest -Path "/providers/Microsoft.BusinessAppPlatform/adminApplications/$ClientId" -Method Put
}
function Invoke-PowerOpsRequest {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('Get', 'Post', 'Patch', 'Delete', 'Put')]
        [String]
        $Method,

        [Parameter(Mandatory = $false)]
        [Object]
        $RequestBody,

        [Parameter(Mandatory = $true)]
        $Path,

        [Parameter(Mandatory = $false)]
        [switch]$Force
    )

    begin {
        # Set base URI
        $BaseUri = "https://api.bap.microsoft.com"
        if (-not $PSBoundParameters['Force']) {
            $ApiVersion = if ($Path -notmatch '\?') { '?api-version=2021-07-01' } else { '&api-version=2021-07-01' }
        }
        else {
            $ApiVersion = $null
        }
        # Acquire token and set default headers
        try {
            $token = Get-AzAccessToken
        }
        catch {
            throw "Unable to acquire token"
        }
        $Headers = @{
            "Content-Type"  = "application/json; charset=utf-8"
            "Authorization" = "Bearer $($Token.Token)"
        }
    }
    process {
        $RestParameters = @{
            "Uri"         = "$($BaseUri)$($Path)$($ApiVersion)"
            "Method"      = $Method
            "Headers"     = $Headers
            "ContentType" = 'application/json; charset=utf-8'
        }
        if ($RequestBody) {
            $RestParameters["Body"] = $RequestBody
        }
        try {
            $Request = Invoke-RestMethod @RestParameters
            if ($Method -eq 'Get') {
                $Request.value
            }
            else {
                $Request
            }
        }
        catch {
            throw $_
        }
    }
    end {

    }
}
function New-PowerOpsEnvironment {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,
        [Parameter(Mandatory = $true)]
        [ValidateSet('unitedstates', 'europe', 'asia', 'australia', 'india',
            'japan', 'canada', 'unitedkingdom', 'unitedstatesfirstrelease',
            'southamerica', 'france', 'switzerland', 'germany', 'unitedarabemirates')]
        [String]$Location,
        [Parameter(Mandatory = $false)]
        [switch]$Force
    )
    begin {
        # Validate if environment with the same name already exists
        if (-not $PSBoundParameters['Force']) {
            $existingEnv = Invoke-PowerOpsRequest -Method Get -Path '/providers/Microsoft.BusinessAppPlatform/scopes/admin/environments' | Where-Object { $_.properties.displayName -eq $Name }
            if ($existingEnv) {
                throw "Environment with DisplayName '$Name' already exists in Power Platform. Retry command with the -Force switch if you really want to create the environment with the duplicate name"
            }
        }
    }
    process {
        # API payload
        # TODO - discuss what parameters that should be available for custioization to toggle settings/enable solutions for new environments
        $PostBody = @{
            "properties" = @{
                "linkedEnvironmentMetadata" = @{
                    "baseLanguage" = ''
                    "domainName"   = "$($Name)"
                    "templates"    = @("D365_DeveloperEdition")
                }
                "databaseType"              = "CommonDataService"
                "displayName"               = "$($Name)"
                "environmentSku"            = "Production"
            }
            "location"   = "$($Location)"
        }
        if ($PSCmdlet.ShouldProcess("Create environment $Name in $Location")) {
            try {
                Write-Verbose -Message "Creating environment $Name in $Location"
                Invoke-PowerOpsRequest -Method Post -Path '/providers/Microsoft.BusinessAppPlatform/environments?api-version=2019-05-01&ud=/providers/Microsoft.BusinessAppPlatform/scopes/admin/environments' -RequestBody ($PostBody | ConvertTo-Json -Depth 100) -Force
                # TODO - currently we don't resturn anything here unless there is an error with environment creation...
            }
            catch {
                Write-Error $_
            }
        }

    }
    end {

    }
    #>
}
function New-PowerOpsDLPPolicy {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,
        [Parameter(Mandatory = $false)]
        [switch]$Force,
        [Parameter(Mandatory = $false)]
        [string]$TemplateFile
    )
    begin {
        # Validate template
        $Template = Get-Content -Raw -Path $TemplateFile | ConvertFrom-Json -Depth 100
        # Validate if environment with the same name already exists
        if (-not $PSBoundParameters['Force']) {
            $existingPolicy = Invoke-PowerOpsRequest -Method Get -Path '/providers/PowerPlatform.Governance/v1/policies?$top=100' | Where-Object { $_.displayName -eq $Name }
            if ($existingPolicy) {
                throw "DLP Policy with DisplayName '$Name' already exists in Power Platform. Retry command with the -Force switch if you really want to create the policy with the duplicate name"
            }
        }
        # Update displayname in template from parameter
        $Template.displayName = $Name
    }
    process {
        # API payload
        try {
            if ($PSCmdlet.ShouldProcess("Create DLP Policy $Name")) {
                Write-Verbose -Message "Creating DLP Policy $Name"
                Invoke-PowerOpsRequest -Method Post -Path '/providers/PowerPlatform.Governance/v1/policies' -RequestBody ($template | ConvertTo-Json -Depth 100)
            }
        }
        catch {
            Write-Error $_
        }

    }
    end {

    }
    #>
}
function Get-PowerOpsTenantSettings {
    $tenantSettings = Invoke-PowerOpsRequest -Method Post -Path '/providers/Microsoft.BusinessAppPlatform/listTenantSettings'
    return $tenantSettings
}
function Invoke-PowerOpsPull {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $false)]
        [switch]$Force
    )
    #region tenant settings
    $tenantDetails = Get-AzTenant | Select-Object -First 1
    $tenantDomain = (((Invoke-AzRestMethod -Uri https://graph.microsoft.com/v1.0/domains).Content | ConvertFrom-Json).value).id
    $tenantSettings = Invoke-PowerOpsRequest -Method Post -Path '/providers/Microsoft.BusinessAppPlatform/listTenantSettings'
    $tenantSettingsFile = '{0}_{1}.json' -f $tenantDomain, $tenantDetails.Id

    $rootDirectory = "$($tenantDomain) ($($tenantDetails.Id))"
    if ($PSBoundParameters['Force']) {
        Remove-Item -Path $rootDirectory -Force -Recurse
    }
    $null = New-Item -ItemType Directory -Name $rootDirectory -Force
    $tenantsettings | ConvertTo-Json -Depth 100 | Out-File -Path $rootDirectory/$tenantSettingsFile -Force
    #endregion tenant settings

    #region get environments
    $environments = Invoke-PowerOpsRequest -Method Get -Path '/providers/Microsoft.BusinessAppPlatform/scopes/admin/environments'
    if ($environments) { $null = New-Item -ItemType Directory -Path $rootDirectory -Name 'environments' -Force }
    foreach ($environment in $environments) {
        $envPath = $rootDirectory + '/environments'
        $filePath = '{0}/{1}_{2}.json' -f $envPath, $environment.properties.displayName, $environment.name
        Write-Output -InputObject "Creating environment $filepath"
        $environment | ConvertTo-Json -Depth 100 | Out-File -Path $filePath -Force
    }
    #endregion get environments

    #region get environments
    $policies = Invoke-PowerOpsRequest -Method Get -Path '/providers/PowerPlatform.Governance/v1/policies'
    if ($policies) { $null = New-Item -ItemType Directory -Path $rootDirectory -Name 'policies' -Force }
    foreach ($policy in $policies) {
        $policyPath = $rootDirectory + '/policies'
        $filePath = '{0}/{1}_{2}.json' -f $policyPath, $policy.displayName, $policy.name
        Write-Output -InputObject "Creating policy $filepath"
        $policy | ConvertTo-Json -Depth 100 | jq 'del (.etag,.createdBy,.createdTime,.lastModifiedBy,.lastModifiedTime,.isLegacySchemaVersion)' | Out-File -Path $filePath -Force
    }
    #endregion get environments
}
function New-PowerOpsScope {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        $FilePath
    )
    $FilePath = Get-ChildItem -Path $FilePath
    $tenantDetails = Get-AzTenant | Select-Object -First 1
    $templateDetails = Get-Content -Path $FilePath.FullName | ConvertFrom-Json -Depth 100

    switch ($FilePath.FullName) {
        { $_.split('/')[-1] -match $tenantDetails.id } {
            [PSCustomObject]@{
                path   = '/providers/Microsoft.BusinessAppPlatform/scopes/admin/updateTenantSettings'
                method = 'Post'
            }
        }
        { $_ -match 'policies' } {
            [PSCustomObject]@{
                path   = '/providers/PowerPlatform.Governance/v1/policies/{0}' -f $templateDetails.Name
                method = 'Patch'
            }
        }
        { $_ -match 'environments' } {
            [PSCustomObject]@{
                path   = '/providers/Microsoft.BusinessAppPlatform/scopes/admin/environments/{0}' -f $templateDetails.Name
                method = 'Patch'
            }
        }
    }
}
function Invoke-PowerOpsPush {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        $ChangeSet
    )
    $deleteSet = @()
    $addModifySet = foreach ($change in $ChangeSet) {
        $operation, $filename = ($change -split "`t")[0, -1]
        if ($operation -eq 'D') {
            $deleteSet += $filename
            continue
        }
        if ($operation -in 'A', 'M', 'R' -or $operation -match '^R0[0-9][0-9]$') { $filename }
    }

    foreach ($addModify in $addModifySet) {
        Write-Output -InputObject "Adding or modifying $addModify"
        New-PowerOpsDeployment -FilePath $addModify
    }
    foreach ($deletion in $deleteSet) {
        Write-Output -InputObject "Removing $deletion"
    }
}
function New-PowerOpsDeployment {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        $FilePath
    )
    $deploymentType = New-PowerOpsScope -FilePath $FilePath
    Write-Output -InputObject "Attempting to deploy $filePath to $($deploymentType.Path)"
    Invoke-PowerOpsRequest -Path $deploymentType.Path -Method $deploymentType.Method -RequestBody (Get-Content -Path $FilePath)
}
function Get-PowerOpsTenantCapacityStatus {
    $TenantId = (Get-AzContext).Tenant.Id
    $AccessTokenLicense = Get-AzAccessToken -ResourceUrl 'https://licensing.powerplatform.microsoft.com'
    $Headers = @{
        "Content-Type"  = "application/json; charset=utf-8"
        "Authorization" = "Bearer $($AccessTokenLicense.Token)"
    }
    Invoke-RestMethod -Headers $Headers -Uri https://licensing.powerplatform.microsoft.com/v0.1-alpha/tenants/$TenantId/TenantCapacity -Method GET -ContentType 'application/json; charset=utf-8'
}
function Get-PowerOpsAADPermissions {
    # TODO - add support for service principals
    $roleDefinitions = ((Invoke-AzRestMethod -Uri https://graph.microsoft.com/v1.0/directoryRoleTemplates).Content  | ConvertFrom-Json -Depth 100).value
    $currentUser = (Invoke-AzRestMethod -Uri https://graph.microsoft.com/v1.0/me).Content | ConvertFrom-Json
    $graphQuery = "?`$filter=principalId eq '{0}'" -f $currentUser.id
    $AADRoleAssignments = (Invoke-AzRestMethod -Uri "https://graph.microsoft.com/beta/roleManagement/directory/roleAssignments$($graphQuery)").Content | ConvertFrom-Json -Depth 100
    foreach ($role in $AADRoleAssignments.value) {
        $roleDetails = $roleDefinitions | Where-Object { $_.id -eq $role.roleDefinitionId }
        $roleDetails
    }
}


<#region validate permissions and capacity
$requiredPermission = '62e90394-69f5-4237-9190-012177145e10', '11648597-926c-4cf3-9c36-bcebb0ba8dcc'
$AADPermissions = Get-PowerOpsAADPermissions

foreach ($permission in $requiredPermission) {
    if ($permission -in $AADPermissions.id) {
        Write-Verbose -Message "Roleassignment id ($permission) found "
        $permissionOk = $true
        return
    }
}
if (-not $permissionOk) {
    throw "Principal doesn't have enough permissions (Global Administrator or Power Platform Administrator) to execute the script."
}

$tenantcapacity = Get-PowerOpsTenantCapacityStatus
if ($tenantcapacity.capacitySummary.status -ne 'Available') {
    throw 'Not enough capacity in Power platform.'
}
#endregion validate permissions and capacity#>