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') {
                if ($Request.value) {
                    return $Request.value
                }
                if ($Request.Properties) {
                    return $Request.Properties
                }
            }
            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)]
        [bool]$Dataverse = $false,
        [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 {
        # 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"    = @()
                }
                "displayName"               = "$($Name)"
                "environmentSku"            = "Production"
                "databaseType"              = "None"
            }
            "location"   = "$($Location)"
        }
        if ($true -eq $Dataverse) {
            $PostBody.properties.databaseType = "CommonDataService"
        }
        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
            }
            catch {
                Write-Error $_
            }
        }

    }
    end {

    }
}
function New-PowerOpsRoleAssignment {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        [string]$PrincipalId,
        [Parameter(Mandatory = $false)]
        [ValidateSet('EnvironmentAdmin', 'EnvironmentMaker')]
        [String]$RoleDefinition,
        [Parameter(Mandatory = $false)]
        [bool]$Dataverse = $false,
        [Parameter(Mandatory = $false)]
        [String]$EnvironmentName
    )
    begin {
        # Validate if environment with the name exists
        $environment = Get-PowerOpsEnvironment | Where-Object { $_.properties.displayName -eq $EnvironmentName }
        if (-not $environment) {
            throw "Environment with DisplayName '$EnvironmentName' doesn't exist."
        }
    }

    process {
        $requestBody = @{
            name       = (New-Guid).Guid
            properties = @{
                roledefinition = @{
                    id = "$($environment.id)/roleDefinitions/$RoleDefinition"
                }
                principal      = @{
                    id = "$PrincipalId"
                }
            }
        }
        if ($PSCmdlet.ShouldProcess("Create roleAssignment $roleDefinition for $($environment.id)")) {
            try {
                $roleAssignmentPayload = @{
                    Method      = 'Post'
                    Path        = '{0}/roleAssignments' -f $environment.id
                    RequestBody = ($requestBody | ConvertTo-Json -Depth 100)
                }
                Write-Verbose -Message "Create roleAssignment $roleDefinition for $($environment.id)"
                Invoke-PowerOpsRequest @roleAssignmentPayload
            }
            catch {
                Write-Error $_
            }
        }
    }
    end {

    }
}
function Remove-PowerOpsEnvironment {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Name')]
        [ValidateNotNullOrEmpty()]
        [string]$EnvironmentName,
        [Parameter(Mandatory = $true, ParameterSetName = 'Id')]
        [ValidateNotNullOrEmpty()]
        [string]$EnvironmentId,
        [Parameter(Mandatory = $false)]
        [switch]$Force
    )
    begin {
        # Validate if environment exists
        if ($EnvironmentName) {
            $existingEnv = Invoke-PowerOpsRequest -Method Get -Path '/providers/Microsoft.BusinessAppPlatform/scopes/admin/environments' | Where-Object { $_.properties.displayName -eq $EnvironmentName }
            if (-not $existingEnv) {
                throw "Environment with DisplayName '$Name' doesn't exists in Power Platform. Nothing to remove"
            }
            $environmentId = $existingEnv.name
        }
        Write-Verbose -Message "EnvironmentId is $EnvironmentId"
        $validateUri = '/providers/Microsoft.BusinessAppPlatform/scopes/admin/environments/{0}/validateDelete?api-version=2018-01-01' -f $environmentId
        $validateDelete = Invoke-PowerOpsRequest -Method Post -Path $validateUri -Force
        Write-Verbose "CanInitiateDelete: $($validateDelete.canInitiateDelete)"
    }
    process {
        if ($validateDelete.canInitiateDelete) {
            if ($PSCmdlet.ShouldProcess("Delete environment $EnvironmentId")) {
                try {
                    $restCall = @{
                        path   = '/providers/Microsoft.BusinessAppPlatform/scopes/admin/environments/{0}?api-version=2018-01-01' -f $existingEnv.Name
                        method = 'Delete'
                    }
                    Write-Verbose -Message "Deleting environment $EnvironmentId"
                    Invoke-PowerOpsRequest @restCall -Force
                    Write-Verbose -Message "Successfully deleted environment $EnvironmentId"
                }
                catch {
                    Write-Error $_
                }
            }
        }
        else {
            Write-Error "Cannot initiate delete"
            return $validateDelete.errors
        }
    }
}
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 Get-PowerOpsEnvironment {
    $existingEnv = Invoke-PowerOpsRequest -Method Get -Path '/providers/Microsoft.BusinessAppPlatform/scopes/admin/environments'
    return $existingEnv
}

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
    }
}
function Set-PowerOpsTenantIsolation {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        [bool]$Enabled,
        [Parameter(Mandatory = $false)]
        [ValidateSet('InboundAndOutbound', 'Inbound', 'Outbound')]
        [string]$AllowedDirection,
        [Parameter(Mandatory = $false)]
        [string]$TenantId
    )
    $Path = "/providers/PowerPlatform.Governance/v1/tenants/{0}/tenantIsolationPolicy" -f (Get-AzTenant).Id
    Write-Verbose -Message "API Path: $Path"
    # Validate tenantId
    if ($TenantId -match '.' -and $TenantId -ne "*") {
        $wellKnown = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$TenantId/.well-known/openid-configuration"
        if (-not $wellKnown.issuer) {
            throw "Tenant $TenantId does not exist in public Azure "
        }
        else {
            $TenantId = $wellknown.issuer.Split("/")[-2]
        }
    }
    Write-Verbose -Message "TenantId: $TenantId"

    # Get existing isolation settings
    $existingSettings = Invoke-PowerOpsRequest -Method Get -Path $Path

    # Update settings
    $newSettings = [PSCustomObject]@{ properties = $existingSettings }
    $newSettings.properties.isDisabled = ($Enabled -eq $false)
    Write-Verbose -Message "Tenant Isolation isDisabled will be set to $($newSettings.properties.isDisabled)"
    if ($TenantId -and $AllowedDirection) {
        # Check if rule for tenant already exists and update accordingly
        $existingRule = $newSettings.properties.allowedTenants | Where-Object { $_.TenantId -eq $TenantId }
        if ($existingRule.tenantId -eq $TenantId) {
            # Update existing tenant rule
            Write-Verbose -Message "Tenant rule for $TenantId already exist"
            $existingRule.direction.inbound = $AllowedDirection -in 'Inbound', 'InboundAndOutBound'
            $existingRule.direction.outbound = $AllowedDirection -in 'Outbound', 'InboundAndOutBound'
        }
        else {
            # Create new rule for tenant
            $newSettings.properties.allowedTenants += [PSCustomObject]@{
                tenantId  = $TenantId
                direction = [PSCustomObject]@{
                    inbound  = $AllowedDirection -in 'Inbound', 'InboundAndOutBound'
                    outbound = $AllowedDirection -in 'Outbound', 'InboundAndOutBound'
                }
            }
        }

    }
    Write-Verbose -Message "Tenant rule for $TenantId will be configured in the direction $AllowedDirection"

    if ($PSCmdlet.ShouldProcess("Update tenant isolation settings to Enabled=$Enabled")) {
        Invoke-PowerOpsRequest -Method Put -Path $Path -RequestBody ($newSettings | ConvertTo-Json -Depth 100)
    }
}