PowerBIManager.psm1

#region Exported Core Functions

function Connect-PBI{
    [CmdletBinding(DefaultParameterSetName = 'default')]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'credential')]
        [System.Management.Automation.CredentialAttribute()]
        [PSCredential] $Credential,
        [Parameter(Mandatory = $true)] [string] $ClientId
    )

    if (!$Script:AuthenticationContext)
    {
        Write-Verbose -Message 'Initialize AuthenticationContext'
        $AuthorityAddress = "https://login.microsoftonline.com/common/oauth2/authorize"
        $script:AuthenticationContext = New-Object -TypeName Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext -ArgumentList ($AuthorityAddress)
    }

    Write-Verbose -Message 'Acquire Access Token'
    $TargetResourceIdentifier = "https://analysis.windows.net/powerbi/api"
    $AuthorizationRedirectUri = "https://login.live.com/oauth20_desktop.srf"

    if ($PSCmdlet.ParameterSetName -eq 'credential')
    {
        Write-Verbose -Message 'Using credential parameter'
        $UserCredential = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.UserPasswordCredential($Credential.UserName, $Credential.Password)
        
        $AuthenicationResult = [Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContextIntegratedAuthExtensions]::AcquireTokenAsync($script:AuthenticationContext
        , $TargetResourceIdentifier
        , $ClientId
        , $UserCredential).Result
    }
    else
    {
        Write-Verbose -Message 'Microsoftonline login'
        $ADPlatformParams= New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters([Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior]::Auto)
        $AuthenicationResult =  $script:AuthenticationContext.AcquireTokenAsync($TargetResourceIdentifier
            , $ClientId, [Uri] $AuthorizationRedirectUri
            , $ADPlatformParams).Result
    }


    if(!$AuthenicationResult)
    {
         Write-Warning -Message "NOT Authenticated"
    }
    else
    {

        Write-Information "Authenticated as $($AuthenicationResult.UserInfo.DisplayableId)"
        $script:TokenExpiresOn = $AuthenicationResult.ExpiresOn
        $script:AccessToken = $AuthenicationResult.AccessToken
        $script:AuthenticatedUser = $AuthenicationResult.UserInfo.DisplayableId
    }
    
}

function Invoke-PBIMethod{
    [CmdletBinding(DefaultParameterSetName = 'default')]
    param(
         [Parameter(Mandatory=$true)] [string] $ApiOperation,        
        [Parameter(Mandatory=$false)] [string] $RequestMethod="GET",
        [Parameter(Mandatory=$false)] [object] $RequestBody,        
        [Parameter(Mandatory=$false)] [switch] $IgnoreGroup = $false 
    )
    $requestUrl = "https://api.powerbi.com/v1.0/myorg/"
    if (!$IgnoreGroup){
        if(![string]::IsNullOrWhiteSpace($script:GroupId))
        {
            $requestUrl =  "$($requestUrl)groups/$($script:GroupId)/"
        }
    }
    $requestUrl = $requestUrl + $ApiOperation

    $requestHeaders = Get-PBIRequestHeader

    Write-Verbose "Invoke $($RequestMethod) $($requestUrl)"
    if ($RequestBody)
    {
        $jsonBody = ConvertTo-Json $RequestBody -Depth 10
        Write-Verbose -Message "Body $($jsonBody)"
        $result=Invoke-RestMethod -Uri $requestUrl  -Headers $requestHeaders -Method $RequestMethod -Body $jsonBody
    }
    else
    {
        $result=Invoke-RestMethod -Uri $requestUrl  -Headers $requestHeaders -Method $RequestMethod
    }
   
   return $result
   
}

function Use-PBIGroup{
    [CmdletBinding(DefaultParameterSetName = 'default')]
    param(
        [Parameter(Mandatory=$false)] [string] $Name,
        [Parameter(Mandatory=$false)] [string] $Id,        
        [Parameter(Mandatory=$false)] [switch] $Clear = $false 
    )
    if (![string]::IsNullOrWhiteSpace($Id))
    {
        $script:GroupId=$Id
        Write-Information "Using Group $($script:GroupId)"
    }
    elseif (![string]::IsNullOrWhiteSpace($Name))
    {
        Write-Verbose -Message "Get Group Id for $($Name)"
        $grps = Invoke-PBIMethod -ApiOperation groups -IgnoreGroup
        $groupId = $grps.value |Where-Object name -eq $Name | Select-Object -ExpandProperty id
        if ([string]::IsNullOrWhiteSpace($groupId))
        {
             Write-Warning "Could not find group $($Name)" 
        }
        else
        {
           $script:GroupId=$groupId 
           Write-Information "Using Group $($Name) ($($script:GroupId))"
        }
    }
    elseif ($Clear)
    {
         $script:GroupId=$null
    }
    else
    {
         Write-Warning "Use-PBIGroup requires either Id or Name parameter or Clear switch."
    }
}

#endregion

#region Exported Upload Functions
Function Import-PBIContent{
    [CmdletBinding()]        
    param(    
        [Parameter(ValueFromPipelineByPropertyName,Mandatory=$true)][Alias('PSPath')] [string]$FilePath,        
        [Parameter(Mandatory=$false)] [string] $Dataset,
        [Parameter(Mandatory=$false)] [object[]] $ConnectionReplacement 
    )

    Begin {
        if ($PSCmdlet.MyInvocation.ExpectingInput -and $PSBoundParameters.ContainsKey(‘Dataset’)) {
            Write-Warning "Dataset parameter will be ignored."
        }
    }

    Process {
        $File = Convert-Path $FilePath
        $requestUrl = "https://api.powerbi.com/v1.0/myorg/"
        if (!$IgnoreGroup){
            if(![string]::IsNullOrWhiteSpace($script:GroupId))
            {
                $requestUrl =  "$($requestUrl)groups/$($script:GroupId)/"
            }
        }
        if ([string]::IsNullOrEmpty($Dataset) -or $PSCmdlet.MyInvocation.ExpectingInput)
        {
            $Dataset =  [IO.Path]::GetFileNameWithoutExtension($File)
        }
        $requestUrl =  "$($requestUrl)imports?datasetDisplayName=$($Dataset)"

        #Check if the dataset already exists, detemine nameConflict value
        $matchDatset = Get-PBIDataset | Where-Object name -eq $Dataset
        if (!$matchDatset){
            $requestUrl =  "$($requestUrl)&nameConflict=Abort"
        }
        else {
            $requestUrl =  "$($requestUrl)&nameConflict=Overwrite"
        }


        $requestHeaders = Get-PBIRequestHeader
        Write-Verbose "Invoke POST $($requestUrl)"
    
        $fileName = [IO.Path]::GetFileName($File)
        $boundary = "--QQQwwQqqydfdsfhjdjdhsgRRfdfdfd545623529ZMNSJYTkkKKKgfddssereewrw5435345bnvv"   
        
        $fileBin = [IO.File]::ReadAllBytes($File)
        $ansiEncoding = [System.Text.Encoding]::GetEncoding("iso-8859-1")
    
        $bodyLines = (
            $boundary,
            "Content-Disposition: form-data; name=`"file0`"; filename=`"$($fileName)`"",
            "Content-Type: application/x-zip-compressed",
            "",        
            $ansiEncoding.GetString($fileBin),
            "$($boundary)--",
            ""
            ) -join [System.Environment]::NewLine
    
        try {    
            $result = Invoke-RestMethod -Uri $requestUrl -Headers $requestHeaders -Method Post -ContentType "multipart/form-data; boundary=$($boundary)" -Body $bodyLines
    
            Write-Information "$($File) uploaded."

            if ($ConnectionReplacement) {
                #get the new Dataset id from the import id
                $WaitCount=0
                $ImportState = "none"
                while (($WaitCount -lt 60) -and ($ImportState  -ne "Succeeded")) {
                    Start-Sleep -Seconds 1
                    $ImportedAssets = $null
                    $ImportedAssets = Invoke-PBIMethod "imports/$($result.id)"
                    $ImportState = $ImportedAssets.importState
                    $WaitCount++
                }
                Write-Verbose "Import state $($ImportState) waited $($WaitCount) * seconds"
                if ($ImportState  -eq "Succeeded") {
                    $ImportedDatasetId = $ImportedAssets.datasets[0].id
                    Write-Verbose "Imported dataset id $($ImportedDatasetId)"
                    #replace datasource connections
                    Set-PBIDatasource -DatasetId $ImportedDatasetId -ConnectionReplacement $ConnectionReplacement
                }
                else {
                    Write-Warning "Cannot replace connections Import $($result.id) State $($ImportState)"
                }
            }
        }
        catch [System.Exception] {
            Write-Error $_.Exception.Message
        }
    }
}

#endregion


#region Exported Wrapper Functions
function Get-PBIGroup
{
    $result = Invoke-PBImethod "groups" -IgnoreGroupuse-pbi
    $result.Value
}
function Get-PBIReport
{
    $result = Invoke-PBImethod "reports"
    $result.Value
}
function Get-PBIDataset
{
    $result = Invoke-PBImethod "datasets"
    $result.Value
}
function Get-PBIDashboard
{
    $result = Invoke-PBImethod "dashboards"
    $result.Value
}
function Get-PBITile
{
   [CmdletBinding(DefaultParameterSetName = 'default')]
    param(
        [Parameter(Mandatory=$true)] [string] $DashboardId 
    )
    $result = Invoke-PBImethod "dashboards/$($DashboardId)/tiles"
    $result.Value
}
#endregion

#region Datasource functions
function Get-PBIDatasource
{
   [CmdletBinding(DefaultParameterSetName = 'default')]
    param(
        [Parameter(Mandatory=$true)] [string] $DatasetId 
    )
    # get dataset check owner
    $dataset = Invoke-PBImethod "datasets/$($DatasetId)"
    if ($dataset.configuredBy -ne $script:AuthenticatedUser)
    {
        Write-Information "Dataset $($DatasetId) transfer ownership from $($dataset.configuredBy) to $($script:AuthenticatedUser)"
        $result = Invoke-PBImethod -ApiOperation "datasets/$($DatasetId)/takeover" -RequestMethod POST
    }
    $result = Invoke-PBImethod "datasets/$($DatasetId)/datasources"
    $result.value
}

function Set-PBIDatasource {
   [CmdletBinding(SupportsShouldProcess = $true ,ConfirmImpact='Medium',DefaultParameterSetName = 'default')]
    param(
        [Parameter(Mandatory=$true)] [string] $DatasetId,
        [Parameter(Mandatory=$true)] [object[]] $ConnectionReplacement 
    )
    if ($PSCmdlet.ShouldProcess("Dataset $($DatasetId)")) {
        $datasources = Get-PBIDatasource -DatasetId $DatasetId
        $ServerReplacement = $ConnectionReplacement | Where-Object {$_.CurrentServer -and !$_.CurrentDatabase}
        Foreach ($datasource in $datasources) {
            if ($datasource.connectionDetails) {
                if($datasource.connectionDetails.server)
                {
                    $matchval = $null
                    $matchval =  $ServerReplacement | Where-Object CurrentServer -eq  $datasource.connectionDetails.server
                    if ($matchval)
                    {
                        Write-Information "Replace dataset $($DatasetId) server $($datasource.connectionDetails.server) with $($ServerReplacement[0].NewServer)"
                        $Newconnection = $datasource.connectionDetails.PSObject.Copy()
                        $Newconnection.server = $ServerReplacement[0].NewServer
                        $RequestBody = @{
                            updateDetails = @(
                                @{connectionDetails=$Newconnection
                                datasourceSelector=$datasource}
                            )
                        }
                        Invoke-PBIMethod -ApiOperation "datasets/$($DatasetId)/updatedatasources" -RequestMethod POST -RequestBody $RequestBody

                    }
                }
            }
        
        }
    }

}
#endregion


#region private functions
function Get-PBIAccesstoken{
    #todo check connected, expiry
    $script:AccessToken
}

Function Get-PBIRequestHeader
{    
    $accesToken    = Get-PBIAccesstoken
    $requestHeaders = @{
        'Content-Type'='application/json'
        'Authorization'= "Bearer $accesToken"
    }
    $requestHeaders
}

#endregion