IAMClient.psm1

function Add-ETHGroupMember {
    [CmdletBinding(SupportsShouldProcess = 1)]
    param (   
        [Parameter(Position = 0, Mandatory = 1, ValueFromPipeline = 1)]
        [string]$Identity,
        [string[]]$Members
    )

    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) {
            throw "No members specified"
        }

        $ToAddMembers = @($Members | Where-Object { $ExistingGroup.members -notcontains $_ })

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

    PROCESS {
        if ($PSCmdlet.ShouldProcess($Identity)) {
            try {
                $result = Invoke-IAMMethod -Url "/groupmgr/group/$Identity/members/add" -Method Put -Body $ToAddMembers -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 {
    param (
        # List Name
        [Parameter(Position = 0, Mandatory = $true)]
        [string]$Identity,

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

    BEGIN {
        Test-IsIAMClientInitialized | Out-Null

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

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


}


function Add-ETHUserITService {
    param (
        # ETH user name
        [Parameter(Position = 0, Mandatory = 1)]
        [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 "/usermgr/user/$Identity/service/$ITServiceName" -Method POST -Credentials $script:IAMCreds -Body $Body)
    }
}


function Clear-ETHMaillistMember {
    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 {
    param (
        [CmdletBinding()]
        [Parameter(Position = 0, Mandatory = 1)]
        [string]$Identity
    )

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

        $url = "/groupmgr/group/$Identity"
    }

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

    END {

    }    
}



function Get-ETHGroupMember {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = 1, ValueFromPipelineByPropertyName = 1)]
        [string]$Identity
    )

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

    PROCESS {
        return ($Group | Select-Object -expand members)
    }

    END {

    }

}


function Get-ETHMaillist {
    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 {
    [CmdletBinding()]
    param (
        # List Name
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Identity
    )

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

    }

    PROCESS {
        
        foreach ($member in $membersCn) {
            $Name = ($member -split ",OU=")[0] -replace "CN="
        
            # 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 = $membersCn; 
            }
        }
    }
}


function Get-ETHPerson {
    param (
        [Parameter(Position = 0, Mandatory = 1)]
        [string]$Identity
    )

    BEGIN {
        $url = "/usermgr/person/$Identity"

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

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

    END {

    }
    
}


function Get-ETHPersonServices {
    param (
        # ETH user name
        [Parameter(Position = 0, Mandatory = 1)]
        [String]$Identity
    )

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

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


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 = "/usermgr/user/$Identity/service/$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","" Get-ETHUserGroupMembership aurels
 
#>

    [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 = "/groupmgr/user/$Identity"

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

        $groups = (Invoke-IAMMethod -Url $url -Method Get -Credentials $script:IAMCreds).Groups
        $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 Write-RequestToConsole {
    param (
        [string]$Method,
        [hashtable]$Headers,
        [string]$JsonBody
    )

    Write-Debug "-- || -- || -- || -- || --"

    Write-Debug "$($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-Debug "${h}: $($Headers[$h])"
        }
        else {
            Write-Debug "${h}: Basic BasicAuthString99999="
        }
    }
    Write-Debug "Body: $JsonBody"
    
}

function Write-ResponseToConsole {
    param (
        $Response
    )
    Write-Debug "------ RESPONSE:" 
    if ($null -ne $Response) {
        Write-Debug (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 Test-IsIAMClientInitialized {
    if ($null -eq $script:IAMCreds) {
        throw "Please initialize the client to use this function"
        return $false
    }

    return $true
}


function Initialize-IAMClient {
    [CmdletBinding()]
    param(
        # Credentials to validate
        [Parameter(Position = 1, Mandatory = 1)]
        [pscredential]
        $Credentials,

        [switch]$Force
    )

    if ($null -ne $script:IAMCreds -and $Force -eq $false) {
        Write-Information "The Client is already initialized, use the -Force Switch to override credentials"
        return
    }

    if (-not (Test-ETHCredentials $Credentials)) {
        throw "Could not validate your credentials"
    }

    # Enable Debug mode for script
    if ($PSBoundParameters.Debug.IsPresent) {
        $script:DebugMode = $true
        $DebugPreference = "Continue" 
    }
    else {
        $DebugPreference = "SilentlyContinue"
    }

    $script:IAMCreds = $Credentials

    Set-StrictMode -Version latest
}



Function Invoke-IAMMethod {
    [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)) {
            try {
                # only provide the bdoy when needed, as it gives an error when used with GET
                if ($Method -eq "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-RequestToConsole -Method $Method.ToString() -Headers $Headers -JsonBody $JsonBody
                    Write-ResponseToConsole -Response $Response
                }
            }
            catch {

                # Only write to console if debug is enabled
                if ($script:DebugMode) {
                    Write-RequestToConsole -Method $Method.ToString() -Headers $Headers -JsonBody $JsonBody
                    Write-ResponseToConsole -Response $Response
                }
                throw "API Request failed. Message: $_"
            }
        }
    }

    END {
        return $Response
    }
}


function New-ETHPersona {
    param(
        # Username of Persona parent
        [Parameter(Position = 0, Mandatory = 1)]
        [string]
        $ParentIdentity,

        # New Username
        [Parameter(Position = 1, Mandatory = 1)]
        [string]
        $NewUserName,

        [Parameter(Position = 2)]
        [string]
        $UserComment = "",

        # Initial Password
        [Parameter(Position = 3, Mandatory = 1)]
        [string]
        $InitPwd

    )

    BEGIN {
        $NewUser = [PSCustomObject]@{
            username    = $NewUserName;
            memo        = $UserComment;
            init_passwd = $NewPassword;
        }
    }

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




function Remove-ETHGroupMember {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Position = 0, Mandatory = 1)]
        [string]$Identity,

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

    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) {
            throw "No members specified"
        }
    }

    PROCESS {
        $ToRemoveMembers = @($Members | Where-Object { $ExistingGroup.members -contains $_ })

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

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

    END {}
}


function Remove-ETHMaillist {
    [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 {
    param (
        # List Name
        [Parameter(Position = 0, Mandatory = $true)]
        [string]$Identity,

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

    BEGIN {
        Test-IsIAMClientInitialized | Out-Null

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

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


}


function Reset-ETHUserPassword {
    param (
        [Parameter(Position = 0, Mandatory = 1)]
        [string]$Identity,

        # Parameter help description
        [Parameter(Position = 1, Mandatory = 1)]
        [securestring]$NewPassword,

        [Parameter(Position = 2, Mandatory = 1)]
        [string]$ServiceName
    )

    BEGIN {
        $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($NewPassword)
        $PlainText = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
    }

    PROCESS {

        try {
            $result = Invoke-IAMMethod -Url "/usermgr/user/$Identity/service/$ServiceName/password" -Body @{password = $PlainText } -Credentials $script:IAMCreds -Method Put
            return $result;
        }
        catch {
            Write-Error "Could not reset the password of the user $Identity, Error: $_"
            return;
        }
    }
}


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 {
    [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 {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = 1)]
        [string]$Identity,
        [Parameter(Position = 1, Mandatory = 1, ValueFromPipeline = 1)]
        [psobject]$User,
        [Parameter(Position = 2, Mandatory = 0)]
        [string]$Service = "Mailbox"
    )

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

        $sourceUser = Get-ETHUser -Identity $Identity
        $changedProperties = Get-ObjectDiffs $sourceUser $User
    }

    PROCESS {
        $result = Invoke-IAMMethod -Url "/usermgr/user/$Identity/service/$Service" -Method Put -Body $changedProperties -Credentials $script:IAMCreds
    }

    END {
        $result
        return (Get-ETHUser -Identity $Identity)
    }
}


function Set-ETHUserITService {
    param (
        # ETH user name
        [Parameter(Position = 0, Mandatory = 1)]
        [string]
        $Identity,

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

        # Body
        [Parameter(Position = 2, Mandatory = 1)]
        [psobject]
        $Body
    )
    BEGIN {
        # Check if client is initialized
        Test-IsIAMClientInitialized | Out-Null
    }

    PROCESS {
        return (Invoke-IAMMethod -Url "/usermgr/user/$Identity/service/$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.members))
                    }
                    "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 = @{ }


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

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

        return $Changes
    }

    END {

    }
}


function Sync-ETHMaillistMember {
    <#
    .SYNOPSIS
        Synchronizes members mailinglists (DEPRECATED)
         
    .DESCRIPTION
        Sync-ETHMaillist members can be used to emulate nesting without actually nesting groups
        The members can be synchronized from other groups, f.ex OC-groups and no_oc-Groups
 
    .PARAMETER ReferenceLists
        The lists to get the members **from**
 
    .PARAMETER SyncList
        The destination list that will be **modified**
 
    .EXAMPLE
        Sync-ETHMaillistMember -ReferenceLists "biol-micro-test-list" -SyncList "biol-micro-list-aebi"
 
        This copies all members from the list "biol-micro-test-list" to the list "biol-micro-list-aebi"
 
    .EXAMPLE
        Sync-ETHMaillistMember -ReferenceLists "biol-micro-test-list","biol-isg-all" -SyncList "biol-micro-list-aebi"
 
        This does the same as example 1, but with multiple sourcelists which will be combined
     
    .LINK
        Set-ETHMaillistMember
        Set-ETHGroupMember
        Sync-ETHGroupMember
         
#>


    [CmdletBinding()]
    param(
        # Group Name
        [Parameter(Position = 0, Mandatory = $true)]
        [string[]]$ReferenceLists,

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

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

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

        # Validate destination list exists
        try {
            $null = Get-ETHMaillist $SyncGroup # discard output
        }
        catch {
            throw "Could not find maillist $ReferenceGroup or $SyncGroup"
        }
    }

    PROCESS {
        # Validate both groups exist
        $AllMembersList = @()
        
        foreach ($RefList in $ReferenceLists) {
            try {
                $List = Get-ETHMaillist $RefList 
                $AllMembersList += $List.members
            }
            catch {
                # Perform fallback if needed
                if (-not $AllowADFallback) {
                    throw "Could not find group $RefList in IAM"
                }
                try {
                    # get all users from AD group as fallback
                    $Members = Get-ADGroupMember -Identity $RefList | Where-Object objectClass -eq "user"
                    $AllMembersList += $Members.name
                }
                catch {
                    throw "Could not find group $RefList in AD"
                }
            
            }
        }

        Write-Debug "New Members: $AllMembersList"
        return (Set-ETHMaillistMembers -Identity $SyncGroup -Members $AllMembersList)
    }
}


function Test-ETHCredentials {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = 1)]
        [pscredential]$Credentials
    )
    try {
        $script:IAMCreds = $Credentials
        Get-ETHUser -Identity $Credentials.UserName
        $script:IAMCreds = $null
        return $true
    }
    catch {
        return $false
    }

}


$script:IAMCreds = $null
$script:ApiHost = "https://iam.password.ethz.ch/iam-ws-legacy"
$script:DebugMode = $false


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