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" } "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-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" } "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 Remove-PowerOpsEnvironment { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$Name, [Parameter(Mandatory = $false)] [switch]$Force ) begin { # Validate if environment exists $existingEnv = Invoke-PowerOpsRequest -Method Get -Path '/providers/Microsoft.BusinessAppPlatform/scopes/admin/environments' | Where-Object { $_.properties.displayName -eq $Name } if (-not $existingEnv) { throw "Environment with DisplayName '$Name' doesn't exists in Power Platform. Nothing to remove" } } process { if ($PSCmdlet.ShouldProcess("Delete environment $Name")) { try { $restCall = @{ path = '/providers/Microsoft.BusinessAppPlatform/scopes/admin/environments/{0}' -f $existingEnv.Name method = 'Delete' } Write-Verbose -Message "Delete environment $Name" Invoke-PowerOpsRequest @restCall } 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 } } #tenant isolation policy # /providers/PowerPlatform.Governance/v1/tenants/5663f39e-feb1-4303-a1f9-cf20b702de61/tenantIsolationPolicy <#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#> |