EasyLife365.psm1

#region _classes
class EL {
    [string]$Id
    [string]$DisplayName
    [object]$Metadata
    [guid]$TemplateId
    [guid]$PolicyId
    [object]$Policy
    
    EL ([object] $el) {
        $this.Id = $el.Id
        $this.DisplayName = $el.DisplayName
        $this.Metadata = $el.Extensions | Get-EasyMetaData
        $this.Policy = $el.Extensions | Get-EasyPolicyData
        if($this.Metadata.tId){
            $this.TemplateId = $this.Metadata.tId
        }
        if($this.Policy.pId){
            $this.PolicyId = $this.Policy.pId
        }
    }
    
    [void] NewTemplateId([guid]$tId,$type) {
        $this.Metadata = [PSCustomObject]@{ 
            tId = $tId 
        } 
        $metadataHash = ($this.Metadata | ConvertTo-EasyHashtable)

        if($type -eq 'ELGuestUser'){
            Write-Verbose "New-MgUserExtension userId: $($this.id) metadata: $($metadataHash | ConvertTo-Json -Compress)"
            New-MgUserExtension -UserId $this.Id -Id $script:elMetadataExtension -AdditionalProperties $metadataHash -ErrorAction Stop
        } elseif($type -eq 'ELGroup'){
            Write-Verbose "New-MgUserExtension groupId: $($this.id) metadata: $($metadataHash | ConvertTo-Json -Compress)"
            New-MgGroupExtension -GroupId $this.Id -Id $script:elMetadataExtension -AdditionalProperties $metadataHash -ErrorAction Stop
        }
    }

    [void] SetTemplateId([guid]$tId,$type) {
        $this.Metadata.tId = $tId
        $metadataHash = ($this.Metadata | ConvertTo-EasyHashtable)

        if($type -eq 'ELGuestUser'){
            Write-Verbose "Update-MgUserExtension userId: $($this.id) metadata: $($metadataHash | ConvertTo-Json -Compress)"
            Update-MgUserExtension -UserId $this.Id -ExtensionId $script:elMetadataExtension -AdditionalProperties $metadataHash -ErrorAction Stop
        } elseif($type -eq 'ELGroup'){
            Write-Verbose "Update-MgUserExtension groupId: $($this.id) metadata: $($metadataHash | ConvertTo-Json -Compress)"
            Update-MgGroupExtension -GroupId $this.Id -ExtensionId $script:elMetadataExtension -AdditionalProperties $metadataHash -ErrorAction Stop
        }
    }

    [void] NewPolicyId([guid]$polId,$type) {
        $this.PolicyId = $polId

        if($type -eq 'ELGuestUser'){
            Write-Verbose "New-MgUserExtension userId: $($this.id) pId: $($this.PolicyId)"
            New-MgUserExtension -UserId $this.Id -Id $script:elPolicyExtension -AdditionalProperties @{
                pId = $this.PolicyId
            } -ErrorAction Stop
        } elseif($type -eq 'ELGroup'){
            Write-Verbose "New-MgGroupExtension groupId: $($this.id) pId: $($this.PolicyId)"
            New-MgGroupExtension -GroupId $this.Id -Id $script:elPolicyExtension -AdditionalProperties @{
                pId = $this.PolicyId
            } -ErrorAction Stop
        }
    }

    [void] SetPolicyId([guid]$polId,$type) {
        $this.PolicyId = $polId

        if($type -eq 'ELGuestUser'){
            Write-Verbose "Update-MgUserExtension userId: $($this.id) pId: $($this.PolicyId)"
            Update-MgUserExtension -UserId $this.Id -ExtensionId $script:elPolicyExtension -AdditionalProperties @{
                pId = $this.PolicyId
            } -ErrorAction Stop
        } elseif($type -eq 'ELGroup'){
            Write-Verbose "Update-MgGroupExtension groupId: $($this.id) pId: $($this.PolicyId)"
            Update-MgGroupExtension -GroupId $this.Id -ExtensionId $script:elPolicyExtension -AdditionalProperties @{
                pId = $this.PolicyId
            } -ErrorAction Stop
        }
    }

    [void] ClearPolicyId($type) {
        $this.PolicyId = [guid]::Empty

        if($type -eq 'ELGuestUser'){
            $this.AccountDisabledState = $null
            $this.AccountDisabledDate = $null
            $this.AccountDisabledStepChange = $null
            $this.AccountDisabledStep = $null
            $this.NoOwnersEscalation = $null
            $this.MinimumOwnerStepChange = $null
            $this.MinimumOwnerStep = $null
            $this.MinimumOwnerState = $null
            $this.ConfirmationStepChange = $null
            $this.ConfirmationStep = $null
            $this.ConfirmationState = $null
            $this.ConfirmationDate = $null
            $this.InactivityStepChange = $null
            $this.InactivityStep = $null
            $this.InactivityState = $null
            $this.InactivityDate = $null
            $this.InvitationStepChange = $null
            $this.InvitationStep = $null
            $this.InvitationState = $null
            $this.InvitationDate = $null
            $this.NamingState = $null
            $this.ClassificationState = $null
            $this.ClassificationDate = $null
            $this.ClassificationStepChange = $null
            $this.ClassificationStep = $null

            Write-Verbose "Remove-MgUserExtension userId: $($this.id) extensionId: cloud.easyLife.policy "
            Remove-MgUserExtension -UserId $this.Id -ExtensionId $script:elPolicyExtension -ErrorAction Stop
        } elseif($type -eq 'ELGroup'){
            $this.AccessReviewPolicyState = $null
            $this.ConfirmationPolicyState = $null
            $this.ExpirationPolicyState = $null
            $this.OwnerPolicyState = $null
            $this.NoOwnerEscalation = $null
            $this.IsArchived = $null

            Write-Verbose "Remove-MgGroupExtension groupId: $($this.id) extensionId: cloud.easyLife.policy "
            Remove-MgGroupExtension -GroupId $this.Id -ExtensionId $script:elPolicyExtension -ErrorAction Stop
        }
    }

    [void] SetMetadata([hashtable]$metadata,$type) {
        $metadataHash = ($this.Metadata | ConvertTo-EasyHashtable)
        foreach($key in $metadata.Keys){
            if($metadataHash.contains($key)){
                $metadataHash[$key] = $metadata[$key]
            } else {
                $metadataHash.Add($key,$metadata[$key])
            }
        }
        $this.Metadata = $metadataHash
        if($type -eq 'ELGuestUser'){
            Write-Verbose "Update-MgUserExtension userId: $($this.id) metadata: $($metadataHash | ConvertTo-Json -Compress)"
            Update-MgUserExtension -UserId $this.Id -ExtensionId $script:elMetadataExtension -AdditionalProperties $metadataHash -ErrorAction Stop
        } elseif($type -eq 'ELGroup'){
            Write-Verbose "Update-MgUserExtension groupId: $($this.id) metadata: $($metadataHash | ConvertTo-Json -Compress)"
            Update-MgGroupExtension -GroupId $this.Id -ExtensionId $script:elMetadataExtension -AdditionalProperties $metadataHash -ErrorAction Stop
        }
    }
}

class ELGuestUser : EL {
    [string]$PrimaryOwner
    [string]$SecondaryOwner
    [string]$Mail
    [string]$CompanyName
    [string]$GivenName
    [string]$Surname
    [bool]$IsCompliant
    [string]$AccountDisabledState
    [string]$AccountDisabledDate
    [string]$AccountDisabledStepChange
    [string]$AccountDisabledStep
    [string]$NoOwnersEscalation
    [string]$MinimumOwnerStepChange
    [string]$MinimumOwnerStep
    [string]$MinimumOwnerState
    [string]$ConfirmationStepChange
    [string]$ConfirmationStep
    [string]$ConfirmationState
    [string]$ConfirmationDate
    [string]$InactivityStepChange
    [string]$InactivityStep
    [string]$InactivityState
    [string]$InactivityDate
    [string]$InvitationStepChange
    [string]$InvitationStep
    [string]$InvitationState
    [string]$InvitationDate
    [string]$NamingState
    [string]$ClassificationState
    [string]$ClassificationDate
    [string]$ClassificationStepChange
    [string]$ClassificationStep

    ELGuestUser ([object] $elGuestUser) : base($elGuestUser) {
        $this.Mail = $elGuestUser.Mail
        $this.CompanyName = $elGuestUser.CompanyName
        $this.GivenName = $elGuestUser.GivenName
        $this.Surname = $elGuestUser.Surname
        $pOwner = $elGuestUser.AdditionalProperties.easylife365_usermetadata.primaryOwner
        $sOwner = $elGuestUser.AdditionalProperties.easylife365_usermetadata.secondaryOwner
        if($pOwner){
            $this.PrimaryOwner = (Get-MgUser -UserId $pOwner -Select UserPrincipalName).UserPrincipalName
        }
        if($sOwner){
            $this.SecondaryOwner = (Get-MgUser -UserId $sOwner -Select UserPrincipalName).UserPrincipalName
        }
        $this.IsCompliant = $elGuestUser.AdditionalProperties.easylife365_usermetadata.isCompliant
        $this.AccountDisabledState = $this.Policy.adSta
        $this.AccountDisabledDate = $this.Policy.adCh
        $this.AccountDisabledStep = $this.Policy.adSt
        $this.AccountDisabledStepChange = $this.Policy.adStCh
        $this.NoOwnersEscalation = $this.Policy.noEsca
        $this.MinimumOwnerState = $this.Policy.owSta
        $this.MinimumOwnerStep = $this.Policy.owSt
        $this.MinimumOwnerStepChange = $this.Policy.owStCh
        $this.ConfirmationState = $this.Policy.coSta
        $this.ConfirmationDate = $this.Policy.coCh
        $this.ConfirmationStep = $this.Policy.coSt
        $this.ConfirmationStepChange = $this.Policy.coStCh
        $this.InactivityState = $this.Policy.inSta
        $this.InactivityDate = $this.Policy.inCh
        $this.InactivityStep = $this.Policy.inSt
        $this.InactivityStepChange = $this.Policy.inStCh
        $this.InvitationState = $this.Policy.invSta
        $this.InvitationDate = $this.Policy.invCh
        $this.InvitationStep = $this.Policy.invSt
        $this.InvitationStepChange = $this.Policy.invStCh
        $this.NamingState = $this.Policy.naSta
        $this.ClassificationState = $this.Policy.clSta
        $this.ClassificationDate = $this.Policy.clCh
        $this.ClassificationStep = $this.Policy.clSt
        $this.ClassificationStepChange = $this.Policy.clStCh
    }
}

class ELGroup : EL {
    [string]$Description
    [string]$Mail
    [bool]$IsArchived
    [string[]]$GroupTypes
    [string[]]$Owner
    [string]$AccessReviewPolicyState
    [string]$AccessReviewPolicyChanged
    [string]$ConfirmationPolicyState
    [string]$ConfirmationPolicyChanged
    [string]$ExpirationPolicyState
    [string]$ExpirationPolicyChanged
    [string]$OwnerPolicyState
    [string]$OwnerPolicyChanged
    [string]$NoOwnerEscalation

    ELGroup ([object] $ELGroup) : base($ELGroup) {
        $this.Description = $ELGroup.Description
        $this.Mail = $ELGroup.Mail
        $this.GroupTypes = $ELGroup.GroupTypes
        $this.Owner = $ELGroup | Get-EasyGroupOwner
        $this.AccessReviewPolicyState = $this.Policy.arSta
        $this.AccessReviewPolicyChanged = $this.Policy.arCh
        $this.ConfirmationPolicyState = $this.Policy.coSta
        $this.ConfirmationPolicyChanged = $this.Policy.coCh
        $this.ExpirationPolicyState = $this.Policy.exSta
        $this.ExpirationPolicyChanged = $this.Policy.exCh
        $this.OwnerPolicyState = $this.Policy.owSta
        $this.OwnerPolicyChanged = $this.Policy.owCh
        $this.NoOwnerEscalation = $this.Policy.noEsca
        $this.IsArchived = $this.Policy.isArchived
    }
}

class ELTelemetry {
    [string]$name = 'Microsoft.ApplicationInsights.Event'
    [datetime]$time = [datetime]::UtcNow
    [guid]$iKey = $elAiInstaceKey
    [hashtable]$tags = @{'ai.cloud.roleInstance' = (Get-MgContext).TenantId}
    [hashtable]$data

    ELTelemetry() {
    }

    ELTelemetry([hashtable]$d) {
        $this.data = $d
    }
    
    ELTelemetry([guid]$k,[hashtable]$t,[hashtable]$d) {
        $this.iKey = $k
        $this.tags = $t
        $this.data = $d
    }

    [string] ToJson(){
        return ($this | ConvertTo-Json -Compress -Depth 100)
    }
}
#endregion _classes

#region _constants

New-Variable -Name elPolicyExtension -Option Constant -Value 'cloud.easyLife.policy'
New-Variable -Name elMetadataExtension -Option Constant -Value 'cloud.easyLife.metadata'
New-Variable -Name elPowerShellClientId -Option Constant -Value '14b899c6-0b3a-4f59-8807-e9df5fb0fb1e'
New-Variable -Name elAiInstaceKey -Option Constant -Value '71103f09-7433-40bb-b664-e851ce5bf6b1'
New-Variable -Name elRequiredScopes -Option Constant -Value @{
    'Get-EasyGroup' = @('User.ReadWrite.All', 'Group.ReadWrite.All')
    'Get-EasyTeam' = @('User.ReadWrite.All', 'Group.ReadWrite.All')
    'Get-EasyGuestUser' = @('User.ReadWrite.All', 'Group.ReadWrite.All')
}
#endregion _constants

#region ConvertTo-EasyHashtable
function ConvertTo-EasyHashtable { 
    param ( 
        [Parameter(Mandatory,ValueFromPipeline)] 
        [object]$inputObject 
    )
    $output = @{}; 
    $inputObject | Get-Member -MemberType *Property | ForEach-Object {
        $output.($_.name) = $inputObject.($_.name); 
    }
    return  $output;
}
#endregion ConvertTo-EasyHashtable

#region Get-EasyGroupOwner
function Get-EasyGroupOwner {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline)]
        [object]
        $inputObject
    )
    process {
        $ownerProperty = (Get-MgGroupOwner -GroupId $InputObject.id -select userPrincipalName).AdditionalProperties
        if($ownerProperty){
            $ownerProperty | ForEach-Object {
                (New-Object -TypeName pscustomobject -Property $_).userPrincipalName
            }
        }
    }
}
#endregion Get-EasyGroupOwner

#region Get-EasyMetaData
function Get-EasyMetaData {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline)]
        [object]
        $inputObject
    )
    process {
        $objParam = @{
            TypeName = 'pscustomobject'
            Property = $inputObject.where{$_.id -eq $script:elMetadataExtension}.AdditionalProperties
        }
        if($objParam.Property){
            New-Object @objParam | Select-Object -ExcludeProperty '@odata.type','extensionName'
        }
    }
}
#endregion Get-EasyMetaData

#region Get-EasyPolicyData
function Get-EasyPolicyData {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline)]
        [object]
        $inputObject
    )
    process {
        $objParam = @{
            TypeName = 'pscustomobject'
            Property = $inputObject.where{$_.id -eq $script:elPolicyExtension}.AdditionalProperties
        }
        if($objParam.Property){
            New-Object @objParam | Select-Object -ExcludeProperty '*@odata.type','extensionName'
        }
    }
}
#endregion Get-EasyPolicyData

#region Get-EasyTeamsActivity

function Get-EasyTeamsActivity {
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateSet('D7', 'D30', 'D90', 'D180')]
        $Period = 'D30',
        [Parameter()]
        [ValidateSet('ActivityDetail','ActivityCounts','ActivityDistributionCounts')]
        $Report = 'ActivityDetail'
    )
    process {
        $uri = "https://graph.microsoft.com/beta/reports/getTeamsTeam{0}(period='{1}')?`$format=application/json" -f $Report, $Period
        $res = Invoke-MgGraphRequest -Uri $uri
        $res.value
    }
}
#endregion Get-EasyTeamsActivity

#region Get-EasyUserId
function Get-EasyUserId {
    [CmdletBinding()]
    param (
        $userId
    )
    process {
        try {
            (Get-MgUser -UserId $userId -ErrorAction Stop).Id
        }
        catch {
            Write-Warning "Could not find user with id: $userId"
        }
    }
}
#endregion Get-EasyUserId

#region New-EasyTelemetryData
function New-EasyTelemetryData {
    param(
        [Parameter()]
        [ValidateSet('Event','Exception')]
        $type
    )
    switch($type){
        'Event' {
            @{
                baseType = "EventData"
                baseData = @{
                    ver = 2
                    name = $name
                    properties = @{
                        moduleVersion = $moduleVersion
                        commandLine = $commandLine
                        duration = $duration
                    }
                }
            }
        }
        'Exception' {
            @{
                baseType = "ExceptionData"
                baseData = @{
                    ver = 2
                    handledAt = "UserCode"
                    properties = @{
                        moduleVersion = $moduleVersion
                        commandLine = $commandLine
                        duration = $duration
                    }
                    exceptions = @()
                }
            }
        }
    }
}
#endregion New-EasyTelemetryData

#region Send-EasyTelemetryEvent
function Send-EasyTelemetryEvent {
    [CmdletBinding()]
    param(
        [string]
        $IngestionEndpoint = 'https://westeurope-5.in.applicationinsights.azure.com',
        [ELTelemetry]
        $Body,
        [switch]
        $PassThru
    )
    process {
        if($EasyLife365DisableTelemetry){
            Write-Verbose "Sending telemetry is disabled on this system."
        } else {
            $Body.time = [datetime]::UtcNow
            $null = Invoke-RestMethod -Uri "$IngestionEndpoint/v2/track" -Method 'POST' -UseBasicParsing -Body ($Body.ToJson())
            if($PassThru){
                $Body
            }
        }
    }
}
#endregion Send-EasyTelemetryEvent

#region Set-EasyGroup
function Set-EasyGroup {
    <#
    .SYNOPSIS
        Set EasyLife properties for group objects through the Graph API.
    .DESCRIPTION
        This function uses Set-MgGroup and Set-MgGroupExtension to set EasyLife properties, such as PolicyId and TemplateId, of groups-based objects through the Graph API.
    .NOTES
        The alias Set-EasyTeam can be used.
    .LINK
        https://docs.easylife365.cloud
    .EXAMPLE
        Get-EasyGroup -Id 0b158de6-5fe8-49d9-81bb-22c23860d3a4 | Set-EasyGroup -PolicyId 2570b1bb-50bc-41a9-9d38-85888e8a04a1
         
        This example sets the EasyLife policy to 2570b1bb-50bc-41a9-9d38-85888e8a04a1 for the Group with the Id 0b158de6-5fe8-49d9-81bb-22c23860d3a4.
        To get the id of a policy, open the policy in the EasyLife cockpit and copy the guid from the URL.
    .EXAMPLE
        Set-EasyGroup -Id 0b158de6-5fe8-49d9-81bb-22c23860d3a4 -TemplateId 51436a74-87aa-43d8-8139-928a26a33e80
         
        This example sets the EasyLife policy to 51436a74-87aa-43d8-8139-928a26a33e80 for the Group with the Id 0b158de6-5fe8-49d9-81bb-22c23860d3a4
        To get the id of a template, open the template in the EasyLife cockpit and copy the guid from the URL.
    #>

    [CmdletBinding(DefaultParameterSetName='byGroupId',SupportsShouldProcess)]
    param (
        [Parameter(Mandatory,ParameterSetName='byGroupId')]
        [ValidateNotNullOrEmpty()]
        [string]$GroupId,
        [Parameter(ParameterSetName='inputObject',ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [object]$InputObject,
        [string]$DisplayName,
        [guid]$PolicyId,
        [guid]$TemplateId,
        [hashtable]$Metadata,
        [switch]$PassThru
    )
    begin {
        Test-EasyCmdletRequieredScopes -cmdlet $MyInvocation.InvocationName 
    }
    process {
        if(-not($InputObject)){
            $InputObject = Get-EasyGroup -Id $GroupId
        }
        $bodyParameter = @{}
        switch($PSBoundParameters.Keys){
            'DisplayName' {
                $bodyParameter.DisplayName = $DisplayName
            }
            'PolicyId' {
                $InputObject | Set-EasyPolicyId -PolicyId $PolicyId
            }
            'TemplateId' {
                $InputObject | Set-EasyTemplateId -TemplateId $TemplateId
            }
            'Metadata' {
                $InputObject | Set-EasyMetadata -Metadata $Metadata
            }
        }
        if($bodyParameter.Keys -ne '') {
            if($PSCmdlet.ShouldProcess("Updating Group $($InputObject.DisplayName) with $($bodyParameter| ConvertTo-Json -Compress)",$null,$null)) {
                Update-MgGroup -GroupId $InputObject.Id -BodyParameter $bodyParameter
            }
        }
        if($PassThru){
            $InputObject
        }
    }
}
# New-Alias -Name Set-EasyTeam -Value Set-EasyGroup
#endregion Set-EasyGroup

#region Set-EasyGuestUser
function Set-EasyGuestUser {
    <#
    .SYNOPSIS
        Set EasyLife properties of (guest) user accounts through the Graph API.
    .DESCRIPTION
        This function uses Set-MgUser and Set-MgUserExtension to set EasyLife properties such as owner, metadata, and policy. This function does not differentiate between users and guest users, use with caution.
    .NOTES
        None.
    .LINK
        https://docs.easylife365.cloud
    .EXAMPLE
        Get-EasyGuestUser -Id 0cefd2d0-ae5a-4ca3-98e9-e8df5418226c | Set-EasyGuestUser -PolicyId 87954648-18f5-4091-93fb-db0d38eca208
         
        This example sets the EasyLife policy to 87954648-18f5-4091-93fb-db0d38eca208 for the user with the Id 0cefd2d0-ae5a-4ca3-98e9-e8df5418226c.
        To get the id of a policy, open the policy in the EasyLife cockpit and copy the guid from the URL.
    .EXAMPLE
       Set-EasyGuestUser -Id 0cefd2d0-ae5a-4ca3-98e9-e8df5418226c -TemplateId 33373f13-8669-40de-b6da-b4a2b0a55e83
         
        This example sets the EasyLife template to 33373f13-8669-40de-b6da-b4a2b0a55e83 for the user with the Id 0cefd2d0-ae5a-4ca3-98e9-e8df5418226c.
        To get the id of a tempate, open the template in the EasyLife cockpit and copy the guid from the URL.
    #>

    [CmdletBinding(DefaultParameterSetName='byUserId',SupportsShouldProcess)]
    param (
        [Parameter(Mandatory,ParameterSetName='byUserId')]
        [ValidateNotNullOrEmpty()]
        [string]$UserId,
        [Parameter(ParameterSetName='inputObject',ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [object]$InputObject,
        [string]$DisplayName,
        [guid]$PolicyId,
        [guid]$TemplateId,
        [hashtable]$Metadata,
        [switch]$PassThru
    )
    begin {
        Test-EasyCmdletRequieredScopes -cmdlet $MyInvocation.InvocationName 
    }
    process {
        if(-not($InputObject)){
            $InputObject = Get-EasyGuestUser -Id $UserId
        }
        $bodyParameter = @{}
        switch($PSBoundParameters.Keys){
            'DisplayName' {
                $bodyParameter.DisplayName = $DisplayName
            }
            'PolicyId' {
                $InputObject | Set-EasyPolicyId -PolicyId $PolicyId
            }
            'TemplateId' {
                $InputObject | Set-EasyTemplateId -TemplateId $TemplateId
            }
            'Metadata' {
                $InputObject | Set-EasyMetadata -Metadata $Metadata
            }
        }
        if($bodyParameter.Keys -ne '') {
            if($PSCmdlet.ShouldProcess("Updating User $($InputObject.DisplayName) with $($bodyParameter| ConvertTo-Json -Compress)",$null,$null)) {
                Update-MgUser -UserId $InputObject.Id -BodyParameter $bodyParameter
            }
        }
        if($PassThru){
            $InputObject
        }
    }
}
#endregion Set-EasyGuestUser

#region Set-EasyMetadata
function Set-EasyMetadata {
    [CmdletBinding(DefaultParameterSetName='user',SupportsShouldProcess)]
    param (
        [Parameter(Mandatory,ParameterSetName='user')]
        [ValidateNotNullOrEmpty()]
        $UserId,
        [Parameter(Mandatory,ParameterSetName='group')]
        [ValidateNotNullOrEmpty()]
        $GroupId,
        [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='inputObject')]
        [ValidateNotNullOrEmpty()]
        $InputObject,
        [Parameter(Mandatory)]
        [hashtable]
        $Metadata
    )
    begin {
        $sw,$diag = Start-EasyDiagnostics -Name $MyInvocation.InvocationName
    }
    process {
        switch($PSBoundParameters.Keys){
            'UserId'{
                $InputObject = Get-EasyGuestUser -Id $UserId
            }
            'GroupId'{
                $InputObject = Get-EasyGroup -Id $GroupId
            }
        }
        $type = $InputObject.GetType().Name
        Write-Verbose "InputObject is of type $type"
        if($InputObject.Metadata -eq $null){
            if($PSCmdlet.ShouldProcess("Add metadata extension to object $($InputObject.DisplayName)",$null,$null)){
                try {
                    $InputObject.NewTemplateId([guid]::Empty,$type)
                } catch {
                    Send-EasyTelemetryEvent -Type Exception -Message $_.Exception  -PassThru
                    $diag.success = $false
                }
            }
        } else {
            if($PSCmdlet.ShouldProcess("Set metadata for object $($InputObject.DisplayName)",$null,$null)){
                try{ 
                    $InputObject.SetMetadata($metadata,$type)
                } catch {
                    Send-EasyTelemetryEvent -Type Exception -Message $_.Exception -PassThru
                    $diag.success = $false
                }
            }
        }
    }
    end {
        $sw.Stop()
        $diag.Duration = $sw.ElapsedMilliseconds
        Send-EasyTelemetryEvent -Type Request -Request $diag -CommandLine $MyInvocation.Line
    }
}
#endregion Set-EasyMetadata

#region Set-EasyPolicyId
function Set-EasyPolicyId {
    [CmdletBinding(DefaultParameterSetName='user',SupportsShouldProcess)]
    param (
        [Parameter(Mandatory,ParameterSetName='user')]
        [ValidateNotNullOrEmpty()]
        $UserId,
        [Parameter(Mandatory,ParameterSetName='group')]
        [ValidateNotNullOrEmpty()]
        $GroupId,
        [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='inputObject')]
        [ValidateNotNullOrEmpty()]
        $InputObject,
        [Parameter(Mandatory)]
        [guid]
        $PolicyId
    )
    begin {
        $sw,$diag = Start-EasyDiagnostics -Name $MyInvocation.InvocationName
    }
    process {
        switch($PSBoundParameters.Keys){
            'UserId'{
                $InputObject = Get-EasyGuestUser -Id $UserId
            }
            'GroupId'{
                $InputObject = Get-EasyGroup -Id $GroupId
            }
        }
        $type = $InputObject.GetType().Name
        Write-Verbose "InputObject is of type $type"

        if($InputObject.PolicyId -eq [guid]::Empty){
            # input object does not have a policy, create policy attribute and set policy
            if($PSCmdlet.ShouldProcess("Add policyId extension with $PolicyId to object $($InputObject.DisplayName)",$null,$null)){
                try {
                    $InputObject.newPolicyId($PolicyId,$type)
                } catch {
                    Send-EasyTelemetryEvent -Type Exception -Message $_.Exception -PassThru
                    $diag.success = $false
                }   
            }
        } else {
            if($PolicyId -eq [guid]::Empty){
                # input object has a policy, assume user wants to clear it, remove policy attribute
                if($PSCmdlet.ShouldProcess("Set policyId to $PolicyId for object $($InputObject.DisplayName)",$null,$null)){
                    try {
                        $InputObject.ClearPolicyId($type)
                    } catch {
                        Send-EasyTelemetryEvent -Type Exception -Message $_.Exception -PassThru
                        $diag.success = $false
                    }   
                }
            } else {
                # input object already has a policy, update policyId
                if($PSCmdlet.ShouldProcess("Set policyId to $PolicyId for object $($InputObject.DisplayName)",$null,$null)){
                    try {
                        $InputObject.setPolicyId($PolicyId,$type)
                    } catch {
                        Send-EasyTelemetryEvent -Type Exception -Message $_.Exception -PassThru
                        $diag.success = $false
                    }
                    
                }
            }
        }
    }
    end {
        $sw.Stop()
        $diag.Duration = $sw.ElapsedMilliseconds
        Send-EasyTelemetryEvent -Type Request -Request $diag -CommandLine $MyInvocation.Line
    }
}
#endregion Set-EasyPolicyId

#region Set-EasyTemplateId
function Set-EasyTemplateId {
    [CmdletBinding(DefaultParameterSetName='user',SupportsShouldProcess)]
    param (
        [Parameter(Mandatory,ParameterSetName='user')]
        [ValidateNotNullOrEmpty()]
        $UserId,
        [Parameter(Mandatory,ParameterSetName='group')]
        [ValidateNotNullOrEmpty()]
        $GroupId,
        [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='inputObject')]
        [ValidateNotNullOrEmpty()]
        $InputObject,
        [Parameter(Mandatory)]
        [guid]
        $TemplateId
    )
    begin {
        $sw,$diag = Start-EasyDiagnostics -Name $MyInvocation.InvocationName
    }
    process {
        switch($PSBoundParameters.Keys){
            'UserId'{
                $InputObject = Get-EasyGuestUser -Id $UserId
            }
            'GroupId'{
                $InputObject = Get-EasyGroup -Id $GroupId
            }
        }
        $type = $InputObject.GetType().Name
        Write-Verbose "InputObject is of type $type"
        if($InputObject.Metadata -eq $null){
            if($PSCmdlet.ShouldProcess("Add metadata extension to object $($InputObject.DisplayName)",$null,$null)){
                $InputObject.newTemplateId($TemplateId,$type)
            }
        } else {
            if($PSCmdlet.ShouldProcess("Set templateId to $TemplateId for object $($InputObject.DisplayName)",$null,$null)){
                $InputObject.setTemplateId($TemplateId,$type)
            }
        }
    }
    end {
        $sw.Stop()
        $diag.data.baseData.properties.duration = $sw.ElapsedMilliseconds
        $diag.data.baseData.properties.commandLine = $MyInvocation.Line
        Send-EasyTelemetryEvent -Body $diag
    }
}
#endregion Set-EasyTemplateId

#region Start-EasyDiagnostics

function Start-EasyDiagnostics {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$Name
    )
    process {
        [System.Diagnostics.Stopwatch]::StartNew()
        $diag = [ELTelemetry]::new()
        $diag.data = New-EasyTelemetryData -type Event
        $diag.data.baseData.name = $Name
        $diag.data.baseData.properties.moduleVersion = $MyInvocation.MyCommand.Module.Version.ToString()
        $diag
    }
}
#endregion Start-EasyDiagnostics

#region Test-EasyCmdletRequieredScopes
function Test-EasyCmdletRequieredScopes {
    [CmdletBinding()]
    param (
        $cmdlet
    )
    process {
        $scopes = (Get-MgContext).Scopes
        $requiredScopes = $elRequiredScopes[$cmdlet]
        $requiredScopes.foreach{
            if($scopes -notcontains $_){
                Write-Warning "Please connect to Microsoft Graph with the following scopes: $($requiredScopes -join ', ')"
                break
            }
        }
    }
}
#endregion Test-EasyCmdletRequieredScopes

#region Test-EasyLife365ModuleVersion
$findLatestModule = Start-Job -Scriptblock {
    Find-Module -Name EasyLife365 -Repository PSGallery
}
function Test-EasyLife365ModuleVersion {
    [CmdletBinding()]
    param()
    process {
        $importedModule = Get-Module EasyLife365
        $latestModule = $findLatestModule | Wait-Job | Receive-Job -Keep
        $latestVersion = $latestModule.version
        $importedVersion = $importedModule.Version
        Write-Verbose "You have imported module version [$importedVersion] the latest version available on the PowerShell Gallery is [$latestVersion]"
        if($latestVersion -gt $importedVersion){ 
            $messageData = [System.Management.Automation.HostInformationMessage]@{
                Message = "`r`nYou are using an outdated version of the module, please update to version [$($latestModule.Version)] by typing: Update-Module EasyLife365`r`n"; 
                ForegroundColor = 'Yellow'
                BackgroundColor = $Host.UI.RawUI.BackgroundColor
            }
            Write-Information -MessageData $messageData -InformationAction Continue 
        }
    } 
}
#endregion Test-EasyLife365ModuleVersion

#region Connect-EasyLife365
function Connect-EasyLife365 {
    <#
    .SYNOPSIS
        Connect to EasyLife 365.
    .DESCRIPTION
        This function uses the Microsoft.Graph.Authentication module to connect to the Graph API with the scopes: User.ReadWrite.All, Group.ReadWrite.All.
    .NOTES
        Currently, this fuction only connects to the Graph API. Connection to the EasyLife API is not yet available.
        You need to have permissions to consent the use of Microsoft Graph PowerShell in your Azure AD.
    .LINK
        https://docs.easylife365.cloud/docs/add-ons/powershell/connect-easylife365/
    .EXAMPLE
        Connect-EasyLife365
         
        This example connects to the Graph API using the scopes User.ReadWrite.All, Group.ReadWrite.All.
    #>

    [CmdletBinding(HelpUri = 'https://docs.easylife365.cloud/docs/add-ons/powershell/connect-easylife365/')]
    param (
        [Parameter()]
        [switch]
        $Identity
    )
    begin {
        $sw,$diag = Start-EasyDiagnostics -Name $MyInvocation.InvocationName
    }
    process {
        $graphParam = @{
            ApplicationId = $elPowerShellClientId
        }
        if($Identity){
            $graphParam.add('Identity',[switch]::present)
        } else {
            $graphParam.add('scopes', 'User.ReadWrite.All, Group.ReadWrite.All')
        }
        Connect-MgGraph @graphParam
        if($?){
            $ctx=Get-MgContext
            Write-Information "`r`nWelcome to EasyLife $($ctx.Account), you are connected to the tenant $($ctx.TenantId) via $($ctx.AppName) with ClientId $($ctx.ClientId).`r`n" -InformationAction Continue
        }
        Test-EasyLife365ModuleVersion
        $sw.Stop()
        $diag.data.baseData.properties.duration = $sw.ElapsedMilliseconds
        $diag.data.baseData.properties.commandLine = $MyInvocation.Line
        Send-EasyTelemetryEvent -Body $diag
    }
}
#endregion Connect-EasyLife365

#region Get-EasyGroup
function Get-EasyGroup {
    <#
    .SYNOPSIS
        Get EasyLife groups from the Graph API.
    .DESCRIPTION
        This function uses Get-MgGroup to retrieve groups from the Graph API. It expands properties relevant to EasyLife such as policy states and metadata.
    .NOTES
        If you use the alias Get-EasyTeam to invoke this function, the filter is updated and only Groups with resourceProvisioningOptions set to Team are returned.
    .LINK
        https://docs.easylife365.cloud/docs/add-ons/powershell/get-easygroup/
    .EXAMPLE
        Get-EasyGroup
         
        This example returns up to 100 groups and their properties.
    .EXAMPLE
        Get-EasyGroup -All
         
        This example returns all groups and their properties.
    .EXAMPLE
        Get-EasyGroup -Top 3
         
        This example returns 3 groups and their properties.
    .EXAMPLE
        Get-EasyGroup -DisplayName Project
         
        This example returns all groups that have a DisplayName that starts with Project.
    #>

    [CmdletBinding(DefaultParameterSetName='byDisplayName', HelpUri = 'https://docs.easylife365.cloud/docs/add-ons/powershell/get-easygroup/')]
    param (
        [Parameter(ParameterSetName='byId')]
        [ValidateNotNullOrEmpty()]
        [string]$Id,
        [Parameter(ParameterSetName='byDisplayName')]
        [ValidateNotNullOrEmpty()]
        [string]$DisplayName,
        [Parameter(ParameterSetName='byDisplayName')]
        [Parameter(ParameterSetName='all')]
        [int]$Top,
        [Parameter(ParameterSetName='all')]
        [switch]$All
    )
    begin {
        Test-EasyCmdletRequieredScopes -cmdlet $MyInvocation.InvocationName 
        $sw,$diag = Start-EasyDiagnostics -Name $MyInvocation.InvocationName
    }
    process{
        $mgGroupParam = @{
            Filter = "groupTypes/any(c:c eq 'Unified')" 
            ExpandProperty = "extensions(`$filter = startsWith(id, 'cloud.easyLife'))"
            Select = 'id','displayName','description','mail','groupTypes','extensions'
        }
        # if the alias Get-EasyTeams is used, update filter to only retrieve teams
        if ($MyInvocation.InvocationName -like 'Get-EasyTeam') {
            $mgGroupParam.Filter += " and resourceProvisioningOptions/Any(x:x eq 'Team')" 
        }
        if($Id){
            $mgGroupParam.Remove('Filter')
            $mgGroupParam.Add('GroupId',$Id)
        }
        if($DisplayName){
            $mgGroupParam.Filter += " and startsWith(DisplayName,'$DisplayName')"
        }
        if($Top){
            $mgGroupParam.add('Top',$Top)
        }
        if($All){
            $mgGroupParam.add('All',([switch]::Present))
        }
        Write-Verbose "Invoking Get-MgGroup with $($mgGroupParam | ConvertTo-Json -Compress)"
        Get-MgGroup @mgGroupParam | ForEach-Object {
            [ELGroup]::new($_)
        }
    }
    end {
        $sw.Stop()
        $diag.data.baseData.properties.duration = $sw.ElapsedMilliseconds
        $diag.data.baseData.properties.commandLine = $MyInvocation.Line
        Send-EasyTelemetryEvent -Body $diag
    }
}
New-Alias -Name Get-EasyTeam -Value Get-EasyGroup
#endregion Get-EasyGroup

#region Get-EasyGuestUser
function Get-EasyGuestUser {
    <#
    .SYNOPSIS
        Get EasyLife guest user accounts from the Graph API.
    .DESCRIPTION
        This function uses Get-MgUser to retrieve guest user accounts from the Graph API. It expands properties relevant to EasyLife such as owners and metadata.
    .NOTES
        None.
    .LINK
        https://docs.easylife365.cloud/docs/add-ons/powershell/get-easyguestuser/
    .EXAMPLE
        Get-EasyGuestUser
         
        This example returns up to 100 guest user accounts and their properties.
    .EXAMPLE
        Get-EasyGuestUser -All
         
        This example returns all guest user accounts and their properties.
    .EXAMPLE
        Get-EasyGuestUser -Top 3
         
        This example returns 3 guest user accounts and their properties.
    .EXAMPLE
        Get-EasyGuestUser -DisplayName Thomas
         
        This example returns all guest user accounts that have a DisplayName that starts with Thomas.
    #>

    [CmdletBinding(DefaultParameterSetName='byDisplayName', HelpUri = 'https://docs.easylife365.cloud/docs/add-ons/powershell/get-easyguestuser/')]
    param (
        [Parameter(ParameterSetName='byId')]
        [ValidateNotNullOrEmpty()]
        [string]$Id,
        [Parameter(ParameterSetName='byDisplayName')]
        [ValidateNotNullOrEmpty()]
        [string]$DisplayName,
        [Parameter(ParameterSetName='byDisplayName')]
        [Parameter(ParameterSetName='all')]
        [int]$Top,
        [Parameter(ParameterSetName='all')]
        [switch]$All
    )
    begin {
        Test-EasyCmdletRequieredScopes -cmdlet $MyInvocation.InvocationName 
        $sw,$diag = Start-EasyDiagnostics -Name $MyInvocation.InvocationName
        
        $mgUserParam = @{
            Filter = "userType eq 'Guest'" 
            ExpandProperty = "extensions(`$filter = startsWith(id, 'cloud.easyLife'))"
            Select = 'id','displayName','GivenName','Surname','CompanyName','mail','accountEnabled','easylife365_usermetadata','externalUserState','externalUserStateChangeDateTime','createdDateTime'
        }
        if($Id){
            $mgUserParam.Remove('Filter')
            $mgUserParam.Add('UserId',$Id)
        }
        if($DisplayName){
            $mgUserParam.Filter += " and startsWith(DisplayName,'$DisplayName')"
        }
        if($all){
            $mgUserParam.add('All',[switch]::Present)
        }
        if($top){
            $mgUserParam.add('Top',$Top)
        }
    }
    process {
        Write-Verbose "Invoking Get-MgUser with $($mgUserParam | ConvertTo-Json -Compress)"
        Get-MgUser @mgUserParam | ForEach-Object {
            [ELGuestUser]::new($_)
        }
    }
    end {
        $sw.stop()
        $diag.data.baseData.properties.duration = $sw.ElapsedMilliseconds
        $diag.data.baseData.properties.commandLine = $MyInvocation.Line
        Send-EasyTelemetryEvent -Body $diag
    }
}

#endregion Get-EasyGuestUser