WhiteboardAdmin.psm1

#
# Constants
#

$Script:ServiceEndpoint = 'https://whiteboard.microsoft.com/api/v1.0'

# Set Resource URI to Whiteboard
$Script:ResourceAppIdUri = 'https://whiteboard.microsoft.com'

# Azure AD tenant authority
$Script:AuthUri = 'https://login.microsoftonline.com/common/'

$Script:UserAgent = 'WhiteboardAdminModule/1.0'

#
# Cmdlets
#

<#
.SYNOPSIS
Gets one or more Whiteboards from the Microsoft Whiteboard service and returns them as objects.
 
.PARAMETER Token
The Azure AD bearer token corresponding to the specified credentials. If unspecified, a new token will be generated.
 
.PARAMETER UserId
(Optional) The ID of the user account to query Whiteboards for. All Whiteboards this account has access to will be returned.
 
.PARAMETER WhiteboardId
(Optional) The ID of a specific Whiteboard.
 
.PARAMETER ForceAuthPrompt
(Optional) Always prompt for auth. Use to ignore cached credentials.
 
.EXAMPLE
Get-Whiteboard -UserId 00000000-0000-0000-0000-000000000001
Get all of a user's Whiteboards.
#>

function Get-Whiteboard
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$false)]
        [Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationResult]$Token,
        
        [Parameter(Mandatory=$true, ParameterSetName='ForUser')]
        [Parameter(Mandatory=$true, ParameterSetName='SingleWhiteboard')]
        [Guid]$UserId,
        
        [Parameter(Mandatory=$true, ParameterSetName='SingleWhiteboard')]
        [Guid]$WhiteboardId,
        
        [Parameter(Mandatory=$false)]
        [switch]$ForceAuthPrompt)

    if ($WhiteboardId -eq $null)
    {
        $tokenAndResult = Get-WhiteboardInternal -Token $Token -UserId $UserId -ForceAuthPrompt:$ForceAuthPrompt
    }
    else
    {
        $tokenAndResult = Get-WhiteboardInternal -Token $Token -UserId $UserId -WhiteboardId $WhiteboardId -ForceAuthPrompt:$ForceAuthPrompt
    }
    
    return $tokenAndResult.RequestResult
}

<#
.SYNOPSIS
Sets the owner for a Whiteboard.
 
.PARAMETER Token
The Azure AD bearer token corresponding to the specified credentials. If unspecified, a new token will be generated.
 
.PARAMETER WhiteboardId
The Whiteboard for which the owner is being changed.
 
.PARAMETER OldOwnerId
The ID of the previous owner.
 
.PARAMETER NewOwnerId
The ID of the new owner.
 
.PARAMETER ForceAuthPrompt
(Optional) Always prompt for auth. Use to ignore cached credentials.
 
.EXAMPLE
Set-WhiteboardOwner -OldOwnerId 00000000-0000-0000-0000-000000000001 -NewOwnerId 00000000-0000-0000-0000-000000000002
Move a Whiteboard from one user to another.
#>

function Set-WhiteboardOwner
{
    [CmdletBinding(
        SupportsShouldProcess = $true,
        ConfirmImpact='High')]
    param(
        [Parameter(Mandatory=$false)]
        [Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationResult]$Token,
        
        [Parameter(Mandatory=$true)]
        [Guid]$WhiteboardId,
        
        [Parameter(Mandatory=$true)]
        [Guid]$OldOwnerId,
        
        [Parameter(Mandatory=$true)]
        [Guid]$NewOwnerId,
        
        [Parameter(Mandatory=$false)]
        [switch]$ForceAuthPrompt)

    if ($PSCmdlet.ShouldProcess("Whiteboard: $WhiteboardId"))
    {
        $uri = "$ServiceEndpoint/users/$OldOwnerId/whiteboards/$WhiteboardId"
        
        # Manually convert the body to JSON (Invoke-RestMethod tries to URL encode the body if you pass it a hash)
        $body = ConvertTo-Json -Compress @(@{"op"="replace"; "path"="/OwnerId"; "value"=$NewOwnerId })
        
        $tokenAndResult = Invoke-RestMethodWithAuth `
            -Token $Token `
            -ForceAuthPrompt:$ForceAuthPrompt `
            -RestMethodParams @{'Method' = 'PATCH'; 'Uri' = $uri; 'ContentType' = 'application/json-patch+json'; 'UserAgent' = $UserAgent; 'Body' = $body }
        
        return $tokenAndResult.RequestResult
    }
}

<#
.SYNOPSIS
Transfer ownership of all Whiteboards owned by a user to another user.
 
.PARAMETER Token
The Azure AD bearer token corresponding to the specified credentials. If unspecified, a new token will be generated.
 
.PARAMETER OldOwnerId
The ID of the previous owner.
 
.PARAMETER NewOwnerId
The ID of the new owner.
 
.PARAMETER WhatIf
Execute the command without making any actual changes. Only calls read methods on the REST service.
 
.PARAMETER ForceAuthPrompt
(Optional) Always prompt for auth. Use to ignore cached credentials.
 
.EXAMPLE
Invoke-TransferAllWhiteboards -OldOwnerId 00000000-0000-0000-0000-000000000001 -NewOwnerId 00000000-0000-0000-0000-000000000002 -WhatIf
Check how many Whiteboards will be transferred without transferring them.
 
.EXAMPLE
Invoke-TransferAllWhiteboards -OldOwnerId 00000000-0000-0000-0000-000000000001 -NewOwnerId 00000000-0000-0000-0000-000000000002
Transfer (and prompt before performing any write actions).
#>

function Invoke-TransferAllWhiteboards
{
    [CmdletBinding(
        SupportsShouldProcess = $true,
        ConfirmImpact='High')]
    param(
        [Parameter(Mandatory=$false)]
        [Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationResult]$Token,

        [Parameter(Mandatory=$true)]
        [Guid]$OldOwnerId,
        
        [Parameter(Mandatory=$true)]
        [Guid]$NewOwnerId,
        
        [Parameter(Mandatory=$false)]
        [switch]$ForceAuthPrompt)
    
    $whiteboardsAndToken = Get-WhiteboardInternal -Token $Token -UserId $OldOwnerId -ForceAuthPrompt:$ForceAuthPrompt
    $Token = $whiteboardsAndToken.Token
    $whiteboards = $whiteboardsAndToken.RequestResult
    
    # Only transfer Whiteboards actually owned by this Id
    $whiteboardsOwned = @($whiteboards |
        Where-Object { [Guid]::Parse($_.ownerId) -eq $OldOwnerId })
    Write-Verbose "Found $($whiteboardsOwned.Length) Whiteboards for owner $($OldOwnerId)"
    
    if ($PSCmdlet.ShouldProcess("Whiteboards for Owner: $OldOwnerId"))
    {
        $whiteboardsOwned |
            ForEach-Object { Set-WhiteboardOwner -Token $Token -OldOwnerId $OldOwnerId -NewOwnerId $NewOwnerId -WhiteboardId $_.id -Confirm:$false }
    }
}

#
# Private methods
#

function Get-WhiteboardInternal
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$false)]
        [Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationResult]$Token,
        
        [Parameter(Mandatory=$true)]
        [Guid]$UserId,
        
        [Parameter(Mandatory=$false)]
        [Guid]$WhiteboardId,
        
        [Parameter(Mandatory=$false)]
        [switch]$ForceAuthPrompt)
    
    if ($null -ne $UserId)
    {
        $uri = "$ServiceEndpoint/users/$UserId/whiteboards"
    }
    else
    {
        $uri = "$ServiceEndpoint/whiteboards"
    }
    
    $tokenAndResult = Invoke-RestMethodWithAuth `
        -Token $Token `
        -ForceAuthPrompt:$ForceAuthPrompt `
        -RestMethodParams @{'Method' = 'GET'; 'Uri' = $uri; 'ContentType' = 'application/json'; 'UserAgent' = $UserAgent }
    
    if ($null -ne $WhiteboardId)
    {
        $filteredResult = $tokenAndResult.RequestResult | ? { [Guid]::Parse($_.id) -eq $WhiteboardId }
    }
    else
    {
        $filteredResult = $tokenAndResult.RequestResult
    }
    
    return New-Object -TypeName PSObject -Prop @{'RequestResult' = $filteredResult; 'Token' = $Token}
}

function Invoke-RestMethodWithAuth(
    [Parameter(Mandatory=$false)]
    [Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationResult]$Token,
    
    [Parameter(Mandatory=$false)]
    [switch]$ForceAuthPrompt,
    
    [Parameter(Mandatory=$true)]
    [Hashtable]$RestMethodParams)
{
    # Use the clientId from the Azure PowerShell module
    $clientId = '1950a258-227b-4e31-a9cf-717495945fc2'
    
    $redirectUri = 'urn:ietf:wg:oauth:2.0:oob'

    Write-Verbose "Authority URI: $AuthUri"
    
    $authContext = [Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext]::new($AuthUri)
    
    if ($ForceAuthPrompt -eq $true)
    {
        $platformParameters =  [Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters]::new(
            [Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior]::Always)
    }
    else
    {
        $platformParameters = [Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters]::new(
            [Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior]::Auto)
    }
    
    $userIdentifier = [Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier]::AnyUser
    
    # No token specified, so get a new one
    if ($Token -eq $null)
    {
        $Token = $authContext.AcquireTokenAsync($Script:ResourceAppIdUri, $clientId, $redirectUri, $platformParameters, $userIdentifier).Result
    }
    
    try
    {
        Set-RequestHeader -Token $Token -RestMethodParams $RestMethodParams
        
        $requestResult = Invoke-RestMethod @RestMethodParams
        Write-Verbose "No need to rebuild token"
    }
    catch [System.Net.WebException]
    {
        Write-Verbose "Rebuilding token from claims"
        $ex = $_
        $stream = $ex.Exception.Response.GetResponseStream()
        $streamReader = [System.IO.StreamReader]::new($stream)
        $content = $streamReader.ReadToEnd()
        
        # If unable to parse the claims JSON, throw the outer exception
        try
        {
            $claims = ($content | ConvertFrom-Json).claims
        }
        catch
        {
            throw $ex
        }
        
        # The second token acquisition should trigger MFA in the cases where it is enabled
        $Token = $authContext.AcquireTokenAsync($Script:ResourceAppIdUri, $clientId, $redirectUri, $platformParameters, $userIdentifier, "claims=$claims").Result
        Set-RequestHeader -Token $Token -RestMethodParams $RestMethodParams
        
        # Invoke the method again now that we have the full auth token
        $requestResult = Invoke-RestMethod @RestMethodParams
    }
    
    return New-Object -TypeName PSObject -Prop @{'RequestResult' = $requestResult; 'Token' = $Token}
}

# Replace the auth header only
function Set-RequestHeader(
    [Parameter(Mandatory=$true)]
    [Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationResult]$Token,
    
    [Parameter(Mandatory=$true)]
    [Hashtable]$RestMethodParams)
{
    if (-not $RestMethodParams.ContainsKey('Headers'))
    {
        $RestMethodParams['Headers'] = @{}
    }
    
    $RestMethodParams['Headers']['Authorization'] = $Token.CreateAuthorizationHeader()
}