public/Get-MsecBusinessCentralEnvironment.ps1
|
function Get-MsecBusinessCentralEnvironment { <# .SYNOPSIS Dynamics 365 Business Central environments - production and sandbox - with how many companies and users each one carries. .DESCRIPTION A SANDBOX IS NOT A LOWER-SENSITIVITY ENVIRONMENT, which is the reason this command exists. A Business Central sandbox is routinely created by copying production, so it can hold the same financial data, and it carries its OWN user list and its OWN permission assignments - granted separately, reviewed separately, and usually reviewed less. An access review that covers production and stops there has covered a fraction of the estate. Measured on one tenant: two production environments and three sandboxes, with the sandboxes holding 33, 37 and 45 users. THE COMMAND DOES NOT CLAIM TO DETECT A PRODUCTION COPY. The admin API exposes no field saying where a sandbox came from, so inferring it from a name containing 'COPY' would be a guess dressed as a measurement. Type, company count and user count are reported and the judgement is left to the reader - a sandbox with as many companies as production is worth a question whatever it is called. COUNTS COST A REQUEST EACH, so -Quick skips them when the environment list alone is wanted. A count that could not be read is $null rather than 0: an environment nobody could enumerate must not render as an empty one. Runs as the signed-in user - see the note below. .PARAMETER Quick Environment list only. Skips the per-environment company and user counts. .PARAMETER SandboxOnly Only non-production environments. .EXAMPLE Get-MsecBusinessCentralEnvironment Every environment with its company and user counts. .EXAMPLE Get-MsecBusinessCentralEnvironment -SandboxOnly The environments most likely to hold production data under least scrutiny. .EXAMPLE Get-MsecBusinessCentralEnvironment | Where-Object { $_.Type -ne 'Production' -and $_.Users -gt 20 } Non-production environments with a real user population behind them. .OUTPUTS PSCustomObject per environment, PSTypeName 'MsecBusinessCentralEnvironment'. .NOTES RUNS AS THE SIGNED-IN USER, NOT AS THE msec APP. Business Central offers only API.ReadWrite.All and Automation.ReadWrite.All as application permissions - there is no read-only variant - so granting one to the msec app would let the Key Vault certificate write to a finance system. A delegated token carries your own Business Central permissions and grants nothing new. Same pattern as Get-MsecPowerPlatformEnvironment. Needs Connect-AzAccount, not Connect-Msec. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [switch] $Quick, [switch] $SandboxOnly ) $ctx = Get-MsecBusinessCentralToken $res = $ctx.Resource $headers = $ctx.Headers try { $environments = @((Invoke-RestMethod -Headers $headers -ErrorAction Stop -Method Get ` -Uri "$res/admin/v2.24/applications/businesscentral/environments").value) } catch { $status = if ($_.Exception.Response) { [int] $_.Exception.Response.StatusCode } else { $null } if ($status -in 401, 403) { throw "Business Central admin API returned $status. This command runs as the signed-in user and needs a Business Central or Dynamics 365 administrator role. Original: $($_.Exception.Message)" } throw "Could not list Business Central environments: $($_.Exception.Message)" } foreach ($e in $environments) { $name = [string] $e.name if ($SandboxOnly -and [string]$e.type -eq 'Production') { continue } # $null, not 0, when a count could not be taken - an environment that refused # enumeration must not read as an empty one. $companies = $null $users = $null if (-not $Quick) { $companyList = $null try { $companyList = @((Invoke-RestMethod -Headers $headers -ErrorAction Stop -Method Get ` -Uri "$res/v2.0/$name/api/v2.0/companies").value) $companies = $companyList.Count } catch { Write-Verbose "Could not list companies in '$name': $($_.Exception.Message)" } if ($companyList -and $companyList.Count) { # The users list is per-environment, not per-company - the route needs a company # id but the result spans the environment, so one call answers it. try { $users = @((Invoke-RestMethod -Headers $headers -ErrorAction Stop -Method Get ` -Uri "$res/v2.0/$name/api/microsoft/automation/v2.0/companies($($companyList[0].id))/usersPermissions").value).Count } catch { Write-Verbose "Could not read users in '$name': $($_.Exception.Message)" } } } [PSCustomObject]@{ PSTypeName = 'MsecBusinessCentralEnvironment' Name = $name FriendlyName = [string] $e.friendlyName # Production or Sandbox. NOT a statement about what data is inside it. Type = [string] $e.type Status = [string] $e.status CountryCode = [string] $e.countryCode Companies = $companies Users = $users ApplicationVersion = [string] $e.applicationVersion PlatformVersion = [string] $e.platformVersion LocationName = [string] $e.locationName GeoName = [string] $e.geoName RingName = [string] $e.ringName WebClientUrl = [string] $e.webClientLoginUrl LinkedPowerPlatformEnvironmentId = [string] $e.linkedPowerPlatformEnvironmentId } } } |