IAMClient.psm1

function Add-ETHGroupMember {
    
    <#
    .SYNOPSIS
        Adds members to a IAM group
    .DESCRIPTION
        Adds one or more members to a IAM group
    .EXAMPLE
        Add-ETHGroupMember biol-micro-isg "aurels"
 
        Adds one member "aurels" to the group "biol-micro-isg"
    .EXAMPLE
        "somegroup","someothergroup","somethirdgroup" | Add-ETHGroupMember "aurels","jgrand"
 
        Adds multiple members to multiple groups
    .EXAMPLE
        Add-ETHGroupMember biol-micro-isg -Members "jgrand" -SubGroups "biol-micro-4sa"
 
        Add a members and a subgroup to the group "biol-micro-isg"
    .OUTPUTS
        PSCustomObject
 
        An object containing all accepted / rejected / failed members
    .FUNCTIONALITY
        Adding members to a group
    #>


    [CmdletBinding(SupportsShouldProcess = 1)]
    param (   
        [Parameter(Position = 0, Mandatory = 1, ValueFromPipeline = 1)]
        [string]$Identity,
        [Parameter(Position = 1)]
        [string[]]$Members = @(),
        [Parameter(Position = 2)]
        [string[]]$SubGroups = @()
    )

    BEGIN {
        # Check if client is initialized
        Test-IsIAMClientInitialized | Out-Null

        # Validate input arguments
        if (-not ($ExistingGroup = Get-ETHGroup -Identity $Identity)) {
            throw "Group $Identity was not found"
        }

        if ($Members.Count -le 0 -and $SubGroups.Count -le 0) {
            throw "No members nor subgroups specified"
        }

        $ToAddMembers = @($Members | Where-Object { $ExistingGroup.users -notcontains $_ })
        $ToAddSubGroups = @($SubGroups | Where-Object { $ExistingGroup.subgroups -notcontains $_ })

        if ($ToAddMembers.Count -eq 0 -and $ToAddSubGroups.Count -eq 0) {
            Write-Debug "No new members/subgroups added to group $Identity"
            return $ExistingGroup
        }

        $Body = @{
            users     = $ToAddMembers
            subgroups = $ToAddSubGroups
        }

    }

    PROCESS {
        if ($PSCmdlet.ShouldProcess($Identity)) {
            try {
                $result = Invoke-IAMMethod -Url "/groups/$Identity/members/add" -Method Put -Body $Body -Credentials $script:IAMCreds
            }
            catch {
                throw "Could not update group $Identity, Error: $_"
                return
            }
        }
    }

    END {
        Write-Debug "Added $($ToAddMembers.Count) Members to Group $Identity"
        return $result
    }
    
}


function Add-ETHMaillistMember {

    <#
    .SYNOPSIS
        Adds members to a IAM maillist
    .DESCRIPTION
        Adds one or more members to an IAM maillist
    .EXAMPLE
        Add-ETHMaillistMember biol-micro-list-aebi -Members "aurels"
 
        Adds one member "aurels" to the maillist "biol-micro-list-aebi"
    .EXAMPLE
        "aurels","jgrand" | Add-ETHMaillistMember "somegroup","someothergroup","somethirdgroup"
 
        Adds multiple users ("aurels" and "jgrand") to multiple maillists
 
    .OUTPUTS
        PSCustomObject
 
        An object containing all accepted / rejected / failed members
 
    .FUNCTIONALITY
        Adding members to a maillist
    #>


    param (
        # List Name
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Identity,

        # Member to add or remove
        [Parameter(Position = 1, Mandatory = $true)]
        [string[]]$Members
    )

    BEGIN {
        Test-IsIAMClientInitialized | Out-Null

        $Body = [PSCustomObject]@{
            users   = $Members;
        }
    }

    PROCESS {
        $Url = "/mailinglists/$Identity/members/add"

        return (Invoke-IAMMethod -Url $Url -Body $Body -Method Put -Credentials $script:IAMCreds)
    }


}


function Add-ETHUserITService {

    <#
    .SYNOPSIS
        Assigns a service to a user
         
    .DESCRIPTION
        Assigns an IT-Service to a user in IAM
 
    .PARAMETER Identity
        The user to add the service to
 
    .PARAMETER ITServiceName
        The name of the service to give to the user (Valid: Mailbox, WLAN_VPN, LDAP)
 
    .PARAMETER Body
        A psobject containing the properties to set directly when assigning the service.
        If you are not sure how to use this parameter, do not specify it and use `Set-ETHUserITService` or `Set-ETHUser` to set the properties afterwards!
 
    .EXAMPLE
        Add-ETHUserITService -Identity aurels -ITServiceName Mailbox
 
        Assigns the service Mailbox to the user 'aurels'
 
    .EXAMPLE
        "aurels","ausc" | Add-ETHUserITService -ITServiceName LDAP
 
        Assigns the service LDAP to the users 'aurels' and 'ausc'
     
    .LINK
        Set-ETHUser
 
    .LINK
        Set-ETHUserITService
 
    #>


    param (
        # ETH user name
        [Parameter(Position = 0, Mandatory = 1, ValueFromPipeline = $true)]
        [string]
        $Identity,

        # IT Service Name
        [Parameter(Position = 1, Mandatory = 1)]
        [string]
        $ITServiceName,

        # Body
        [Parameter(Position = 2)]
        [psobject]
        $Body
    )

    BEGIN {
        # Check if client is initialized
        Test-IsIAMClientInitialized | Out-Null

        if ($Body -eq $null) {
            $Body = @{ }
        }
    }

    PROCESS {
        return (Invoke-IAMMethod -Url "/users/$Identity/services/$ITServiceName" -Method POST -Credentials $script:IAMCreds -Body $Body)
    }
}



function Add-ETHUserMailAlias {
<#
    .SYNOPSIS
        Adds a new e-mail alias (proxyAddress) to a user's mailbox
         
    .DESCRIPTION
        Adds a new e-mail alias (proxyAddress) to a user's mailbox, this works by changing the main
        e-mail to the new alias and then back to the existing.
 
        !! WARNING: The Cmdlet currently does not reset the main e-mail address again, because IAM does not work (bug as of 14.02.2020) !!
 
    .PARAMETER Identity
        The user to add the alias to
 
    .PARAMETER Alias
        The alias(es) to give to the user
 
    .EXAMPLE
        Add-ETHUserMailAlias -Identity aurels -Alias "aurels.new@ethz.ch","my_cool_email@micro.biol.ethz.ch"
     
    #>


    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = 1)]
        [string]$Identity,

        [Parameter(Position = 1, Mandatory = 1, ValueFromPipeline = 1)]
        [string[]]$Alias
    )


    BEGIN {
        # Check if client is initialized
        $null = Test-IsIAMClientInitialized

        $User = Get-ETHUser -Identity $Identity
        $SavedEmail = $User.mail
    }

    PROCESS {
        foreach ($_alias in $Alias) {
            if ($User.proxyAdresses -icontains "smtp:$_alias" -or $User.mail -eq $_alias){
                # this alias already exists, nothing do to
                continue
            }
            $User.mail = $_alias

            $User = Set-ETHUser -Identity $Identity -User $User
        }  
    }

    END {
        if ($User.mail -ne $SavedEmail){
            # only reset e-mail when we actually changed aliases
            $User.mail = $SavedEmail
            Set-ETHUser -Identity $Identity -User $User
        }
    }
    
}


function Clear-ETHMaillistMember {

    <#
    .SYNOPSIS
        Removes all members from an IAM maillist
    .DESCRIPTION
        Removes all members from an IAM maillist
    .EXAMPLE
        Clear-ETHMaillistMember biol-micro-list-aebi
 
        Removes all members from the maillist "biol-micro-list-aebi"
     
    .OUTPUTS
        PSCustomObject
 
    .FUNCTIONALITY
        Clearing members of a maillist
    #>


    param (
        # List Name
        [Parameter(Position = 0, Mandatory = $true)]
        [string]$Identity
    )

    BEGIN {
        Test-IsIAMClientInitialized | Out-Null

        $Url = "/mailinglists/$Identity/members"
    }

    PROCESS {
        return (Invoke-IAMMethod -Url $Url -Method Delete -Credentials $script:IAMCreds)
    }
}


Function Compare-ObjectProperties {
    Param(
        [PSObject]$ReferenceObject,
        [PSObject]$DifferenceObject 
    )
    $objprops = $ReferenceObject | Get-Member -MemberType Property, NoteProperty | Select-Object -expand  Name
    $objprops += $DifferenceObject | Get-Member -MemberType Property, NoteProperty | Select-Object -expand Name
    $objprops = $objprops | Sort-Object -Unique
    $diffs = @()
    foreach ($objprop in $objprops) {
        $diff = Compare-Object $ReferenceObject $DifferenceObject -Property $objprop
        if ($diff) {            
            $diffprops = @{
                PropertyName = $objprop
                RefValue     = ($diff | Where-Object { $_.SideIndicator -eq '<=' } | Foreach-Object $($objprop))
                DiffValue    = ($diff | Where-Object { $_.SideIndicator -eq '=>' } | Foreach-Object $($objprop))
            }
            $diffs += New-Object PSObject -Property $diffprops
        }        
    }
    if ($diffs) { return ($diffs | Select-Object PropertyName, RefValue, DiffValue) }     
}

Function Get-ObjectDiffs {
    param (
        [PSObject]$ReferenceObject,
        [PSObject]$DifferenceObject
    )

    $ChangedProps = @{ }

    Compare-ObjectProperties $ReferenceObject $DifferenceObject | ForEach-Object {
        $ChangedProps.Add($_.PropertyName, $_.DiffValue);
    }

    return $ChangedProps
}

function Compare-GroupMembers {

    param (
        [string[]]$ExistingMembers,
        [string[]]$NewMembers
    )

    BEGIN {
        $ToAddMembers = @()
        $ToRemoveMembers = @()
        $ToKeepMembers = @()
    }

    PROCESS {
        if ($ExistingMembers.Count -eq 0) {
            # No existing members -> add all
            $ToAddMembers = $NewMembers
        }
        elseif ($NewMembers.Count -eq 0) {
            # No new members -> remove all existing
            $ToRemoveMembers = $ExistingMembers
        }
        else {
            # everything fine, we can run compare-object
            $ComparisionResult = Compare-Object -ReferenceObject $ExistingMembers -DifferenceObject $Members -IncludeEqual

            $ToAddMembers = ($ComparisionResult | Where-Object SideIndicator -eq "=>").InputObject
            $ToRemoveMembers = ($ComparisionResult | Where-Object SideIndicator -eq "<=").InputObject
            $ToKeepMembers = ($ComparisionResult | Where-Object SideIndicator -eq "==").InputObject
        }
    
        return [PSCustomObject]@{
            ToAdd    = $ToAddMembers;
            ToRemove = $ToRemoveMembers;
            ToKeep   = $ToKeepMembers;
        }

    }
}


function Get-ETHGroup {

    <#
    .SYNOPSIS
        Gets details of a group from IAM
    .DESCRIPTION
        Gets properties (and members) from a group in IAM
    .EXAMPLE
        Get-ETHGroup biol-micro-isg
 
        Gets the details of a group in iam.
 
    .OUTPUTS
        PSCustomObject
 
        An object with all properties of a group
 
    .FUNCTIONALITY
        Loading a group
    #>


    param (
        [CmdletBinding()]
        [Parameter(Position = 0, Mandatory = 1)]
        [string]$Identity
    )

    BEGIN {
        # Check if client is initialized
        Test-IsIAMClientInitialized | Out-Null

        $url = "/groups/$Identity"
    }

    PROCESS {
        return (Invoke-IAMMethod -Url $url -Method Get -Credentials $script:IAMCreds)
    }

    END {

    }    
}



function Get-ETHGroupMember {

    <#
    .SYNOPSIS
        Gets members of a group in IAM
    .DESCRIPTION
        Gets members of a group in IAM
    .EXAMPLE
        Get-ETHGroupMember biol-micro-isg
 
        Gets all members of the group biol-micro-isg
 
    .OUTPUTS
        String[]
 
        A list of user/group names that are members of the group
 
    .FUNCTIONALITY
        Getting members of a group
    #>


    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = 1, ValueFromPipelineByPropertyName = 1)]
        [string]$Identity
    )

    BEGIN {
        $Group = Get-ETHGroup -Identity $Identity
    }

    PROCESS {
        $ErrorActionPreference = 'Stop' #in order to go into catch
        try{
            $Members = ($Group | Select-Object -expand users)
        }catch{
            $Members = @()
        }
    }

    END {
        return $Members
    }

}


function Get-ETHMaillist {

    <#
    .SYNOPSIS
        Gets details of a maillist
    .DESCRIPTION
        Gets details of a IAM maillist, f.ex members, memberOf, mail, proxyAddresses, displayName, etc.
    .EXAMPLE
        Get-ETHMaillist biol-micro-aebi
 
        Gets details of the maillist biol-micro-aebi
     
    .OUTPUTS
        PSCustomObject
 
        An object with the AD properties of the maillist
 
    .FUNCTIONALITY
        Getting details for a maillist
    #>


    param (
        # List Name
        [Parameter(Position = 0, Mandatory = $true)]
        [string]$Identity
    )

    BEGIN {
        Test-IsIAMClientInitialized | Out-Null

        $Url = "/mailinglists/$Identity"
    }

    PROCESS {
        return (Invoke-IAMMethod -Url $Url -Method Get -Credentials $script:IAMCreds)
    }
}


function Get-ETHMaillistMember {

    <#
    .SYNOPSIS
        Gets members of a maillist
    .DESCRIPTION
        Gets a list of members for an IAM maillist.
 
        The property objectClass is either "group" or "user" depending on the location of the user/group in AD.
        If the AD Path is below the OU "OU=EthLists", then it is considered a "group", otherwise a "user".
 
        Execute Example 2 for more info.
 
    .EXAMPLE
        Get-ETHMaillistMember biol-micro-aebi
 
        Gets a list of all members of the maillist biol-micro-aebi.
         
    .EXAMPLE
        Get-ETHMaillistMember MICRO_ALL
 
        Gets a list of users and sub-maillists for the maillist "MICRO_ALL".
        Execute this example to understand the output.
     
    .OUTPUTS
        PSCustomObject
 
        An object with the three properties (Name, objectClass, distinguishedName)
 
    .FUNCTIONALITY
        Getting members of a maillist
    #>


    [CmdletBinding()]
    param (
        # List Name
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Identity
    )

    BEGIN { }

    PROCESS {
        
        $membersCn = (Get-ETHMaillist -Identity $Identity).members

        foreach ($member in $membersCn) {
            $Name = Convert-CnToName $member
        
            # determine if the member is a user or a mailinglist
            if ($member -like "*OU=EthLists,*") {
                $objectClass = "group"
            }
            else {
                $objectClass = "user"
            }
            
            # create simple return object
            [pscustomobject]@{
                name              = $Name;
                objectClass       = $objectClass;
                distinguishedName = $member; 
            }
        }
    }
}


function Get-ETHPerson {

    <#
    .SYNOPSIS
        Gets details of a person(a) from IAM
    .DESCRIPTION
        Gets properties of a person(a) from IAM, does not work for unames.
 
        Properties that will be loaded:
 
        - firstname
        - familyname
        - persid
        - npid
        - email
        - orcid
        - primary_username
        - primary_perskat
        - usernames
        - perskats
 
    .EXAMPLE
        Get-ETHPerson aurels
 
        Gets the details of the person 'aurels'
 
    .EXAMPLE
        Get-ETHPerson ti03388
 
        Gets the details of the person 'aurels'
 
    .OUTPUTS
        PSCustomObject
 
        An object with all properties of a person
 
    .FUNCTIONALITY
        Loading a person
    #>


    param (
        [Parameter(Position = 0, Mandatory = 1)]
        [string]$Identity
    )

    BEGIN {
        $url = "/users/$Identity"

        # is client initialized?
        Test-IsIAMClientInitialized | Out-Null
    }

    PROCESS {
        return (Invoke-IAMMethod -Url $url -Method Get -Credentials $script:IAMCreds)
    }

    END {

    }
    
}


function Get-ETHPersonServices {

    <#
    .SYNOPSIS
        Gets all services of a user (DEPRECATED)
    .DESCRIPTION
        (DEPRECATED, see Get-ETHUserServices)
 
        Loads all services of the specified user / person from IAM with their expiry dates.
 
    .EXAMPLE
        Get-ETHPersonServices aurels
 
        Gets the services of the person 'aurels'
 
    .EXAMPLE
        Get-ETHPersonServices ausc
 
        Gets the details of the uname 'ausc'
 
    .OUTPUTS
        PSCustomObject[]
 
        An array of all services of the user
 
    .FUNCTIONALITY
        Loading services
    #>



    [CmdLetBinding()]
    param (
        # ETH user name
        [Parameter(Position = 0, Mandatory = 1, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = "username")]
        [String]$Identity
    )

    BEGIN {
        Write-Warning "'Get-ETHPersonServices' is deprectated, use 'Get-ETHUserServices' instead"
    }

    PROCESS {
        return (Get-ETHUserServices -Identity $Identity)
    }
}


function Get-ETHUser {
    <#
    .SYNOPSIS
        Gets the parameters of a IT-Service for a user (Similar to Get-ADUser)
         
    .DESCRIPTION
        Gets all parameters for a given service from IAM for the given user
        Default is the "Mailbox" service
 
    .PARAMETER Identity
        The username to find
 
    .PARAMETER Service
        The service name to get the parameters for
 
    .EXAMPLE
        Get-ETHUser aurels
 
    .EXAMPLE
        Get-ETHUser aurels -Service LDAP
     
    #>

    param (
        [Parameter(Position = 0, Mandatory = 1, ValueFromPipeline = $true)]
        [string]$Identity,
        [Parameter(Position = 1, Mandatory = 0)]
        [string]$Service = "Mailbox"
    )

    BEGIN {
        $url = "/users/$Identity/services/$Service"

        # is client initialized?
        Test-IsIAMClientInitialized | Out-Null
    }
    
    PROCESS {
        $result = Invoke-IAMMethod -Url $url -Method Get -Credentials $script:IAMCreds
    }

    END {
        return $result
    }
    
}


function Get-ETHUserGroupMembership {
    <#
.SYNOPSIS
    Gets all memberships of the given user
     
.DESCRIPTION
    This will load all group memberships with type "Custom","Admin" and "Netsup" for the given user(s)
 
.PARAMETER Identity
    The username to find
 
.EXAMPLE
    Get-ETHUserGroupMembership aurels
 
.EXAMPLE
    "aurels","jgrand" | Get-ETHUserGroupMembership
 
#>

    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = 1, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = "Name")]
        [string]$Identity
    )

    BEGIN {
        # Check if client is initialized
        Test-IsIAMClientInitialized | Out-Null
        
    }

    PROCESS {
        $url = "/users/$Identity/groups"

        if (-not (Get-ETHUser $Identity)) {
            throw "User $Identity could not be found"
        }

        $groups = Invoke-IAMMethod -Url $url -Method Get -Credentials $script:IAMCreds
        $groups | Add-Member -MemberType NoteProperty -Value $Identity -Name "User"

        # set type for all objects
        $groups | ForEach-Object { $_.pstypenames.Insert(0, "ETHZ.ID.IAMClient.IAMGroupMembership") }

        return ($groups | Sort-Object -property Type, Name)
        
    }

    END { }

}


function Get-ETHUserServices {

    <#
    .SYNOPSIS
        Gets all services of a user (DEPRECATED)
    .DESCRIPTION
        (DEPRECATED, see Get-ETHUserServices)
        Loads all services of the specified user / person from IAM with their expiry dates.
 
    .EXAMPLE
        Get-ETHUserServices aurels
 
        Gets the services of the person 'aurels'
 
    .EXAMPLE
        Get-ETHUserServices ausc
 
        Gets the details of the uname 'ausc'
 
    .OUTPUTS
        PSCustomObject[]
 
        An array of all services of the user
 
    .FUNCTIONALITY
        Loading services
    #>


    [CmdLetBinding()]
    param (
        # ETH user name
        [Parameter(Position = 0, Mandatory = 1, ValueFromPipeline = $true, HelpMessage = "The username to find the services for")]
        [String]$Identity
    )

    BEGIN {
        # Check if client is initialized
        Test-IsIAMClientInitialized | Out-Null
    }

    PROCESS {
        return (Invoke-IAMMethod -Url "/users/$Identity/services" -Method GET -Credentials $script:IAMCreds)
    }
}


Function Write-RequestToConsole {
    param (
        [string]$Method,
        [hashtable]$Headers,
        [string]$JsonBody
    )

    Write-Verbose "-- || -- || -- || -- || --"

    Write-Verbose "$($Method.toUpper()) $Uri"

    foreach ($h in $Headers.Keys) {
        # Do not print basic auth string to console, instead override with some value
        if ($h -ne "Authorization") {
            Write-Verbose "${h}: $($Headers[$h])"
        }
        else {
            Write-Verbose "${h}: Basic BasicAuthString99999="
        }
    }
    Write-Verbose "Body: $JsonBody"
    
}

function Write-ResponseToConsole {
    param (
        $Response
    )
    Write-Verbose "------ RESPONSE:" 
    if ($null -ne $Response) {
        Write-Verbose (ConvertTo-Json $Response)
    }
}

function Convert-CnToName {
    [CmdletBinding()]
    param(
        # Canonical Name
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Cn
    )

    PROCESS {
        return ($Cn -split ",")[0] -replace "CN="
    }
}

function ParseHttpException {
    [CmdLetBinding()]
    param(
        [Parameter(Position = 0, Mandatory = $true)]
        [System.Management.Automation.ErrorRecord]$InputObject
    )

    # if response is a string, is contains a json object
    if ($null -ne $InputObject.Exception.Response) {
        # read answer from stream
        # > use .ToString() and string comparision instead of -is operator
        # > as the -is operator requires a vaild type, which is not the case in PowerShell 6 (type does not exist)
        if ($InputObject.Exception.Response.GetType().FullName -eq "System.Net.HttpWebResponse") {
            # PowerShell 5
            $responseStream = $InputObject.Exception.Response.GetResponseStream()

            # create streamreader to read from stream
            $streamReader = New-Object System.IO.StreamReader -ArgumentList $responseStream

            # read
            $responseStream.Seek(0, [System.IO.SeekOrigin]::Begin)
            $errResponse = $streamReader.ReadToEnd()
                    
            # clean up reader and stream
            $streamReader.Dispose()
            $responseStream.Dispose()

        }
        else {
            # PowerShell 6+
            $errResponse = $InputObject.ErrorDetails.Message
        }                    

        # try to parse Response message
        if (-not [string]::IsNullOrEmpty($errResponse)) {
                             
            try {
                $errObject = ConvertFrom-Json $errResponse
                $errMessage = ($errObject.level + " -> " + $errObject.message)
            }
            catch {
                $errMessage = $errResponse
            }

            $newException = New-Object System.Exception -ArgumentList $errMessage, $InputObject.Exception
            # response did not contain valid JSON, return original error message ( see below )
            return $newException
                             
        }
        # we did not get any additional info from the error message, just throw the original message
                                             
        # throw original error
        return $InputObject
    }
}

function IsPsWindows {
    <#
        .SYNOPSIS
            Used as fallback for the $isWindows variable on PSv5 and before
    #>


    if (Get-Variable -Name "IsWindows" -ErrorAction SilentlyContinue) {
        return $IsWindows
    }
    return ($PSVersionTable.PSVersion.Major -le 5)
}

function ValidatePath {
 
    <#
        .SYNOPSIS
            Validates a given path for `Set-ETHUser`
    #>


    [CmdLetBinding()]
    param (
        [AllowEmptyString()]
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [Parameter(Mandatory = $false)]
        [switch]$AllowEmptyOrWhiteSpace
    )

    $isNullOrWhiteSpace = [string]::IsNullOrWhiteSpace($Path)
    if ($AllowEmptyOrWhiteSpace -and $isNullOrWhiteSpace){
        return $true
    } elseif ($isNullOrWhiteSpace){
        return $false
    }

    return [System.IO.Path]::IsPathRooted($Path)
}


function Test-IsIAMClientInitialized {
    [CmdLetBinding(SupportsShouldProcess = $true)]
    param()

    if (! $PSCmdlet.ShouldProcess("Only check if we are not running in -WhatIf mode")) {
        return
    }
    if ($null -eq $script:IAMCreds) {
        # remove all "normal" output from this cmdlet -> do not display hints
        Initialize-IAMClient 6>$null
        return $true
    }

    return $true
}

function SaveCredToCredMan {

    <#
        .SYNOPSIS
            Stores a pscredential in the PwManager
    #>


    [CmdLetBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscredential]$Credential,

        [Parameter(Mandatory = $true)]
        [string]$PwIdentifier
    )

    $PwVault = InitCredMan -ErrorAction Stop
    

    $PwCred = New-Object Windows.Security.Credentials.PasswordCredential -ArgumentList ($PwIdentifier, $Credential.UserName, $Credential.GetNetworkCredential().Password)
    $PwVault.Add($PwCred)


}

function RetreiveCredFromCredMan {

    <#
        .SYNOPSIS
            Retrieves a password stored in the windows credential manager
    #>


    [CmdLetBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$PwIdentifier
    )

    $PwVault = InitCredMan -ErrorAction Stop

    $PwCreds = $PwVault.FindAllByResource($PwIdentifier)
    if ($PwCreds.Count -ne 1) {
        throw "Found $($PwCreds.Count) matching credentials for identifier '$($PwIdentifier)'. Please clean up credman!"
    }

    # read password
    $PwCreds[0].RetrievePassword()

    return [pscredential]::new($PwCreds[0].UserName, (ConvertTo-SecureString $PwCreds[0].Password -AsPlainText -Force))

}

function IsPwStoredInCredMan {
    
    <#
        .SYNOPSIS
            Checks if a password is stored in the windows credential manager
    #>


    [CmdLetBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$PwIdentifier
    )

    $PwVault = InitCredMan -ErrorAction Stop

    try {
        return @($PwVault.FindAllByResource($PwIdentifier)).Count -ge 1
    }
    catch {
        return $false
    }
}

function InitCredMan {

    
    <#
        .SYNOPSIS
            Initializes the Credential Manager and throws an error when is does not exist
    #>


    [CmdLetBinding()]
    param ()

    try {
        return [Windows.Security.Credentials.PasswordVault, Windows.Security.Credentials, ContentType = WindowsRuntime]::new()
    }
    catch {
        $w = IsPsWindows
        throw [System.NotSupportedException]::new("Credential manager is only supported on Windows, isWindows = $w", $_.Exception)
    }

}

function IsCredManSupported {


    <#
        .SYNOPSIS
            Initializes the Credential Manager and throws an error when is does not exist
        .OUTPUTS
            System.Boolean
    #>


    [CmdLetBinding()]
    param ()

    if (-not (IsPsWindows)){
        Write-Information "Credential Manager disabled, Reason = 'OS != Windows'" -InformationAction Continue
        return $false
    }

    if ($PSVersionTable.PSVersion.Major -eq 5){
        return $true
    }

    Write-Information "Credential Manager disabled, Reason = 'powershell-edition == core'" -InformationAction Continue
    return $false
}


Function Initialize-IAMClient {
    <#
    .SYNOPSIS
        Initializes the IAMClient to work with the API
         
    .DESCRIPTION
        Performs a login to the IAM Api and saves the credentials for the current session (or optionally permanent)
 
    .PARAMETER Credential
        The login to use when logging in to the API. If not given, PowerShell will ask manually for username / password.
 
    .PARAMETER SaveCredential
        When given, stores the password in the Windows Credential Manager for later re-use.
        **This works only on Windows!**
 
    .PARAMETER EnableDebugOutput
        Use this parameter to enable verbose logging if an error occures and you are not sure if the API answers correctly.
        The verbose output can used in e-mail communication (very recommended!)
 
    .PARAMETER ApiHost
        You can specify a custom API endpoint if you want f.ex to connect to the QSS environment.
 
    .EXAMPLE
        Initialize-IAMClient myuser4ea
 
        Signs in to the IAM api with the user "myuser4ea", you will be prompted for the password.
 
    .EXAMPLE
        Initialize-IAMClient myuser4ea -SaveCred
 
        See Example 1, but stores the credential in the Windows Credential Manager
        You will **never** need to perform the initialize command again!
 
    .FUNCTIONALITY
        Use this CmdLet to connect the PS Module with the API.
    #>

    [CmdletBinding()]
    param(
        # Credentials to validate
        [Parameter(Position = 0, Mandatory = $false)]
        [Alias("Credentials")]
        [pscredential]$Credential,

        [Parameter(Mandatory = $false)]
        [switch]$SaveCredential,

        # included for backwards compatibility
        [Parameter(DontShow = $true)]
        [switch]$Force,

        # for debug purposes
        [switch]$EnableDebugOutput,

        [Parameter(Mandatory = $false)]
        [ValidateScript({[Uri]::IsWellFormedUriString($_, [UriKind]::Absolute)})]
        [string]$ApiHost = "https://iamws.ethz.ch/"
    )

    if ($null -eq $Credential) {
        if ((IsCredManSupported) -and (IsPwStoredInCredMan -PwIdentifier $ApiHost)){
            $Credential = RetreiveCredFromCredMan -PwIdentifier $ApiHost
        } else {
            $Credential = Get-Credential -Message "Enter your credentials for IAM"
        }
    }

    # store the API host for the module to use
    $script:ApiHost = $ApiHost

    # message about force not being used.
    if ($Force -eq $true) {
        Write-Warning "The -Force switch is included for backwards compatibility only, it has no functionality"
    }

    # Enable Debug mode for script
    if ($EnableDebugOutput) {
        $script:DebugMode = $true
        $VerbosePreference = "Continue" 
    }
    else {
        $VerbosePreference = "SilentlyContinue"
    }

    # test credentials and fail if they were entered wrong
    if (-not (Test-ETHCredentials $Credential)) {
        $script:IAMCreds = $null
        throw "Could not validate your credentials"
    }
    
    if ($SaveCredential) {
        # save credential to credman
        SaveCredToCredMan -Credential $Credential -PwIdentifier $ApiHost
    }
    elseif ((IsCredManSupported) -and -not (IsPwStoredInCredMan -PwIdentifier $ApiHost)) {
        # hints about new functionality
        Write-Host -f Cyan "HINT: Use the -SaveCredential switch to save your password to the Windows Credential Manager!"
        Write-Host -f Cyan "HINT: The password will then be stored secure and permanent!"
        Write-Host -f Cyan "Example: PS> Initialize-IAMClient -SaveCredential"
    }

    # save credentials in module
    $script:IAMCreds = $Credential

    Set-StrictMode -Version latest

}  




Function Invoke-IAMMethod {

    <#
        .SYNOPSIS
            Invokes a URL of the REST API in IAM
        .DESCRIPTION
            Used internally in the IAMClient for access to the API.
            !! Do not use in Scripts !!
    #>


    [CmdletBinding(SupportsShouldProcess = 1)]
    param (
        [Parameter(Position = 0, Mandatory = 1)]
        [string]$Url,

        [Parameter(Mandatory = 1)]
        [Microsoft.PowerShell.Commands.WebRequestMethod]$Method,

        [Parameter(Position = 1)]
        [psobject]$Body = "",
        
        [Parameter(Position = 2)]
        [pscredential]$Credentials

    )

    BEGIN {

        $Headers = @{ }

        If ($Credentials -ne $null) {
            $AuthHeader = "Basic " + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$($Credentials.UserName):$($Credentials.GetNetworkCredential().Password)"));
            # only set auth header when needed
            $Headers.Add("Authorization", $AuthHeader);
        }

        if ($Body -ne "") {
            if ($Body -is [Array]) {
                $JsonBody = ConvertTo-Json @($Body) -Compress
            }
            else {
                $JsonBody = ConvertTo-Json $Body -Compress
            }
        }
        else {
            $JsonBody = ""
        }

        # Accept header
        $Headers.Add("Accept", "application/json");

        if (-not [string]::IsNullOrWhiteSpace($JsonBody)) {
            $Headers.Add("Content-type", "application/json; charset=utf-8");
        }

        # Form complete URL and parse it
        $Uri = $script:ApiHost + $Url
        if (-not [uri]::IsWellFormedUriString($Uri, "Absolute")) {
            throw "Could not parse URI $Uri"
            return
        }

    }

    PROCESS {
        if ($PSCmdlet.ShouldProcess($Url)) {
            if ($script:DebugMode) {
                Write-RequestToConsole -Method $Method.ToString() -Headers $Headers -JsonBody $JsonBody
            }

            try {


                # only provide the body when needed, as it gives an error when used with GET
                if ($Method -eq [Microsoft.PowerShell.Commands.WebRequestMethod]::Get) {
                    $Response = Invoke-RestMethod -Uri $Uri -Method $Method -Headers $Headers
                }
                else {
                    $Response = Invoke-RestMethod -Uri $Uri -Method $Method -Headers $Headers -Body $JsonBody
                }

                # Only write to console if debug is enabled
                if ($script:DebugMode) {
                    Write-ResponseToConsole -Response $Response
                }
            }
            catch {
                # parse the error message and throw the output
                throw (ParseHttpException -InputObject $_)
                
            }
        }
    }

        

    END {
        return $Response
    }

}


function New-ETHGroup {
    <#
    .SYNOPSIS
        Creates a new group in IAM
         
    .DESCRIPTION
        Creates a new group in IAM with the given properties
 
    .PARAMETER Name
        The name of the group
 
    .PARAMETER Description
        The description to give the group
     
    .PARAMETER Targets
        The targets to export the group to. Valid are "Active Directory" and "LDAPS" (both can be specified)
 
    .PARAMETER AdminGroup
        The Admin Group to assign this group to
 
    .PARAMETER CertificationPeriod
        How often the group needs to be recetificated, allowed are "No recertification", "Quarterly", "Annual", "Biennial"
     
    .PARAMETER CertificationNote
        This note is only needed if "No recertification" is chosen
     
    .PARAMETER GroupADOU
        The location where the group is created within the AD, only predefinded values are allowed
 
    .PARAMETER GroupManagers
        The managers of the group
 
    .PARAMETER Members
        The members to assign to the newly created group
 
    .EXAMPLE
        New-ETHGroup -Name "biol-micro-isg-testgroup_api" -Description "20191203/jog: TestGroup" -Targets LDAPS -AdminGroup "D-BIOL" -CertificationPeriod "No recertification"
 
        This example creates one group, with the name "biol-micro-isg-testgroup_api" with a description and Admin Group "D-BIOL" exported to LDAPS only. Note that the CertificationNote is filled automatically if not provided.
 
         
    .EXAMPLE
        "biol-micro-testgroup1","biol-micro-testgroup2" | New-ETHGroup -Description "20191203/asc: TestGroup" -Targets "Active Directory", LDAPS -AdminGroup "D-BIOL" -Members "jgrand","scheusss"
         
        This example creates two groups, both with the same members, "jgrand" and "scheusss"
    #>

    param (
        [CmdletBinding()]
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Name,
        
        [Parameter(Position = 1, Mandatory = $false)]
        [string]$Description,
        
        [Parameter(Position = 2, Mandatory = $false)]
        [ValidateSet("Active Directory", "LDAPS")]
        [string[]]$Targets,

        [Parameter(Position = 3, Mandatory = $true)]
        [string]$AdminGroup,

        [Parameter(Position = 4, Mandatory = $true)]
        [ValidateSet("No recertification", "Quarterly", "Annual", "Biennial")]
        [string]$CertificationPeriod,

        [Parameter(Position = 5, Mandatory = $false)]
        [string]$CertificationNote,

        [Parameter(Position = 6, Mandatory = $false)]
        [string]$GroupADOU,

        [Parameter(Position = 7, Mandatory = $false)]
        [string[]]$GroupManagers,

        [Parameter(Position = 8, Mandatory = $false)]
        [string[]]$Members
    )

    BEGIN {
        # Check if client is initialized
        Test-IsIAMClientInitialized | Out-Null

        $url = "/groups"
        if($CertificationPeriod -eq "No recertification" -and [string]::IsNullOrEmpty($CertificationNote) ){
            $CertificationNote = "via PS Module created"
        }

    }

    PROCESS {

        $Body = @{
            "name"                  = $Name
            "description"           = $Description
            "admingroup"            = $AdminGroup
            "targets"               = $Targets
            "certificationPeriod"   = $CertificationPeriod
            "certificationNote"     = $CertificationNote
            "groupADOU"             = $GroupADOU
            "groupManager"          = $GroupManagers
        }

        $creationResult = Invoke-IAMMethod -Url $url -Method Post -Body $Body -Credentials $script:IAMCreds

        # only add members when the creation was successful
        if ($Members -and $?){
            return (Add-ETHGroupMember -Identity $Name -Members $Members)
        }
    }

    END {
        return $creationResult
    }    
}



Function New-ETHPersona {

    <#
    .SYNOPSIS
        Creates a new uname for the specified persona
    .DESCRIPTION
        Creates a new uname for the specified persona
    .EXAMPLE
        New-ETHPersona -ParentIdentity "ti03388" -NewUserName "mynewuser" -UserComment "For testing"
 
        Creates a new uname 'mynewuser' under the persona ti03388 with comment "For testing"
 
    .FUNCTIONALITY
        Creating usernames
    #>



    param(
        # Username of Persona parent
        [Parameter(Position = 0, Mandatory = 1, HelpMessage = "The parent persona (f.ex. ti123456)")]
        [string]
        $ParentIdentity,

        # New Username
        [Parameter(Position = 1, Mandatory = 1, HelpMessage = "The new username to assign to the user")]
        [string]
        $NewUserName,

        [Parameter(Position = 2, HelpMessage = "The comment to set (only visible in IAM)")]
        [string]
        $Description = ""
    )

    BEGIN {
        # is client initialized?
        Test-IsIAMClientInitialized | Out-Null

        $NewUser = [PSCustomObject]@{
            username    = $NewUserName;
            description = $Description;
        }
    }

    PROCESS {
        Invoke-IAMMethod -Url "/users/$ParentIdentity/personas" -Method POST -Credentials $script:IAMCreds -Body $NewUser
    }
}


function Remove-ETHGroup {
    <#
    .SYNOPSIS
        Deletes a group in IAM
         
    .DESCRIPTION
        Deletes a group with the specified name in IAM
 
    .PARAMETER Name
        The name of the group
 
    .EXAMPLE
        Remove-ETHGroup -Name "biol-micro-isg-testgroup_api"
 
        In this example, the group "biol-micro-isg-testgroup_api" is deleted
 
    .EXAMPLE
        Get-ETHGroup "biol-micro-isg-testgroup_api" | Remove-ETHGroup
 
        In this example, the group "biol-micro-isg-testgroup_api" is deleted using the pipeline.
             
     
    #>

    param (
        [CmdletBinding()]
        [Parameter(Position = 0, Mandatory = 1, ValueFromPipelineByPropertyName = "name")]
        [string]$Name
    )

    BEGIN {
        # Check if client is initialized
        Test-IsIAMClientInitialized | Out-Null

        
    }

    PROCESS {

        $url = "/groups/$Name"

        Invoke-IAMMethod -Url $url -Method Delete -Credentials $script:IAMCreds

        return $Name
    }

    END {

    }    
}



function Remove-ETHGroupMember {

    <#
    .SYNOPSIS
        Removes members from a group in IAM
    .DESCRIPTION
        Removes members from a group in IAM
    .EXAMPLE
        Remove-ETHGroupMember -Identity "biol-micro-isg" -Members "aurels","jgrand"
 
        Removes two members from the group "biol-micro-isg"
    .EXAMPLE
        "somegroup","someothergroup","somethirdgroup" | Remove-ETHGroupMember -Members "aurels","jgrand"
 
        Removes multiple members from multiple groups
    .EXAMPLE
        Remove-ETHGroupMember -Identity "biol-micro-isg" -Members "aurels","jgrand" -SubGroups "biol-micro-isg-4ea","biol-micro-isg-4sa"
 
        Removes two members and two subgroups from the group "biol-micro-isg"
    .FUNCTIONALITY
        Removing users and/or subgroups from groups
    #>


    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Identity,

        [Parameter(Position = 1)]
        [string[]]$Members = @(),

        [Parameter(Position = 2)]
        [string[]]$SubGroups = @()
    )

    BEGIN {
        # Check if client is initialized
        Test-IsIAMClientInitialized | Out-Null

        # check if any members were specified
        if ($Members.Count -le 0 -and $SubGroups.Count -le 0) {
            throw "No members nor subgroups specified"
        }

        # Validate group exists
        if (-not ($ExistingGroup = Get-ETHGroup -Identity $Identity)) {
            throw "Group $Identity was not found"
        }

        # get list of users to remove (skip non-members)
        $ToRemoveMembers = @($Members | Where-Object { $ExistingGroup.users -contains $_ })
        $ToRemoveSubGroups = @($SubGroups | Where-Object { $ExistingGroup.subgroups -contains $_ })

        # Check if there are any members in the group that need to be removed
        if ($ToRemoveMembers.Count -eq 0 -and $ToRemoveSubGroups -eq 0) {
            Write-Debug "Did not need to remove any members/subgroups from $Identity"
            return $ExistingGroup
        }

        $Body = @{
            users     = $ToRemoveMembers
            subgroups = $ToRemoveSubGroups 
        }
    }

    PROCESS {

        if ($PSCmdlet.ShouldProcess($Identity)) {
            try {
                return (Invoke-IAMMethod -Url "/groups/$Identity/members/remove" -Method Put -Body $Body -Credentials $script:IAMCreds)
            }
            catch {
                throw "Could not update group $Identity"
            }
        }
    }

    END { }
}


function Remove-ETHMaillist {

    <#
    .SYNOPSIS
        Removes a maillist
    .DESCRIPTION
        Deletes a maillist from IAM (irreversably)
    .EXAMPLE
        Remove-ETHMaillist biol-micro-aebi
 
        Removes the maillist biol-micro-aebi
 
    .EXAMPLE
        Remove-ETHMaillist biol-micro-aebi -WhatIf
 
        Simulates the removal of the maillist without actually deleting it.
     
 
    .FUNCTIONALITY
        Removing a maillist
    #>


    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        # List Name
        [Parameter(Position = 0, ParameterSetName = "ByName")]
        [string]$Identity,

        [Parameter(ValueFromPipeline = $true, ParameterSetName = "ByPipeline")]
        [psobject]$MailObject

    )

    BEGIN {
        Test-IsIAMClientInitialized | Out-Null

        if ($MailObject -ne $null) {
            $Identity = $MailObject.listName
        }

        $Url = "/mailinglists/$Identity"
    }

    PROCESS {
        if ($PSCmdlet.ShouldProcess("Deleting Mailliglist $Identity")) {
            return (Invoke-IAMMethod -Url $Url -Method Delete -Credentials $script:IAMCreds)
        }
    }
}


function Remove-ETHMaillistMember {

    <#
    .SYNOPSIS
        Removes members of an IAM maillist
    .DESCRIPTION
        Removes one or more members from an IAM maillist
    .EXAMPLE
        Remove-ETHMaillistMember biol-micro-list-aebi -Members "aurels"
 
        Removes one member "aurels" from the maillist "biol-micro-list-aebi"
    .EXAMPLE
        "aurels","jgrand" | Remove-ETHMaillistMember "somegroup","someothergroup","somethirdgroup"
 
        Removes multiple users ("aurels" and "jgrand") From multiple maillists
 
    .OUTPUTS
        PSCustomObject
 
        An object containing all accepted / rejected / failed members
 
    .FUNCTIONALITY
        Removing members from a maillist
    #>


    param (
        # List Name
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Identity,

        # Member to add or remove
        [Parameter(Position = 1, Mandatory = $true)]
        [string[]]$Members
    )

    BEGIN {
        Test-IsIAMClientInitialized | Out-Null

        $Body = [PSCustomObject]@{
            users   = $Members;
        }
    }

    PROCESS {
        $Url = "/mailinglists/$Identity/members/remove"

        return (Invoke-IAMMethod -Url $Url -Body $Body -Method Put -Credentials $script:IAMCreds)
    }


}


function Set-ETHGroup {
    <#
    .SYNOPSIS
        Edits an existing group in IAM
         
    .DESCRIPTION
        Sets specific properties of an existing group in IAM
 
    .PARAMETER Identity
        The name of the group
 
    .PARAMETER NewName
        Rename the group to this new name
 
    .PARAMETER NewDescription
        Set the description of this group
 
    .PARAMETER NewCertPeriod
        Set the new recertification period. Possible values are: "No recertification", "Quarterly", "Annual", "Biennial"
 
    .PARAMETER NewCertNote
        This note is only needed if "No recertification" is chosen
 
    .PARAMETER NewGroupManager
        The new managers of the group
 
    .PARAMETER NewGroupADOU
        The new hosting location in the AD
 
    .EXAMPLE
        Set-ETHGroup -Identity "biol-micro-api_test" -NewDescription "NewDescription"
         
        Change the description of the group "biol-micro-api_test".
     
    #>

    param (
        [CmdletBinding()]
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipelineByPropertyName = "name")]
        [string]$Identity,

        [Parameter(Position = 1, Mandatory = $false)]
        [string]$NewName,
        
        [Parameter(Position = 2, Mandatory = $false)]
        [string]$NewDescription, 

        [Parameter(Position = 3, Mandatory = $false)]
        [ValidateSet("No recertification", "Quarterly", "Annual", "Biennial")]
        [string]$NewCertPeriod, 

        [Parameter(Position = 4, Mandatory = $false)]
        [string]$NewCertNote, 

        [Parameter(Position = 5, Mandatory = $false)]
        [string[]]$NewGroupManager,

        [Parameter(Position = 5, Mandatory = $false)]
        [string[]]$NewGroupADOU

    )

    BEGIN {
        # Check if client is initialized
        Test-IsIAMClientInitialized | Out-Null
    }

    PROCESS {

        $url = "/groups/$Identity"

        $Body = @{ }

        if ($NewName) { $Body["newName"] = $NewName }
        if ($NewDescription) { $Body["newDescription"] = $NewDescription }
        if ($NewGroupManager) { $Body["newGroupManager"] = $NewGroupManager }
        if ($NewGroupADOU) { $Body["newGroupADOU"] = $NewGroupADOU }


        if ($NewCertPeriod) { 
            if($NewCertPeriod -eq "No recertification" -and [string]::IsNullOrEmpty($NewCertNote)){
                Write-Host -ForegroundColor Yellow "Changing recertification to 'No recertification' is only allowed when providing a certification note!"
            }else{
                $Body["newCertPeriod"] = $NewCertPeriod 
                $Body["newCertNote"] = $NewCertNote 
            }
        }

        if ($Body.Count -gt 0) {
            Invoke-IAMMethod -Url $url -Method Put -Body $Body -Credentials $script:IAMCreds -ea Continue
        }

        $Identity = if ($NewName) { $NewName } else { $Identity }
        Get-ETHGroup $Identity
    }

    END {

    }    
}



function Set-ETHGroupMember {
    <#
    .SYNOPSIS
        Sets the members of an ETH group to the specified member list
         
    .DESCRIPTION
        Removes / Adds members to the given group until the memberlist is equal to the one submitted
        You can specify either usernames or *custom* groups
 
    .PARAMETER Identity
        The group to edit
 
    .PARAMETER Members
        The list of members to set the group memberlist to
 
    .EXAMPLE
        PS> Set-ETHGroupMember -Identity biol-micro-isg -Members @("aurels","ausc")
 
    .EXAMPLE
        PS> Set-ETHGroupMember -Identity biol-micro-isg -Members @("biol-micro-isg-sadm","aurels")
 
        Added: {"aurels", "ausc"}
        Removed: {}
        Kept: {}
 
    .OUTPUTS
        pscustomobject. Returns a custom object with 3 properties Added, Removed and Kept to show what the cmdlet did
 
    #>

    [CmdletBinding(SupportsShouldProcess = $true, HelpUri = "https://gitlab.ethz.ch/aurels/iam-powershell/tree/master/docs/Set-ETHGroupMember.md")]
    [OutputType([PSCustomObject])]
    param(
        # Group Name
        [Parameter(Position = 0, Mandatory = $true)]
        [string]$Identity,

        # Members to sync to
        [Parameter(Position = 1, Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = "Name")]
        [AllowEmptyCollection()]
        [string[]]$Members
    )

    BEGIN {
        # Check if client is initialized
        Test-IsIAMClientInitialized | Out-Null

        try {
            $ExistingMembers = @(Get-ETHGroupMember $Identity)
        }
        catch {
            throw "Could not find group $Identity"
        }
    }

    PROCESS {
        # get members to be removed & added
        $MemberCompare = Compare-GroupMembers -ExistingMembers $ExistingMembers -NewMembers $Members
        $ToBeAdded = $MemberCompare.ToAdd
        $ToBeRemoved = $MemberCompare.ToRemove

        try {

            if ($ToBeAdded.Count -gt 0) {
                if ($PSCmdlet.ShouldProcess($Identity, "Add-ETHGroupMember")) {
                    # Discard output of Add-ETHGroupMember
                    $null = Add-ETHGroupMember -Identity $Identity -Members $ToBeAdded
                }
            }

            if ($ToBeRemoved.Count -gt 0) {
                if ($PSCmdlet.ShouldProcess($Identity, "Remove-ETHGroupMember")) {
                    $null = Remove-ETHGroupMember -Identity $Identity -Members $ToBeRemoved
                }
            }

        }
        catch {
            throw "Failed to update group membership of group $Identity, try again to restore group integrity!`r`nError: $_"
        }

        return @{
            Added   = $ToBeAdded;
            Removed = $ToBeRemoved;
            Kept    = $MemberCompare.ToKeep;
        }
        
    }

    END { }
}


Function Set-ETHMaillistMembers {

    <#
    .SYNOPSIS
        Replaces members of an IAM maillist
    .DESCRIPTION
        Replaces the whole maillist members with the new provided members.
    .EXAMPLE
        Set-ETHMaillistMembers biol-micro-list-aebi -Members "aurels","jgrand","scheusss"
 
        Replaces the whole content of biol-micro-list-aebi with the members aurels, jgrand and scheusss
 
    .OUTPUTS
        pscustomobject. Returns a custom object with 3 properties Added, Removed and Kept to show what the cmdlet did
 
    .FUNCTIONALITY
        Setting members for a maillist
    #>


    [CmdletBinding()]
    param (
        # Maillist name
        [Parameter(Position = 0, Mandatory = $true)]
        [string]
        $Identity,

        # Members
        [Parameter(Position = 1, Mandatory = $true)]
        [string[]]$Members
    )

    BEGIN {
        Test-IsIAMClientInitialized | Out-Null

        try {
            $ExistingMembers = @((Get-ETHMaillistMember -Identity $Identity).name)
        }
        catch {
            throw "Could not find Mailinglist $Identity"
        }
    }

    PROCESS {
        $MemberCompare = Compare-GroupMembers -ExistingMembers $ExistingMembers -NewMembers $Members
        $ToAddMembers = $MemberCompare.ToAdd
        $ToRemoveMembers = $MemberCompare.ToRemove

        # Add members
        try {
            if ($ToAddMembers.Count -gt 0) {
                $null = Add-ETHMaillistMember -Identity $Identity -Members $ToAddMembers
                Write-Debug "Successfully added $($ToAddMembers.Count) new members to Mailinglist $Identity"
            }
        }
        catch {
            Write-Error "Failed to add $($ToAddMembers.Count) members to mailinglist. $([System.Environment]::NewLine)Error: $_"
            return
        }

        # Remove members
        try {
            if ($ToRemoveMembers.Count -gt 0) {
                $null = Remove-ETHMaillistMember -Identity $Identity -Members $ToRemoveMembers
                Write-Debug "Successfully removed $($ToRemoveMembers.Count) members from Mailinglist $Identity"
            }
        }
        catch {
            Write-Error "Failed to remove $($ToRemoveMembers.Count) members from mailinglist. $([System.Environment]::NewLine)Error: $_"
            return
        }

        return @{
            Added   = $ToAddMembers;
            Removed = $ToRemoveMembers;
            Kept    = $MemberCompare.ToKeep;
        }
    }

}


Function Set-ETHUser {
    <#
    .SYNOPSIS
        Sets the parameters of an IT-Service for a user (Similar to Set-ADUser)
         
    .DESCRIPTION
        Changes parameters for an IT service for a user
 
    .PARAMETER Identity
        Username of the userobject to update
 
    .PARAMETER User
        The modified user object to save, use the output of Get-ETHUser.
 
    .EXAMPLE
        PS C:\> $user = Get-ETHUser aurels
 
        PS C:\> $user.homeDrive = "P:"
 
        PS C:\> $user.homeDirectory = "\\server\share\%username%"
 
        PS C:\> $user.profilePath = ""
 
        PS C:\> Set-ETHUser -Identity aurels -User $user
 
        This will update the homeDrive, homeDirectory and profilePath properties of the user 'aurels'
    #>


    [CmdletBinding(DefaultParameterSetName = "ByMailboxParams", 
        HelpUri = "https://gitlab.ethz.ch/aurels/iam-powershell/-/blob/master/docs/Set-ETHUser.md",
        SupportsShouldProcess = $true)]
    param (
        [Parameter(Position = 0, Mandatory = $true, HelpMessage = "Username of the userobject to update")]
        [string]$Identity,
        
        [Parameter(Position = 2, Mandatory = $false, HelpMessage = "The service to update the parameters for (only specify if you know what you are doing!)")]
        [string]$Service = "Mailbox",

        [Parameter(Position = 1, Mandatory = $true, ValueFromPipeline = $true, HelpMessage = "Edited User object to save")]
        [psobject]$User
    )

    BEGIN {
        # Check if client is initialized
        $null = Test-IsIAMClientInitialized

        # Variable which is used for PUT body in server request
        $UploadBody = @{}

        # Load existing user
        $sourceUser = Get-ETHUser -Identity $Identity

        # see what was changed with the object
        $changedProperties = Get-ObjectDiffs $sourceUser $User
    }

    PROCESS {
        # if nothing changed, nothing to do!
        if ($changedProperties.Count -eq 0) {
            # nothing to do
            return $sourceUser
        }

        # Upload changed properties to IAM and let it do its things
        $UploadBody = $changedProperties

        # Use .ShouldProcess to allow using -WhatIf (for testing and for -WhatIf purposes :) )
        if ($PSCmdlet.ShouldProcess("Update $($UploadBody.Count) Parameters of Service '$Service' of User '$Identity'")) {
            Invoke-IAMMethod -Url "/users/$Identity/services/$Service" -Method Put -Body $UploadBody -Credentials $script:IAMCreds
        }
    }
}


Function Set-ETHUserITService {

    <#
        .SYNOPSIS
            Sets properties of a user
        .DESCRIPTION
            Basic method for setting properties of a user in IAM.
            NOTE: use `Set-ETHUser` instead (easier, better). Only use this if you know what you are doing.
         
        .LINK
            Set-ETHUser
    #>


    param (
        # ETH user name
        [Parameter(Position = 0, Mandatory = 1, HelpMessage = "The username to set")]
        [string]
        $Identity,

        # IT Service Name
        [Parameter(Position = 1, Mandatory = 1, HelpMessage = "The service to edit")]
        [string]
        $ITServiceName,

        # Body
        [Parameter(Position = 2, Mandatory = 1, HelpMessage = "The properties with their values to edit")]
        [psobject]
        $Body
    )
    BEGIN {
        # Check if client is initialized
        Test-IsIAMClientInitialized | Out-Null
    }

    PROCESS {
        return (Invoke-IAMMethod -Url "/users/$Identity/services/$ITServiceName" -Method Put -Credentials $script:IAMCreds -Body $Body)
    }
}


function Sync-ETHGroupMember {

    <#
    .SYNOPSIS
    Synchronizes users from multiple groups and mailing lists to a group and a mailinglist
     
    .DESCRIPTION
    Copies all **users** from the source groups/lists to the given destination group/list
     
    .PARAMETER SourceGroups
    The list of groups to read members from
     
    .PARAMETER SourceLists
    The list of mailinglists to read members from
     
    .PARAMETER DestGroup
    The destination group that will be set to all members from the given groups / lists
 
    .PARAMETER DestList
    The destination mailinglist that will be set to all members from the given groups / lists
 
    .EXAMPLE
    PS> Sync-ETHGroupMember -SourceGroups "biol-micro-isg" -DestList "MICRO_IT_STAFF"
 
    Copies all members from the group "biol-micro-isg" to the Maillinglist "MICRO_IT_STAFF"
     
    .EXAMPLE
    PS> Sync-ETHGroupMember -SourceLists "MICRO_IT_STAFF","MICRO_AD_STAFF" -DestList "MICRO_STAFF"
 
    Copies all members from the source lists to the destination list
 
    .EXAMPLE
    PS> Sync-ETHGroupMember -SourceLists "MICRO_IT_STAFF","MICRO_AD_STAFF" -SourceGroups "biol-micro-institute" -DestGroup "biol-micro-institute"
 
    Adds all members from the given lists to the destination group without removing users
 
    .OUTPUTS
        System.Collections.Hashtable
        A hashtable with a report on each group/list that was modified and what was done (Add / Remove / Keep Members)
     
    #>



    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "ToGroup")]
    [OutputType([System.Collections.HashTable])]
    param(
        # Group Name
        [Parameter(Position = 0)]
        [Alias("ReferenceGroups")]
        [string[]]$SourceGroups,

        # Mailinglist to sync from
        [Parameter(Position = 1)]
        [string[]]$SourceLists,

        # Group to sync members to
        [Parameter(Position = 2, ParameterSetName = "ToGroup", Mandatory = 1)]
        [Parameter(ParameterSetName = "ToBoth", Mandatory = 1)]
        [Alias("SyncGroup")]
        [string]$DestGroup,

        # List to sync members to
        [Parameter(Position = 3, ParameterSetName = "ToList", Mandatory = 1)]
        [Parameter(ParameterSetName = "ToBoth", Mandatory = 1)]
        [string]$DestList,

        # Falls back to AD if group cannot be loaded via IAM
        [Parameter()]
        [switch]$AllowADFallback
    )

    BEGIN {

        # Validate input arguments
        if ($SourceGroups.Count -eq 0 -and $SourceLists.Count -eq 0) {
            throw "At least one source group or list has to be specified!"
        }

        if ([string]::IsNullOrWhiteSpace($DestGroup) -and [string]::IsNullOrWhiteSpace($DestList)) {
            throw "At least one destination group has to be specified!"
        }

        if ($AllowADFallback -and (Get-Module).Name -notcontains "ActiveDirectory") {
            try {
                Import-Module ActiveDirectory
            }
            catch {
                throw "To use the ActiveDirectory fallback, install RSAT tools!"
            }
        }

        # Check if client is initialized
        Test-IsIAMClientInitialized | Out-Null

        # Validate destination group exists
        if ($DestGroup) {
            try {
                $null = Get-ETHGroup -Identity $DestGroup # discard output
            }
            catch {
                throw "Could not find group $DestGroup"
            }
        }

        if ($DestList) {
            try {
                $null = Get-ETHMaillist -Identity $DestList # discard output
            }
            catch {
                throw "Could not find list $DestList"
            }
        }

        $ListsToProcess = @()
        $GroupsToProcess = @()

        if ($SourceLists.Count -gt 0) { 
            $ListsToProcess = @($SourceLists | ForEach-Object { [PSCustomObject]@{Name = $_; Type = "List" } }) 
        }

        if ($SourceGroups.Count -gt 0) {
            $GroupsToProcess = @($SourceGroups | ForEach-Object { [PSCustomObject]@{Name = $_; Type = "Group" } })
        }
    }

    PROCESS {
        
        # Store all members from the different sourcegroups in a hashset,
        # so that duplicates are automatically eliminated
        $AllMembersList = New-Object 'System.Collections.Generic.HashSet[string]' 
        
        foreach ($Source in @($ListsToProcess + $GroupsToProcess)) {
            # retrieve type for output messages
            $SourceType = $Source.Type
            try {
                switch ($SourceType) {
                    "Group" { 
                        # Get Group members
                        $Group = Get-ETHGroup $Source.Name
                        $AllMembersList.UnionWith([string[]]@($Group.users))
                    }
                    "List" {
                        # Get Maillist members
                        $ListMembers = Get-ETHMaillistMember $Source.Name | Where-Object objectClass -eq "user"
                        $AllMembersList.UnionWith([string[]]@($ListMembers.name)) # add all members to the list
                    }
                    Default {
                        # Invalid
                        throw "GroupType '$SourceType' invalid. Valid are 'List','Group'!"
                    }
                }
            
            }
            catch {
                # Group / List was not found in IAM
                # Perform ad fallback if needed
                if (-not $AllowADFallback) {
                    throw "Could not find $SourceType '$($Source.Name)' in IAM"
                }
                try {
                    # get all users from AD group as fallback
                    $Members = Get-ADGroupMember -Identity $Source.Name | Where-Object objectClass -eq "user"
                    $AllMembersList.UnionWith([string[]]($Members.name))
                }
                catch {
                    throw "Could not find $SourceType '$($Source.Name)' in AD"
                }
            
            }
        }

        # Store changes in a hashtable for every group modified
        $Changes = @{ }

        [string[]]$AllMembersArray = $AllMembersList | Where {$_ -ne ""} # convertion of Set to Array needed in order to work

        if ($DestGroup -ne "" -and $PSCmdlet.ShouldProcess($DestGroup, "Set-ETHGroupMember")) {
            $result = Set-ETHGroupMember -Identity $DestGroup -Members $AllMembersArray
            $Changes.Add($DestGroup, $result)
        }

        if ($DestList -ne "" -and $PSCmdlet.ShouldProcess("$DestList", "Set-ETHMaillistMembers")) {
            $result = Set-ETHMaillistMembers -Identity $DestList -Members $AllMembersArray
            $Changes.Add($DestList, $result)
        }
    }

    END {
        return $Changes
    }
}


Function Test-ETHCredentials {

    <#
        .SYNOPSIS
            Tests a given credential set with IAM
        .DESCRIPTION
            Tests if the given credential can read the detail of the service 'Mailbox' in IAM.
 
        .OUTPUTS
            boolean
    #>


    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = 1, HelpMessage = "The credentials to check")]
        [ValidateNotNull()]
        [pscredential]$Credentials
    )
    
    $script:IAMCreds = $Credentials
    
    try {    
        Get-ETHUser -Identity $Credentials.UserName -ErrorAction Stop
        return $true
    }
    catch {
        # write error to error stream as non-terminating error
        Write-Error $_
        return $false
    }
    finally {
        $script:IAMCreds = $null
    }
    
}


function Update-ETHGroupCertification {

    <#
    .SYNOPSIS
        Recertifies a group of IAM
    .DESCRIPTION
        Recertifies a group of IAM
    .EXAMPLE
        Update-ETHGroupCertification biol-micro-isg
 
        Recertifies the group biol-micro-isg
 
    .OUTPUTS
        PSCustomObject
 
        An object with all properties of a group
 
    .FUNCTIONALITY
        Recertifies a group
    #>


    param (
        [CmdletBinding()]
        [Parameter(Position = 0, Mandatory = 1)]
        [string]$Identity
    )

    BEGIN {
        # Check if client is initialized
        Test-IsIAMClientInitialized | Out-Null

        $url = "/groups/$Identity/recertify"
    }

    PROCESS {
        return (Invoke-IAMMethod -Url $url -Method Put -Credentials $script:IAMCreds)
    }  
}


function Update-ETHMaillistCertification {

    <#
    .SYNOPSIS
        Recertifies a maillist of IAM
    .DESCRIPTION
        Recertifies a maillist of IAM
    .EXAMPLE
        Update-ETHMaillistCertification biol-micro-list-aebi
 
        Recertifies maillist biol-micro-list-aebi
     
    .OUTPUTS
        PSCustomObject
 
        An object with all properties of a maillist
 
    .FUNCTIONALITY
        Recertifies a maillist
    #>


    param (
        # List Name
        [Parameter(Position = 0, Mandatory = $true)]
        [string]$Identity
    )

    BEGIN {
        Test-IsIAMClientInitialized | Out-Null

        $Url = "/mailinglists/$Identity/recertify"
    }

    PROCESS {
        return (Invoke-IAMMethod -Url $Url -Method Put -Credentials $script:IAMCreds)
    }
}


$script:IAMCreds = $null
$script:ApiHost = "" # will set during initialization
$script:DebugMode = $false

if ($PSVersionTable.PSVersion.Major -le 5){
    # set TLS1.2 as default when running in PSv5
    # if TLS1.3 is out, this should be specified
    # https://docs.microsoft.com/en-us/security/engineering/solving-tls1-problem#update-windows-powershell-scripts-or-related-registry-settings
    [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
}


Export-ModuleMember -Function 'Invoke-IAMMethod','Add-ETHGroupmember','Get-ETHGroup','Get-ETHGroupMember','New-ETHGroup','Remove-ETHGroup','Remove-ETHGroupMember','Set-ETHGroup','Set-ETHGroupMember','Sync-ETHGroupMember','Update-ETHGroupCertification','Add-ETHMaillistMember','Clear-ETHMaillistMember','Get-ETHMaillist','Get-ETHMaillistMember','Remove-ETHMaillist','Remove-ETHMaillistMember','Set-ETHMaillistMembers','Update-ETHMaillistCertification','Get-ETHPerson','Get-ETHPersonServices','New-ETHPersona','Add-ETHUserITService','Add-ETHUserMailAlias','Get-ETHUser','Get-ETHUserGroupMembership','Get-ETHUserServices','Set-ETHUser','Set-ETHUserITService','Initialize-IAMClient','Test-ETHCredentials'