UMN-Google.psm1

###############
# Module for interacting with Google API
#
#
###############

###
# Copyright 2017 University of Minnesota, Office of Information Technology

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with Foobar. If not, see <http://www.gnu.org/licenses/>.
###

function ConvertTo-Base64URL
{
    <#
        .Synopsis
        convert text or byte array to URL friendly BAse64
        .DESCRIPTION
        convert text or byte array to URL friendly BAse64
        .EXAMPLE
        ConvertTo-Base64URL -text $headerJSON
        .EXAMPLE
        ConvertTo-Base64URL -Bytes $rsa.SignData($toSign,"SHA256")
    #>

    param
    (
        [Parameter(ParameterSetName='String')]
        [string]$text,

        [Parameter(ParameterSetName='Bytes')]
        [System.Byte[]]$Bytes
    )

    if($Bytes){$base = $Bytes}
    else{$base =  [System.Text.Encoding]::UTF8.GetBytes($text)}
    $base64Url = [System.Convert]::ToBase64String($base)
    $base64Url = $base64Url.Split('=')[0]
    $base64Url = $base64Url.Replace('+', '-')
    $base64Url = $base64Url.Replace('/', '_')
    $base64Url
}

################################################## OAuth #################################################################
function Get-GOAuthTokenUser
{
    <#
        .Synopsis
        Get Valid OAuth Token. The access token is good for an hour, the refresh token is mostly permanent and can be used to get a new access token without having to reauthenticate
        .DESCRIPTION
        Long description
        .EXAMPLE
        You can provide multiple scopes for access to multiple APIs at the same time. They just need to be separated by a space
        $scope = "https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive" provides access to spreadsheets and drive api.
        .EXAMPLE
        Another example of how to use this cmdlet
    #>

    [CmdletBinding()]
    [OutputType([array])]
    Param
    (
        # projectID, appKey, and appSecret
        [Parameter(Mandatory)]
        [string]$projectID, # Name of API Project

        [Parameter(Mandatory)]
        [string]$appKey,    # App key ID

        [Parameter(Mandatory)]
        [string]$appSecret, # App key secret

        [Parameter(Mandatory)]
        [string]$scope, ## example $scope = "https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive"

        [Parameter(Mandatory)]
        [string]$redirectUri, ## example $redirectUri = "https://umn.edu" has to be set to https

        [string]$refreshToken
    )

    Begin
    {
    }
    Process
    {
        ### If no refresh token - requires human interaction with IE
        if(!($refreshToken))
        { 
            ### Get Google API access - https://developers.google.com/identity/protocols/OAuth2WebServer#offline
            #$scope = "https://www.googleapis.com/auth/spreadsheets"
            $response_type = "code"
            $approval_prompt = "force"
            $access_type = "offline"
 
            ### Get the authorization code
            $auth_string = "https://accounts.google.com/o/oauth2/auth?scope=$scope&response_type=$response_type&redirect_uri=$redirectUri&client_id=$appKey&access_type=$access_type&approval_prompt=$approval_prompt"
 
            $ie = New-Object -comObject InternetExplorer.Application
            if($approval_prompt -eq "force"){$ie.visible = $true}
            $null = $ie.navigate($auth_string)
            #Wait for user interaction in IE, manual approval
            do{Start-Sleep 1}until($ie.LocationURL -match 'code=([^&]*)')
            $null = $ie.LocationURL -match 'code=([^&]*)'
            $authorizationCode = $matches[1]
            $null = $ie.Quit()

            ### exchange the authorization code for a refresh token and access token
            $grantType = "authorization_code"
            $requestUri = "https://accounts.google.com/o/oauth2/token"
            $requestBody = "code=$authorizationCode&client_id=$appKey&client_secret=$appSecret&grant_type=$grantType&redirect_uri=$redirectUri"
 
            $response = Invoke-RestMethod -Method Post -Uri $requestUri -ContentType "application/x-www-form-urlencoded" -Body $requestBody

            $props = @{
                accessToken = $response.access_token
                refreshToken = $response.refresh_token
            }
        }

        ### If refresh token exists
        else
        { 
            ### exchange the refresh token for an access token
            $grantType = "refresh_token"
            $requestUri = "https://accounts.google.com/o/oauth2/token"
            $requestBody = "refresh_token=$refreshToken&client_id=$appKey&client_secret=$appSecret&grant_type=$grantType"
 
            $response = Invoke-RestMethod -Method Post -Uri $requestUri -ContentType "application/x-www-form-urlencoded" -Body $requestBody
            $props = @{
                accessToken = $response.access_token
                refreshToken = $refreshToken
            }
        }
        
        return new-object psobject -Property $props
    }
    End
    {
    }
}

function Get-GOAuthTokenService
{
    <#
        .Synopsis
        Get google auth 2.0 token for a service account
        .DESCRIPTION
        Long description
        .EXAMPLE
        Get-GOAuthTokenService -scope "https://www.googleapis.com/auth/spreadsheets" -certPath "C:\users\$env:username\Desktop\googleSheets.p12" -certPswd 'notasecret' -iss "oit-automation@oit-mpt-powershell-sheets.iam.gserviceaccount.com"
        .EXAMPLE
        Another example of how to use this cmdlet
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [string]$scope,

        [Parameter(Mandatory)]
        [string]$certPath,

        [Parameter(Mandatory)]
        [string]$certPswd,

        [Parameter(Mandatory)]
        [string]$iss ## Google service account email address
    )

    Begin
    {
    }
    Process
    {        
        # build JWT header
        $headerJSON = [Ordered]@{
            alg = "RS256"
            typ = "JWT"
        } | ConvertTo-Json -Compress
        $headerBase64 = ConvertTo-Base64URL -text $headerJSON

        ## Claims
        ## Build date times needed in seconds
        $now = (Get-Date).ToUniversalTime()
        $iat = [Math]::Floor([decimal](Get-Date($now) -UFormat "%s"))
        $exp = [Math]::Floor([decimal](Get-Date($now.AddMinutes(59)) -UFormat "%s")) 
        $aud = "https://www.googleapis.com/oauth2/v4/token"
        $claimsJSON = [Ordered]@{
            iss = $iss
            scope = $scope
            aud = $aud
            exp = $exp
            iat = $iat
        } | ConvertTo-Json -Compress

        $claimsBase64 = ConvertTo-Base64URL -text $claimsJSON

        ################# Create JWT
        # Prep Cert
        #$certPath = "C:\users\$env:username\Desktop\googleSheets.p12"
        $googleCert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certPath, $certPswd,[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable ) 
        # get just the private key
        $rsaPrivate = $googleCert.PrivateKey 
        # get a new RSA provider
        $rsa = New-Object System.Security.Cryptography.RSACryptoServiceProvider 
        # copy the parameters from the private key into our new rsa provider
        $null = $rsa.ImportParameters($rsaPrivate.ExportParameters($true))
        # signature is our base64urlencoded header and claims, seperated by a .
        $toSign = [System.Text.Encoding]::UTF8.GetBytes($headerBase64 + "." + $claimsBase64)
        # sign the sig, we then serialize to UTF-8 bytes, then base64url encode the signature
        $signature = ConvertTo-Base64URL -Bytes $rsa.SignData($toSign,"SHA256") ## this needs to be converted back to regular text
        ## request
        $jwt = $headerBase64 + "." + $claimsBase64 + "." + $signature
        $fields = 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion='+$jwt

        $response = Invoke-RestMethod -Uri "https://www.googleapis.com/oauth2/v4/token" -Method Post -Body $fields -ContentType "application/x-www-form-urlencoded"
        $response.access_token
    }
    End
    {
        
    }
}

################################################## sheets #################################################################

function Get-GSheetData
{
    <#
        .Synopsis
        Basic function for retrieving data from a specific Google Sheet.
        .DESCRIPTION
        Long description
        .EXAMPLE
        Example of how to use this cmdlet
        .EXAMPLE
        Another example of how to use this cmdlet
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [string]$sheetID,

        [Parameter(Mandatory)]
        [string]$accessToken,

        [Parameter(Mandatory)]
        [string]$sheetName,

        ## Range in A1 notation https://msdn.microsoft.com/en-us/library/bb211395(v=office.12).aspx
        ## The dimensions of the $values you put in MUST fit within this range
        [string]$rangeA1,

        [Parameter(Mandatory)]
        [ValidateSet('AllData','Range')]
        [string]$cell,

        [Parameter()]
        [ValidateSet('FORMATTED_VALUE', 'UNFORMATTED_VALUE', 'FORMULA')]
        [string]$valueRenderOption = "FORMATTED_VALUE"

    )

    Begin
    {
    }
    Process
    {
        $uri = "https://sheets.googleapis.com/v4/spreadsheets/$sheetID/values/$sheetName"

        if($cell -eq "Range") {
            $uri += "!$rangeA1"
        }

        $uri += "?valueRenderOption=$valueRenderOption"

        $result = Invoke-RestMethod -Method GET -Uri $uri -Headers @{"Authorization"="Bearer $accessToken"}
        $sheet = $result.values
        $Rows = $sheet.Count
        $Columns = $sheet[0].Count
        $HeaderRow = 0
        $Header = $sheet[0]
        foreach ($Row in (($HeaderRow + 1)..($Rows-1))) { 
            $h = [Ordered]@{}
            foreach ($Column in 0..($Columns-1)) {
                if ($sheet[0][$Column].Length -gt 0) {
                    $Name = $Header[$Column]
                    $h.$Name = $Sheet[$Row][$Column]
                }
            }
            [PSCustomObject]$h
        }
    }
    End
    {
    }
}

function Set-GSheetData
{
    <#
        .Synopsis
        Set values in sheet in specific cell locations or append data to a sheet
        .DESCRIPTION
        Long description
        .EXAMPLE
        Set-GSheetData -sheetID '1LvqbZSTlgQNIBC2bkv9Ze6nFWPBY98ASI2_sMc1DQSE' -accessToken $accessToken -sheetName 'Sheet1' -rangeA1 'A19:B20' -values @(@("a","b"),@("c","D"))
        .EXAMPLE
        Another example of how to use this cmdlet
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [string]$sheetID,

        [Parameter(Mandatory)]
        [string]$accessToken,

        [Parameter(Mandatory)]
        [string]$sheetName,

        ## Range in A1 notation https://msdn.microsoft.com/en-us/library/bb211395(v=office.12).aspx
        ## The dimensions of the $values you put in MUST fit within this range
        [Parameter(ParameterSetName='set')]
        [string]$rangeA1,

        [Parameter(ParameterSetName='Append')]
        [switch]$append,

        ## This shoudl be an array or arrays. Each internal array represents one ROW
        [Parameter(Mandatory)]
        [System.Collections.ArrayList]$values,

        [string]$valueInputOption = 'RAW'

    )

    Begin
    {
    }
    Process
    {
        if ($append)
        {
            $method = 'POST'
            $uri = "https://sheets.googleapis.com/v4/spreadsheets/$sheetID/values/$sheetName"+":append?valueInputOption=$valueInputOption"
        }
        else
        {
            $method = 'PUT'
            $uri = "https://sheets.googleapis.com/v4/spreadsheets/$sheetID/values/$sheetName!$rangeA1"+"?valueInputOption=$valueInputOption"
        }
        $json = @{values=$values} | ConvertTo-Json
        ###### uncomment the following two lines for debug
        #$uri
        #$json
        $ContentType = "application/json"
        
        Invoke-RestMethod -Method $method -Uri $uri -Body $json -ContentType $ContentType -Headers @{"Authorization"="Bearer $accessToken"}
        
    }
    End
    {
    }
}

function Get-GSheetProperties
{
    <#
        .Synopsis
        Short description
        .DESCRIPTION
        Long description
        .EXAMPLE
        Example of how to use this cmdlet
        .EXAMPLE
        Another example of how to use this cmdlet
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [string]$sheetID,

        [Parameter(Mandatory)]
        [string]$accessToken

    )

    Begin
    {
    }
    Process
    {
        $uri = "https://sheets.googleapis.com/v4/spreadsheets/$sheetID"
        $Data = Invoke-RestMethod -Method GET -Uri $uri -Headers @{"Authorization"="Bearer $accessToken"}
    }
    End
    {
        return([array]$data)
    }
}

function get-GSheetID
{
    <#
        .Synopsis
        Get a spreadsheet ID.
        .DESCRIPTION
        Provide a case sensative sheet name to the function to get back the sheetID used in many other API calls.
        mimeTymes are split out to only retrieve spreadSheet IDs (no folders or other files)
        .EXAMPLE
        $fileName = 'Test'
        get-GSheetID -fileName $fileName
        There are 2 files matching the provided name. Please investigate the following sheet IDs to verify which file you want.
            sheetID0 = 1X3JO1sbchh6F8ZxOwTxyAVVqUisyVM2qWRipfDoeLdQ
            sheetID1 = 1I7mhlRDazIF3f5chIcDN7ru1hkrGlxAWCXtpHw6L4Nc
        .EXAMPLE
            Case sensitivity -
        $fileName = 'test'
        get-GSheetID -FileName 'test'
            1wKsiD7QOglTwA27eoKqmdwudJL4XAjt2rx58gpscBVY
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [string]$fileName,

        [Parameter(Mandatory)]
        [string]$accessToken
    )

    Begin
    {
    }
    Process
    {

        $uri = "https://www.googleapis.com/drive/v3/files?q=name%3D'$FileName'"
        $sheetID = (((Invoke-RestMethod -Method get -Uri $uri -Headers @{"Authorization"="Bearer $accessToken"}).files) | where {$_.mimetype -eq "application/vnd.google-apps.spreadsheet"}).id
        If ($sheetID.count -eq 0){throw "There are no files matching the name $fileName"}
        If ($sheetID.count -gt 1){Write-Warning "There are $($sheetid.Count) files matching the provided name. Please investigate the following sheet IDs to verify which file you want.";return($sheetID)}
        Else{return($sheetID)}
    }
    End
    {                
    }
}

function New-GSheet
{
    <#
        .Synopsis
        Provide JSON properties to create new Google Sheet.
        .DESCRIPTION
        The properties that can be set are extensive. Cell color, formatting etc - most commonly this will only be used for setting the file name.
        Return is array data of the created sheet in order to retrieve the newly generated sheetID to continue working with the remaining functions.
        .EXAMPLE
            Example for setting the title of the sheet
        $properties = @{properties=@{title="JSON ARRAYS ARE AWESOME"}} |convertto-json
        create-GSheet -properties $properties -accessToken $accessToken
        .EXAMPLE
        Another example of how to use this cmdlet
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [string]$Title,

        [array]$properties
    )

    Begin
    {
        If (!$properties)
            {
            $properties = @{properties=@{title=$Title}} |convertto-json
            }
    }
    Process
    {
        $uri = "https://sheets.googleapis.com/v4/spreadsheets"
        $ContentType = "application/json"
        $data = Invoke-RestMethod -Method Post -Uri $uri -Body $properties -ContentType $ContentType -Headers @{"Authorization"="Bearer $accessToken"}
    }
    End
    {
    return([array]$data)
    }
}

function set-GSheetPermissions
{
    <#
        .Synopsis
        Provide JSON properties to set file permissions.
        .DESCRIPTION

        .EXAMPLE

        .EXAMPLE
        Another example of how to use this cmdlet
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [string]$emailAddress, ## email address of user or group to be shared with

        [Parameter(Mandatory)]
        [string]$fileID,

        [string]$role = "writer",

        [ValidateSet('user','group')]
        [string]$type

 
    )

    Begin
    {
    }
    Process
    {
        $json = @{emailAddress=$emailAddress;type=$type;role=$role} | ConvertTo-Json
        $ContentType = "application/json"
        $uri = "https://www.googleapis.com/drive/v3/files/$fileID/permissions"
        
        Invoke-RestMethod -Method post -Uri $uri -Body $json -ContentType $ContentType -Headers @{"Authorization"="Bearer $accessToken"}
    }
    End
    {
    return([array]$data)
    }
}


function move-GSheetData
{
    <#
        .Synopsis
            'Move' data around
        .DESCRIPTION
            This is a cut and paste between sheets in a spreadsheet.
            The function will find the row index based on search criteria, and move between the sheets provided.
        .EXAMPLE
            $newSheetName = 'Decommissioned'
            $currentSheetName = 'Servers'
            $sheetID = get-GSheetID -FileName 'MPT-ServerDoco'
            $columnKey = 'Server name' # value based on column name to search
            $query = 'test1124' # Key item in column to search for. Such as the server's name
            $accessToken = Get-GOAuthTokenService -scope $scope -certPath $certPath -certPswd $certPswd -iss $iss

            move-gsheetData -sheetID $sheetID -accessToken $accessToken -CurrentSheetName $currentSheetName -newSheetName $newSheetName -query $query -columnKey $columnKey
        .EXAMPLE
            move-GSheetData -sheetID $sheetID -accessToken $accessToken -currentSheetName 'Servers' -newSheetName 'Decommissioned' -query 'Virt-vum-dev' -columnKey 'Server name'
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [string]$sheetID,

        [Parameter(Mandatory)]
        [string]$accessToken,

        [Parameter(Mandatory)]
        [string]$currentSheetName,

        [Parameter(Mandatory)]
        [string]$newSheetName,

        [Parameter(Mandatory)]
        [string]$query,

        [Parameter(Mandatory)]
        [string]$columnKey

    )

    Begin
    {


    }
    Process
    {
        ## Query all data from sheet
        $data = Get-GSheetData -sheetID $sheetID -accessToken $accessToken -sheetName $currentSheetName -cell AllData
        $destinationData = Get-GSheetData -sheetID $sheetID -accessToken $accessToken -sheetName $newSheetName -cell AllData

        ## Get row query belongs to
        $Index = (0..($data.count -1) | where {$Data[$_].$columnKey -eq $query})
        
        ## Sanity Check - is this the data?
        if (!$Index) {write-host "$Query in $columnKey does not exist"
            break}
        Else {
        $rowIndex = $index +2    
        $startRow = $Index + 1
        $destinationRow = ($destinationData).count + 2
        $destinationStartRow = ($destinationData).count + 1
        }

        ## Get sheet index ID numbers
        $allSheetProperties = (Get-GSheetProperties -sheetID $sheetID -accessToken $accessToken).sheets.properties

        $currentSheetIndex = ($allSheetProperties | where {$_.title -eq $currentSheetName}).sheetID
        $newSheetIndex = ($allSheetProperties | where {$_.title -eq $newSheetName}).sheetID                                

        $method = 'POST'
        $uri = "https://sheets.googleapis.com/v4/spreadsheets/$sheetID"+":batchUpdate"
        $ContentType = "application/json"
        
   
        ## cutPaste row to row
        $values = @{"cutPaste"=@{"source"=@{"sheetId"=$currentSheetIndex;"startRowIndex"=$startRow;"endRowIndex"=$rowIndex};"destination"=@{"sheetId"=$newSheetIndex;"rowIndex"=$destinationRow};"pasteType"="PASTE_NORMAL"}}
        $JSON = @{"requests"=$values} |ConvertTo-Json -Depth 20
            
        
        
        Invoke-RestMethod -Method $method -Uri $uri -Body $json -ContentType $ContentType -Headers @{"Authorization"="Bearer $accessToken"}
        
    }
    End
    {
    }
}

function add-GSheet
{
    <#
    .Synopsis
        Add named pages/sheets to an existing document
    .DESCRIPTION
        This function will add a specified sheet name to a google spreadsheet.
    .EXAMPLE
        $sheetName = "Data 2"
        $sheetID = ## the id number of the file
        add-gSheet -sheetName $sheetName -sheetID $sheetID -accessToken $accessToken

    .EXAMPLE
    
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [string]$sheetName,

        [Parameter(Mandatory)]
        [string]$sheetID,

        [Parameter(Mandatory)]
        [string]$accessToken
    )

    Begin
    {
        If (!$properties)
            {
            $properties = @{requests=@(@{addSheet=@{properties=@{title=$sheetName}}})} |convertto-json -Depth 10
            }
    }
    Process
    {
        $suffix = "$sheetID" + ":batchUpdate"
        $uri = "https://sheets.googleapis.com/v4/spreadsheets/$suffix"
        $ContentType = "application/json"
        $data = Invoke-RestMethod -Method Post -Uri $uri -Body $properties -ContentType $ContentType -Headers @{"Authorization"="Bearer $accessToken"}
    }
    End
    {
    return([array]$data)
    }
}

function remove-GSheet
{
    <#
        .Synopsis
        Removes a sheet from a spreadsheet based on sheetID
        .DESCRIPTION
            This function will delete a sheet.
        .EXAMPLE
            $pageID = 0 ## using pageID to differentiate from sheetID --
            In this case, index 0 is the actual sheetID per the API and will be deleted.

            $sheetID = ## the id number of the file/spreadsheet

            remove-gsheet -pageID $pageID -sheetID $sheetID -accessToken $accessToken

        .EXAMPLE
        
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [string]$pageID,
        
        [Parameter(Mandatory)]
        [string]$sheetID,

        [Parameter(Mandatory)]
        [string]$accessToken
    )

    Begin
    {
        If (!$properties)
            {
            $properties = @{requests=@(@{deleteSheet=@{sheetId=$pageID}})} |convertto-json -Depth 10
            }
    }
    Process
    {
        $suffix = "$sheetID" + ":batchUpdate"
        $uri = "https://sheets.googleapis.com/v4/spreadsheets/$suffix"
        $ContentType = "application/json"
        $data = Invoke-RestMethod -Method Post -Uri $uri -Body $properties -ContentType $ContentType -Headers @{"Authorization"="Bearer $accessToken"}
    }
    End
    {
    return([array]$data)
    }
}

##########################################################################################################################
Export-ModuleMember -Function *