diff --git a/lib/ansible/modules/windows/win_rds_cap.ps1 b/lib/ansible/modules/windows/win_rds_cap.ps1 new file mode 100644 index 00000000000..c247321479f --- /dev/null +++ b/lib/ansible/modules/windows/win_rds_cap.ps1 @@ -0,0 +1,357 @@ +#!powershell + +# Copyright: (c) 2018, Kevin Subileau (@ksubileau) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#Requires -Module Ansible.ModuleUtils.Legacy +#Requires -Module Ansible.ModuleUtils.SID + +$ErrorActionPreference = "Stop" + +# List of authentication methods as string. Used for parameter validation and conversion to integer flag, so order is important! +$auth_methods_set = @("none", "password", "smartcard", "both") +# List of session timeout actions as string. Used for parameter validation and conversion to integer flag, so order is important! +$session_timeout_actions_set = @("disconnect", "reauth") + +$params = Parse-Args -arguments $args -supports_check_mode $true +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false +$diff_mode = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false + +$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent","present","enabled","disabled" +$auth_method = Get-AnsibleParam -obj $params -name "auth_method" -type "str" -validateset $auth_methods_set +$order = Get-AnsibleParam -obj $params -name "order" -type "int" +$session_timeout = Get-AnsibleParam -obj $params -name "session_timeout" -type "int" +$session_timeout_action = Get-AnsibleParam -obj $params -name "session_timeout_action" -type "str" -default "disconnect" -validateset $session_timeout_actions_set +$idle_timeout = Get-AnsibleParam -obj $params -name "idle_timeout" -type "int" +$allow_only_sdrts_servers = Get-AnsibleParam -obj $params -name "allow_only_sdrts_servers" -type "bool" +$user_groups = Get-AnsibleParam -obj $params -name "user_groups" -type "list" +$computer_groups = Get-AnsibleParam -obj $params -name "computer_groups" -type "list" + +# Device redirections +$redirect_clipboard = Get-AnsibleParam -obj $params -name "redirect_clipboard" -type "bool" +$redirect_drives = Get-AnsibleParam -obj $params -name "redirect_drives" -type "bool" +$redirect_printers = Get-AnsibleParam -obj $params -name "redirect_printers" -type "bool" +$redirect_serial = Get-AnsibleParam -obj $params -name "redirect_serial" -type "bool" +$redirect_pnp = Get-AnsibleParam -obj $params -name "redirect_pnp" -type "bool" + + +function Get-CAP([string] $name) { + $cap_path = "RDS:\GatewayServer\CAP\$name" + $cap = @{ + Name = $name + } + + # Fetch CAP properties + Get-ChildItem -Path $cap_path | ForEach-Object { $cap.Add($_.Name,$_.CurrentValue) } + # Convert boolean values + $cap.Enabled = $cap.Status -eq 1 + $cap.Remove("Status") + $cap.AllowOnlySDRTSServers = $cap.AllowOnlySDRTSServers -eq 1 + + # Convert multiple choices values + $cap.AuthMethod = $auth_methods_set[$cap.AuthMethod] + $cap.SessionTimeoutAction = $session_timeout_actions_set[$cap.SessionTimeoutAction] + + # Fetch CAP device redirection settings + $cap.DeviceRedirection = @{} + Get-ChildItem -Path "$cap_path\DeviceRedirection" | ForEach-Object { $cap.DeviceRedirection.Add($_.Name, ($_.CurrentValue -eq 1)) } + + # Fetch CAP user and computer groups in Down-Level Logon format + $cap.UserGroups = @( + Get-ChildItem -Path "$cap_path\UserGroups" | + Select-Object -ExpandProperty Name | + ForEach-Object { Convert-FromSID -sid (Convert-ToSID -account_name $_) } + ) + $cap.ComputerGroups = @( + Get-ChildItem -Path "$cap_path\ComputerGroups" | + Select-Object -ExpandProperty Name | + ForEach-Object { Convert-FromSID -sid (Convert-ToSID -account_name $_) } + ) + + return $cap +} + +function Set-CAPPropertyValue { + [CmdletBinding(SupportsShouldProcess=$true)] + param ( + [Parameter(Mandatory=$true)] + [string] $name, + [Parameter(Mandatory=$true)] + [string] $property, + [Parameter(Mandatory=$true)] + $value, + [Parameter()] + $resultobj = @{} + ) + + $cap_path = "RDS:\GatewayServer\CAP\$name" + + try { + Set-Item -Path "$cap_path\$property" -Value $value -ErrorAction Stop + } catch { + Fail-Json -obj $resultobj -message "Failed to set property $property of CAP ${name}: $($_.Exception.Message)" + } +} + +$result = @{ + changed = $false +} +$diff_text = $null + +# Validate CAP name +if ($name -match "[*/\\;:?`"<>|\t]+") { + Fail-Json -obj $result -message "Invalid character in CAP name." +} + +# Validate user groups +if ($null -ne $user_groups) { + if ($user_groups.Count -lt 1) { + Fail-Json -obj $result -message "Parameter 'user_groups' cannot be an empty list." + } + + $user_groups = $user_groups | ForEach-Object { + $group = $_ + # Test that the group is resolvable on the local machine + $sid = Convert-ToSID -account_name $group + if (!$sid) { + Fail-Json -obj $result -message "$group is not a valid user group on the host machine or domain" + } + + # Return the normalized group name in Down-Level Logon format + Convert-FromSID -sid $sid + } + $user_groups = @($user_groups) +} + +# Validate computer groups +if ($null -ne $computer_groups) { + $computer_groups = $computer_groups | ForEach-Object { + $group = $_ + # Test that the group is resolvable on the local machine + $sid = Convert-ToSID -account_name $group + if (!$sid) { + Fail-Json -obj $result -message "$group is not a valid computer group on the host machine or domain" + } + + # Return the normalized group name in Down-Level Logon format + Convert-FromSID -sid $sid + } + $computer_groups = @($computer_groups) +} + +# Validate order parameter +if ($null -ne $order -and $order -lt 1) { + Fail-Json -obj $result -message "Parameter 'order' must be a strictly positive integer." +} + +# Ensure RemoteDesktopServices module is loaded +if ((Get-Module -Name RemoteDesktopServices -ErrorAction SilentlyContinue) -eq $null) { + Import-Module -Name RemoteDesktopServices +} + +# Check if a CAP with the given name already exists +$cap_exist = Test-Path -Path "RDS:\GatewayServer\CAP\$name" + +if ($state -eq 'absent') { + if ($cap_exist) { + Remove-Item -Path "RDS:\GatewayServer\CAP\$name" -Recurse -WhatIf:$check_mode + $diff_text += "-[$name]" + $result.changed = $true + } +} else { + $diff_text_added_prefix = '' + if (-not $cap_exist) { + if ($null -eq $user_groups) { + Fail-Json -obj $result -message "User groups must be defined to create a new CAP." + } + + # Auth method is required when creating a new CAP. Set it to password by default. + if ($null -eq $auth_method) { + $auth_method = "password" + } + + # Create a new CAP + if (-not $check_mode) { + $CapArgs = @{ + Name = $name + UserGroupNames = $user_groups -join ';' + } + $return = Invoke-CimMethod -Namespace Root\CIMV2\TerminalServices -ClassName Win32_TSGatewayConnectionAuthorizationPolicy -MethodName Create -Arguments $CapArgs + if ($return.ReturnValue -ne 0) { + Fail-Json -obj $result -message "Failed to create CAP $name (code: $($return.ReturnValue))" + } + } + + $cap_exist = -not $check_mode + + $diff_text_added_prefix = '+' + $result.changed = $true + } + + $diff_text += "$diff_text_added_prefix[$name]`n" + + # We cannot configure a CAP that was created above in check mode as it won't actually exist + if($cap_exist) { + $cap = Get-CAP -Name $name + $wmi_cap = Get-CimInstance -ClassName Win32_TSGatewayConnectionAuthorizationPolicy -Namespace Root\CIMv2\TerminalServices -Filter "name='$($name)'" + + if ($state -in @('disabled', 'enabled')) { + $cap_enabled = $state -ne 'disabled' + if ($cap.Enabled -ne $cap_enabled) { + $diff_text += "-State = $(@('disabled', 'enabled')[[int]$cap.Enabled])`n+State = $state`n" + Set-CAPPropertyValue -Name $name -Property Status -Value ([int]$cap_enabled) -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + } + + if ($null -ne $auth_method -and $auth_method -ne $cap.AuthMethod) { + $diff_text += "-AuthMethod = $($cap.AuthMethod)`n+AuthMethod = $auth_method`n" + Set-CAPPropertyValue -Name $name -Property AuthMethod -Value ([array]::IndexOf($auth_methods_set, $auth_method)) -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + + if ($null -ne $order -and $order -ne $cap.EvaluationOrder) { + # Order cannot be greater than the total number of existing CAPs (InvalidArgument exception) + $cap_count = (Get-ChildItem -Path "RDS:\GatewayServer\CAP").Count + if($order -gt $cap_count) { + Add-Warning -obj $result -message "Given value '$order' for parameter 'order' is greater than the number of existing CAPs. The actual order will be capped to '$cap_count'." + $order = $cap_count + } + + $diff_text += "-Order = $($cap.EvaluationOrder)`n+Order = $order`n" + Set-CAPPropertyValue -Name $name -Property EvaluationOrder -Value $order -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + + if ($null -ne $session_timeout -and ($session_timeout -ne $cap.SessionTimeout -or $session_timeout_action -ne $cap.SessionTimeoutAction)) { + try { + Set-Item -Path "RDS:\GatewayServer\CAP\$name\SessionTimeout" ` + -Value $session_timeout ` + -SessionTimeoutAction ([array]::IndexOf($session_timeout_actions_set, $session_timeout_action)) ` + -ErrorAction Stop ` + -WhatIf:$check_mode + } catch { + Fail-Json -obj $result -message "Failed to set property ComputerGroupType of RAP ${name}: $($_.Exception.Message)" + } + + $diff_text += "-SessionTimeoutAction = $($cap.SessionTimeoutAction)`n+SessionTimeoutAction = $session_timeout_action`n" + $diff_text += "-SessionTimeout = $($cap.SessionTimeout)`n+SessionTimeout = $session_timeout`n" + $result.changed = $true + } + + if ($null -ne $idle_timeout -and $idle_timeout -ne $cap.IdleTimeout) { + $diff_text += "-IdleTimeout = $($cap.IdleTimeout)`n+IdleTimeout = $idle_timeout`n" + Set-CAPPropertyValue -Name $name -Property IdleTimeout -Value $idle_timeout -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + + if ($null -ne $allow_only_sdrts_servers -and $allow_only_sdrts_servers -ne $cap.AllowOnlySDRTSServers) { + $diff_text += "-AllowOnlySDRTSServers = $($cap.AllowOnlySDRTSServers)`n+AllowOnlySDRTSServers = $allow_only_sdrts_servers`n" + Set-CAPPropertyValue -Name $name -Property AllowOnlySDRTSServers -Value ([int]$allow_only_sdrts_servers) -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + + if ($null -ne $redirect_clipboard -and $redirect_clipboard -ne $cap.DeviceRedirection.Clipboard) { + $diff_text += "-RedirectClipboard = $($cap.DeviceRedirection.Clipboard)`n+RedirectClipboard = $redirect_clipboard`n" + Set-CAPPropertyValue -Name $name -Property "DeviceRedirection\Clipboard" -Value ([int]$redirect_clipboard) -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + + if ($null -ne $redirect_drives -and $redirect_drives -ne $cap.DeviceRedirection.DiskDrives) { + $diff_text += "-RedirectDrives = $($cap.DeviceRedirection.DiskDrives)`n+RedirectDrives = $redirect_drives`n" + Set-CAPPropertyValue -Name $name -Property "DeviceRedirection\DiskDrives" -Value ([int]$redirect_drives) -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + + if ($null -ne $redirect_printers -and $redirect_printers -ne $cap.DeviceRedirection.Printers) { + $diff_text += "-RedirectPrinters = $($cap.DeviceRedirection.Printers)`n+RedirectPrinters = $redirect_printers`n" + Set-CAPPropertyValue -Name $name -Property "DeviceRedirection\Printers" -Value ([int]$redirect_printers) -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + + if ($null -ne $redirect_serial -and $redirect_serial -ne $cap.DeviceRedirection.SerialPorts) { + $diff_text += "-RedirectSerial = $($cap.DeviceRedirection.SerialPorts)`n+RedirectSerial = $redirect_serial`n" + Set-CAPPropertyValue -Name $name -Property "DeviceRedirection\SerialPorts" -Value ([int]$redirect_serial) -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + + if ($null -ne $redirect_pnp -and $redirect_pnp -ne $cap.DeviceRedirection.PlugAndPlayDevices) { + $diff_text += "-RedirectPnP = $($cap.DeviceRedirection.PlugAndPlayDevices)`n+RedirectPnP = $redirect_pnp`n" + Set-CAPPropertyValue -Name $name -Property "DeviceRedirection\PlugAndPlayDevices" -Value ([int]$redirect_pnp) -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + + if ($null -ne $user_groups) { + $groups_to_remove = @($cap.UserGroups | Where-Object { $user_groups -notcontains $_ }) + $groups_to_add = @($user_groups | Where-Object { $cap.UserGroups -notcontains $_ }) + + $user_groups_diff = $null + foreach($group in $groups_to_add) { + if (-not $check_mode) { + $return = $wmi_cap | Invoke-CimMethod -MethodName AddUserGroupNames -Arguments @{ UserGroupNames = $group } + if ($return.ReturnValue -ne 0) { + Fail-Json -obj $result -message "Failed to add user group $($group) (code: $($return.ReturnValue))" + } + } + $user_groups_diff += " +$group`n" + $result.changed = $true + } + + foreach($group in $groups_to_remove) { + if (-not $check_mode) { + $return = $wmi_cap | Invoke-CimMethod -MethodName RemoveUserGroupNames -Arguments @{ UserGroupNames = $group } + if ($return.ReturnValue -ne 0) { + Fail-Json -obj $result -message "Failed to remove user group $($group) (code: $($return.ReturnValue))" + } + } + $user_groups_diff += " -$group`n" + $result.changed = $true + } + + if($user_groups_diff) { + $diff_text += "~UserGroups`n$user_groups_diff" + } + } + + if ($null -ne $computer_groups) { + $groups_to_remove = @($cap.ComputerGroups | Where-Object { $computer_groups -notcontains $_ }) + $groups_to_add = @($computer_groups | Where-Object { $cap.ComputerGroups -notcontains $_ }) + + $computer_groups_diff = $null + foreach($group in $groups_to_add) { + if (-not $check_mode) { + $return = $wmi_cap | Invoke-CimMethod -MethodName AddComputerGroupNames -Arguments @{ ComputerGroupNames = $group } + if ($return.ReturnValue -ne 0) { + Fail-Json -obj $result -message "Failed to add computer group $($group) (code: $($return.ReturnValue))" + } + } + $computer_groups_diff += " +$group`n" + $result.changed = $true + } + + foreach($group in $groups_to_remove) { + if (-not $check_mode) { + $return = $wmi_cap | Invoke-CimMethod -MethodName RemoveComputerGroupNames -Arguments @{ ComputerGroupNames = $group } + if ($return.ReturnValue -ne 0) { + Fail-Json -obj $result -message "Failed to remove computer group $($group) (code: $($return.ReturnValue))" + } + } + $computer_groups_diff += " -$group`n" + $result.changed = $true + } + + if($computer_groups_diff) { + $diff_text += "~ComputerGroups`n$computer_groups_diff" + } + } + } +} + +if ($diff_mode -and $result.changed -eq $true) { + $result.diff = @{ + prepared = $diff_text + } +} + +Exit-Json $result \ No newline at end of file diff --git a/lib/ansible/modules/windows/win_rds_cap.py b/lib/ansible/modules/windows/win_rds_cap.py new file mode 100644 index 00000000000..4f7eeb77773 --- /dev/null +++ b/lib/ansible/modules/windows/win_rds_cap.py @@ -0,0 +1,123 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Kevin Subileau (@ksubileau) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: win_rds_cap +short_description: Manage Connection Authorization Policies (CAP) on a Remote Desktop Gateway server +description: + - Creates, removes and configures a Remote Desktop connection authorization policy (RD CAP). + - A RD CAP allows you to specify the users who can connect to a Remote Desktop Gateway server. +version_added: "2.8" +author: + - Kevin Subileau (@ksubileau) +options: + name: + description: + - Name of the connection authorization policy. + required: yes + state: + description: + - The state of connection authorization policy. + - If C(absent) will ensure the policy is removed. + - If C(present) will ensure the policy is configured and exists. + - If C(enabled) will ensure the policy is configured, exists and enabled. + - If C(disabled) will ensure the policy is configured, exists, but disabled. + choices: [ absent, present, enabled, disabled ] + default: present + auth_method: + description: + - Specifies how the RD Gateway server authenticates users. + - When a new CAP is created, the default value is C(password). + choices: [ password, smartcard, both, none ] + order: + description: + - Evaluation order of the policy. + - The CAP in which I(order) is set to a value of '1' is evaluated first. + - By default, a newly created CAP will take the first position. + - If the given value exceed the total number of existing policies, + the policy will take the last position but the evaluation order + will be capped to this number. + type: int + session_timeout: + description: + - The maximum time, in minutes, that a session can be idle. + - A value of zero disables session timeout. + type: int + session_timeout_action: + description: + - The action the server takes when a session times out. + - 'C(disconnect): disconnect the session.' + - 'C(reauth): silently reauthenticate and reauthorize the session.' + choices: [ disconnect, reauth ] + default: disconnect + idle_timeout: + description: + - Specifies the time interval, in minutes, after which an idle session is disconnected. + - A value of zero disables idle timeout. + type: int + allow_only_sdrts_servers: + description: + - Specifies whether connections are allowed only to Remote Desktop Session Host servers that + enforce Remote Desktop Gateway redirection policy. + type: bool + user_groups: + description: + - A list of user groups that is allowed to connect to the Remote Gateway server. + - Required when a new CAP is created. + type: list + computer_groups: + description: + - A list of computer groups that is allowed to connect to the Remote Gateway server. + type: list + redirect_clipboard: + description: + - Allow clipboard redirection. + type: bool + redirect_drives: + description: + - Allow disk drive redirection. + type: bool + redirect_printers: + description: + - Allow printers redirection. + type: bool + redirect_serial: + description: + - Allow serial port redirection. + type: bool + redirect_pnp: + description: + - Allow Plug and Play devices redirection. + type: bool +requirements: + - Windows Server 2008R2 (6.1) or higher. + - The Windows Feature "RDS-Gateway" must be enabled. +''' + +EXAMPLES = r''' +- name: Create a new RDS CAP with a 30 minutes timeout and clipboard redirection enabled + win_rds_cap: + name: My CAP + user_groups: + - BUILTIN\users + session_timeout: 30 + session_timeout_action: disconnect + allow_only_sdrts_servers: true + redirect_clipboard: true + redirect_drives: false + redirect_printers: false + redirect_serial: false + redirect_pnp: false + state: enabled +''' + +RETURN = r''' +''' diff --git a/lib/ansible/modules/windows/win_rds_rap.ps1 b/lib/ansible/modules/windows/win_rds_rap.ps1 new file mode 100644 index 00000000000..d21108d8196 --- /dev/null +++ b/lib/ansible/modules/windows/win_rds_rap.ps1 @@ -0,0 +1,282 @@ +#!powershell + +# Copyright: (c) 2018, Kevin Subileau (@ksubileau) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#Requires -Module Ansible.ModuleUtils.Legacy +#Requires -Module Ansible.ModuleUtils.SID + +$ErrorActionPreference = "Stop" + +# List of authentication methods as string. Used for parameter validation and conversion to integer flag, so order is important! +$computer_group_types = @("rdg_group", "ad_network_resource_group", "allow_any") +$computer_group_types_wmi = @{rdg_group = "RG"; ad_network_resource_group = "CG"; allow_any = "ALL"} + +$params = Parse-Args -arguments $args -supports_check_mode $true +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false +$diff_mode = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false + +$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true +$description = Get-AnsibleParam -obj $params -name "description" -type "str" +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent","present","enabled","disabled" +$computer_group_type = Get-AnsibleParam -obj $params -name "computer_group_type" -type "str" -validateset $computer_group_types +$computer_group = Get-AnsibleParam -obj $params -name "computer_group" -type "str" -failifempty ($computer_group_type -eq "ad_network_resource_group" -or $computer_group_type -eq "rdg_group") +$user_groups = Get-AnsibleParam -obj $params -name "user_groups" -type "list" +$allowed_ports = Get-AnsibleParam -obj $params -name "allowed_ports" -type "list" + + +function Get-RAP([string] $name) { + $rap_path = "RDS:\GatewayServer\RAP\$name" + $rap = @{ + Name = $name + } + + # Fetch RAP properties + Get-ChildItem -Path $rap_path | ForEach-Object { $rap.Add($_.Name,$_.CurrentValue) } + # Convert boolean values + $rap.Enabled = $rap.Status -eq 1 + $rap.Remove("Status") + + # Convert computer group name from UPN to Down-Level Logon format + if($rap.ComputerGroupType -ne 2) { + $rap.ComputerGroup = Convert-FromSID -sid (Convert-ToSID -account_name $rap.ComputerGroup) + } + + # Convert multiple choices values + $rap.ComputerGroupType = $computer_group_types[$rap.ComputerGroupType] + + # Convert allowed ports from string to list + if($rap.PortNumbers -eq '*') { + $rap.PortNumbers = @("any") + } else { + $rap.PortNumbers = @($rap.PortNumbers -split ',') + } + + # Fetch RAP user groups in Down-Level Logon format + $rap.UserGroups = @( + Get-ChildItem -Path "$rap_path\UserGroups" | + Select-Object -ExpandProperty Name | + ForEach-Object { Convert-FromSID -sid (Convert-ToSID -account_name $_) } + ) + + return $rap +} + +function Set-RAPPropertyValue { + [CmdletBinding(SupportsShouldProcess=$true)] + param ( + [Parameter(Mandatory=$true)] + [string] $name, + [Parameter(Mandatory=$true)] + [string] $property, + [Parameter(Mandatory=$true)] + $value, + [Parameter()] + $resultobj = @{} + ) + + $rap_path = "RDS:\GatewayServer\RAP\$name" + + try { + Set-Item -Path "$rap_path\$property" -Value $value -ErrorAction stop + } catch { + Fail-Json -obj $resultobj -message "Failed to set property $property of RAP ${name}: $($_.Exception.Message)" + } +} + +$result = @{ + changed = $false +} +$diff_text = $null + +# Validate RAP name +if ($name -match "[*/\\;:?`"<>|\t]+") { + Fail-Json -obj $result -message "Invalid character in RAP name." +} + +# Validate user groups +if ($null -ne $user_groups) { + if ($user_groups.Count -lt 1) { + Fail-Json -obj $result -message "Parameter 'user_groups' cannot be an empty list." + } + + $user_groups = $user_groups | ForEach-Object { + $group = $_ + # Test that the group is resolvable on the local machine + $sid = Convert-ToSID -account_name $group + if (!$sid) { + Fail-Json -obj $result -message "$group is not a valid user group on the host machine or domain." + } + + # Return the normalized group name in Down-Level Logon format + Convert-FromSID -sid $sid + } + $user_groups = @($user_groups) +} + +# Validate computer group parameter +if ($computer_group_type -eq "allow_any" -and $null -ne $computer_group) { + Add-Warning -obj $result -message "Parameter 'computer_group' ignored because the computer_group_type is set to allow_any." +} elseif ($computer_group_type -eq "rdg_group" -and -not (Test-Path -Path "RDS:\GatewayServer\GatewayManagedComputerGroups\$computer_group")) { + Fail-Json -obj $result -message "$computer_group is not a valid gateway managed computer group" +} elseif ($computer_group_type -eq "ad_network_resource_group") { + $sid = Convert-ToSID -account_name $computer_group + if (!$sid) { + Fail-Json -obj $result -message "$computer_group is not a valid computer group on the host machine or domain." + } + # Ensure the group name is in Down-Level Logon format + $computer_group = Convert-FromSID -sid $sid +} + +# Validate port numbers +if ($null -ne $allowed_ports) { + foreach ($port in $allowed_ports) { + if (-not ($port -eq "any" -or ($port -is [int] -and $port -ge 1 -and $port -le 65535))) { + Fail-Json -obj $result -message "$port is not a valid port number." + } + } +} + +# Ensure RemoteDesktopServices module is loaded +if ((Get-Module -Name RemoteDesktopServices -ErrorAction SilentlyContinue) -eq $null) { + Import-Module -Name RemoteDesktopServices +} + +# Check if a RAP with the given name already exists +$rap_exist = Test-Path -Path "RDS:\GatewayServer\RAP\$name" + +if ($state -eq 'absent') { + if ($rap_exist) { + Remove-Item -Path "RDS:\GatewayServer\RAP\$name" -Recurse -WhatIf:$check_mode + $diff_text += "-[$name]" + $result.changed = $true + } +} else { + $diff_text_added_prefix = '' + if (-not $rap_exist) { + if ($null -eq $user_groups) { + Fail-Json -obj $result -message "User groups must be defined to create a new RAP." + } + + # Computer group type is required when creating a new RAP. Set it to allow connect to any resource by default. + if ($null -eq $computer_group_type) { + $computer_group_type = "allow_any" + } + + # Create a new RAP + if (-not $check_mode) { + $RapArgs = @{ + Name = $name + ResourceGroupType = 'ALL' + UserGroupNames = $user_groups -join ';' + ProtocolNames = 'RDP' + PortNumbers = '*' + } + $return = Invoke-CimMethod -Namespace Root\CIMV2\TerminalServices -ClassName Win32_TSGatewayResourceAuthorizationPolicy -MethodName Create -Arguments $RapArgs + if ($return.ReturnValue -ne 0) { + Fail-Json -obj $result -message "Failed to create RAP $name (code: $($return.ReturnValue))" + } + } + $rap_exist = -not $check_mode + + $diff_text_added_prefix = '+' + $result.changed = $true + } + + $diff_text += "$diff_text_added_prefix[$name]`n" + + # We cannot configure a RAP that was created above in check mode as it won't actually exist + if($rap_exist) { + $rap = Get-RAP -Name $name + $wmi_rap = Get-CimInstance -ClassName Win32_TSGatewayResourceAuthorizationPolicy -Namespace Root\CIMv2\TerminalServices -Filter "name='$($name)'" + + if ($state -in @('disabled', 'enabled')) { + $rap_enabled = $state -ne 'disabled' + if ($rap.Enabled -ne $rap_enabled) { + $diff_text += "-State = $(@('disabled', 'enabled')[[int]$rap.Enabled])`n+State = $state`n" + Set-RAPPropertyValue -Name $name -Property Status -Value ([int]$rap_enabled) -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + } + + if ($null -ne $description -and $description -ne $rap.Description) { + Set-RAPPropertyValue -Name $name -Property Description -Value $description -ResultObj $result -WhatIf:$check_mode + $diff_text += "-Description = $($rap.Description)`n+Description = $description`n" + $result.changed = $true + } + + if ($null -ne $allowed_ports -and @(Compare-Object $rap.PortNumbers $allowed_ports -SyncWindow 0).Count -ne 0) { + $diff_text += "-AllowedPorts = [$($rap.PortNumbers -join ',')]`n+AllowedPorts = [$($allowed_ports -join ',')]`n" + if ($allowed_ports -contains 'any') { $allowed_ports = '*' } + Set-RAPPropertyValue -Name $name -Property PortNumbers -Value $allowed_ports -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + + if ($null -ne $computer_group_type -and $computer_group_type -ne $rap.ComputerGroupType) { + $diff_text += "-ComputerGroupType = $($rap.ComputerGroupType)`n+ComputerGroupType = $computer_group_type`n" + if ($computer_group_type -ne "allow_any") { + $diff_text += "+ComputerGroup = $computer_group`n" + } + $return = $wmi_rap | Invoke-CimMethod -MethodName SetResourceGroup -Arguments @{ + ResourceGroupName = $computer_group + ResourceGroupType = $computer_group_types_wmi.$($computer_group_type) + } + if ($return.ReturnValue -ne 0) { + Fail-Json -obj $result -message "Failed to set computer group type to $($computer_group_type) (code: $($return.ReturnValue))" + } + + $result.changed = $true + + } elseif ($null -ne $computer_group -and $computer_group -ne $rap.ComputerGroup) { + $diff_text += "-ComputerGroup = $($rap.ComputerGroup)`n+ComputerGroup = $computer_group`n" + $return = $wmi_rap | Invoke-CimMethod -MethodName SetResourceGroup -Arguments @{ + ResourceGroupName = $computer_group + ResourceGroupType = $computer_group_types_wmi.$($rap.ComputerGroupType) + } + if ($return.ReturnValue -ne 0) { + Fail-Json -obj $result -message "Failed to set computer group name to $($computer_group) (code: $($return.ReturnValue))" + } + $result.changed = $true + } + + if ($null -ne $user_groups) { + $groups_to_remove = @($rap.UserGroups | Where-Object { $user_groups -notcontains $_ }) + $groups_to_add = @($user_groups | Where-Object { $rap.UserGroups -notcontains $_ }) + + $user_groups_diff = $null + foreach($group in $groups_to_add) { + if (-not $check_mode) { + $return = $wmi_rap | Invoke-CimMethod -MethodName AddUserGroupNames -Arguments @{ UserGroupNames = $group } + if ($return.ReturnValue -ne 0) { + Fail-Json -obj $result -message "Failed to add user group $($group) (code: $($return.ReturnValue))" + } + } + $user_groups_diff += " +$group`n" + $result.changed = $true + } + + foreach($group in $groups_to_remove) { + if (-not $check_mode) { + $return = $wmi_rap | Invoke-CimMethod -MethodName RemoveUserGroupNames -Arguments @{ UserGroupNames = $group } + if ($return.ReturnValue -ne 0) { + Fail-Json -obj $result -message "Failed to remove user group $($group) (code: $($return.ReturnValue))" + } + } + $user_groups_diff += " -$group`n" + $result.changed = $true + } + + if($user_groups_diff) { + $diff_text += "~UserGroups`n$user_groups_diff" + } + } + } +} + +if ($diff_mode -and $result.changed -eq $true) { + $result.diff = @{ + prepared = $diff_text + } +} + +Exit-Json $result \ No newline at end of file diff --git a/lib/ansible/modules/windows/win_rds_rap.py b/lib/ansible/modules/windows/win_rds_rap.py new file mode 100644 index 00000000000..c8c9176afad --- /dev/null +++ b/lib/ansible/modules/windows/win_rds_rap.py @@ -0,0 +1,81 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Kevin Subileau (@ksubileau) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: win_rds_rap +short_description: Manage Resource Authorization Policies (RAP) on a Remote Desktop Gateway server +description: + - Creates, removes and configures a Remote Desktop resource authorization policy (RD RAP). + - A RD RAP allows you to specify the network resources (computers) that users can connect + to remotely through a Remote Desktop Gateway server. +version_added: "2.8" +author: + - Kevin Subileau (@ksubileau) +options: + name: + description: + - Name of the resource authorization policy. + required: yes + state: + description: + - The state of resource authorization policy. + - If C(absent) will ensure the policy is removed. + - If C(present) will ensure the policy is configured and exists. + - If C(enabled) will ensure the policy is configured, exists and enabled. + - If C(disabled) will ensure the policy is configured, exists, but disabled. + choices: [ absent, present, enabled, disabled ] + default: present + description: + description: + - Optionnal description of the resource authorization policy. + user_groups: + description: + - List of user groups that are associated with this resource authorization policy (RAP). + A user must belong to one of these groups to access the RD Gateway server. + - Required when a new RAP is created. + type: list + allowed_ports: + description: + - List of port numbers through which connections are allowed for this policy. + - To allow connections through any port, specify 'any'. + type: list + computer_group_type: + description: + - 'The computer group type:' + - 'C(rdg_group): RD Gateway-managed group' + - 'C(ad_network_resource_group): Active Directory Domain Services network resource group' + - 'C(allow_any): Allow users to connect to any network resource.' + choices: [ rdg_group, ad_network_resource_group, allow_any ] + computer_group: + description: + - The computer group name that is associated with this resource authorization policy (RAP). + - This is required when I(computer_group_type) is C(rdg_group) or C(ad_network_resource_group). +requirements: + - Windows Server 2008R2 (6.1) or higher. + - The Windows Feature "RDS-Gateway" must be enabled. +''' + +EXAMPLES = r''' +- name: Create a new RDS RAP + win_rds_rap: + name: My RAP + description: 'Allow all users to connect to any resource through ports 3389 and 3390' + user_groups: + - BUILTIN\users + computer_group_type: allow_any + allowed_ports: + - 3389 + - 3390 + state: enabled +''' + +RETURN = r''' +''' diff --git a/lib/ansible/modules/windows/win_rds_settings.ps1 b/lib/ansible/modules/windows/win_rds_settings.ps1 new file mode 100644 index 00000000000..ebd7a342869 --- /dev/null +++ b/lib/ansible/modules/windows/win_rds_settings.ps1 @@ -0,0 +1,100 @@ +#!powershell + +# Copyright: (c) 2018, Kevin Subileau (@ksubileau) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#Requires -Module Ansible.ModuleUtils.Legacy + +$ErrorActionPreference = "Stop" + +# List of ssl bridging methods as string. Used for parameter validation and conversion to integer flag, so order is important! +$ssl_bridging_methods = @("none", "https_http", "https_https") + +$params = Parse-Args -arguments $args -supports_check_mode $true +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false +$diff_mode = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false + +$certificate = Get-AnsibleParam $params -name "certificate_hash" -type "str" +$max_connections = Get-AnsibleParam $params -name "max_connections" -type "int" +$ssl_bridging = Get-AnsibleParam -obj $params -name "ssl_bridging" -type "str" -validateset $ssl_bridging_methods +$enable_only_messaging_capable_clients = Get-AnsibleParam $params -name "enable_only_messaging_capable_clients" -type "bool" + +$result = @{ + changed = $false +} +$diff_text = $null + +# Ensure RemoteDesktopServices module is loaded +if ((Get-Module -Name RemoteDesktopServices -ErrorAction SilentlyContinue) -eq $null) { + Import-Module -Name RemoteDesktopServices +} + +if ($null -ne $certificate) +{ + # Validate cert path + $cert_path = "cert:\LocalMachine\My\$certificate" + If (-not (Test-Path $cert_path) ) + { + Fail-Json -obj $result -message "Unable to locate certificate at $cert_path" + } + + # Get current certificate hash + $current_cert = (Get-Item -Path "RDS:\GatewayServer\SSLCertificate\Thumbprint").CurrentValue + if ($current_cert -ne $certificate) { + Set-Item -Path "RDS:\GatewayServer\SSLCertificate\Thumbprint" -Value $certificate -WhatIf:$check_mode + $diff_text += "-Certificate = $current_cert`n+Certificate = $certificate`n" + $result.changed = $true + } +} + +if ($null -ne $max_connections) +{ + # Set the correct value for unlimited connections + # TODO Use a more explicit value, maybe a string (ex: "max", "none" or "unlimited") ? + If ($max_connections -eq -1) + { + $max_connections = (Get-Item -Path "RDS:\GatewayServer\MaxConnectionsAllowed").CurrentValue + } + + # Get current connections limit + $current_max_connections = (Get-Item -Path "RDS:\GatewayServer\MaxConnections").CurrentValue + if ($current_max_connections -ne $max_connections) { + Set-Item -Path "RDS:\GatewayServer\MaxConnections" -Value $max_connections -WhatIf:$check_mode + $diff_text += "-MaxConnections = $current_max_connections`n+MaxConnections = $max_connections`n" + $result.changed = $true + } +} + +if ($null -ne $ssl_bridging) +{ + $current_ssl_bridging = (Get-Item -Path "RDS:\GatewayServer\SSLBridging").CurrentValue + # Convert the integer value to its representative string + $current_ssl_bridging_str = $ssl_bridging_methods[$current_ssl_bridging] + + if ($current_ssl_bridging_str -ne $ssl_bridging) { + Set-Item -Path "RDS:\GatewayServer\SSLBridging" -Value ([array]::IndexOf($ssl_bridging_methods, $ssl_bridging)) -WhatIf:$check_mode + $diff_text += "-SSLBridging = $current_ssl_bridging_str`n+SSLBridging = $ssl_bridging`n" + $result.changed = $true + } +} + +if ($null -ne $enable_only_messaging_capable_clients) +{ + $current_enable_only_messaging_capable_clients = (Get-Item -Path "RDS:\GatewayServer\EnableOnlyMessagingCapableClients").CurrentValue + # Convert the integer value to boolean + $current_enable_only_messaging_capable_clients = $current_enable_only_messaging_capable_clients -eq 1 + + if ($current_enable_only_messaging_capable_clients -ne $enable_only_messaging_capable_clients) { + Set-Item -Path "RDS:\GatewayServer\EnableOnlyMessagingCapableClients" -Value ([int]$enable_only_messaging_capable_clients) -WhatIf:$check_mode + $diff_text += "-EnableOnlyMessagingCapableClients = $current_enable_only_messaging_capable_clients`n+EnableOnlyMessagingCapableClients = $enable_only_messaging_capable_clients`n" + $result.changed = $true + } +} + +if ($diff_mode -and $result.changed -eq $true) { + $result.diff = @{ + prepared = $diff_text + } +} + +Exit-Json $result \ No newline at end of file diff --git a/lib/ansible/modules/windows/win_rds_settings.py b/lib/ansible/modules/windows/win_rds_settings.py new file mode 100644 index 00000000000..653e645d1c3 --- /dev/null +++ b/lib/ansible/modules/windows/win_rds_settings.py @@ -0,0 +1,55 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Kevin Subileau (@ksubileau) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: win_rds_settings +short_description: Manage main settings of a Remote Desktop Gateway server +description: + - Configure general settings of a Remote Desktop Gateway server. +version_added: "2.8" +author: + - Kevin Subileau (@ksubileau) +options: + certificate_hash: + description: + - Certificate hash (thumbprint) for the Remote Desktop Gateway server. The certificate hash is the unique identifier for the certificate. + max_connections: + description: + - The maximum number of connections allowed. + - If set to C(0), no new connections are allowed. + - If set to C(-1), the number of connections is unlimited. + ssl_bridging: + description: + - Specifies whether to use SSL Bridging. + - 'C(none): no SSL bridging.' + - 'C(https_http): HTTPS-HTTP bridging.' + - 'C(https_https): HTTPS-HTTPS bridging.' + choices: [ none, https_http, https_https ] + enable_only_messaging_capable_clients: + description: + - If enabled, only clients that support logon messages and administrator messages can connect. + type: bool +requirements: + - Windows Server 2008R2 (6.1) or higher. + - The Windows Feature "RDS-Gateway" must be enabled. +''' + +EXAMPLES = r''' +- name: Configure the Remote Desktop Gateway + win_rds_settings: + certificate_hash: B0D0FA8408FC67B230338FCA584D03792DA73F4C + max_connections: 50 + notify: + - Restart TSGateway service +''' + +RETURN = r''' +''' diff --git a/test/integration/targets/win_rds_cap/aliases b/test/integration/targets/win_rds_cap/aliases new file mode 100644 index 00000000000..215e0b06920 --- /dev/null +++ b/test/integration/targets/win_rds_cap/aliases @@ -0,0 +1 @@ +shippable/windows/group4 diff --git a/test/integration/targets/win_rds_cap/defaults/main.yml b/test/integration/targets/win_rds_cap/defaults/main.yml new file mode 100644 index 00000000000..174d09e3d7a --- /dev/null +++ b/test/integration/targets/win_rds_cap/defaults/main.yml @@ -0,0 +1 @@ +test_win_rds_cap_name: Ansible Test CAP \ No newline at end of file diff --git a/test/integration/targets/win_rds_cap/tasks/main.yml b/test/integration/targets/win_rds_cap/tasks/main.yml new file mode 100644 index 00000000000..2ec4d1adacd --- /dev/null +++ b/test/integration/targets/win_rds_cap/tasks/main.yml @@ -0,0 +1,30 @@ +--- +# Cannot use win_feature to install RDS on Server 2008. +# Run a brief check and skip hosts that don't support +# that operation +- name: check if win_feature will work on test host + win_command: powershell.exe "exit (-not (Get-Command -Name Add-WindowsFeature -ErrorAction SilentlyContinue))" + register: module_available + failed_when: False + +# Run actual tests +- block: + - name: ensure Remote Desktop Gateway services are installed + win_feature: + name: RDS-Gateway,RDS-Licensing,RDS-RD-Server + state: present + include_management_tools: True + register: rds_install + + - name: reboot server if needed + win_reboot: + post_reboot_delay: 10 + when: rds_install.reboot_required + + - include_tasks: tests.yml + + always: + - name: delete all CAPs + win_shell: Import-Module RemoteDesktopServices; Remove-Item -Path RDS:\GatewayServer\CAP\* -Recurse + + when: module_available.rc == 0 diff --git a/test/integration/targets/win_rds_cap/tasks/tests.yml b/test/integration/targets/win_rds_cap/tasks/tests.yml new file mode 100644 index 00000000000..1c84cc8741a --- /dev/null +++ b/test/integration/targets/win_rds_cap/tasks/tests.yml @@ -0,0 +1,264 @@ +--- +- name: test create a new CAP (check mode) + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + user_groups: + - administrators + - users@builtin + state: present + register: new_cap_check + check_mode: yes + +- name: get result of create a new CAP (check mode) + win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\CAP\{{ test_win_rds_cap_name }}") + register: new_cap_actual_check + +- name: assert results of create a new CAP (check mode) + assert: + that: + - new_cap_check.changed == true + - new_cap_actual_check.stdout_lines[0] == "False" + +- name: test create a new CAP + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + user_groups: + - administrators + - users@builtin + state: present + register: new_cap + +- name: get result of create a new CAP + win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\CAP\{{ test_win_rds_cap_name }}") + register: new_cap_actual + +- name: assert results of create a new CAP + assert: + that: + - new_cap.changed == true + - new_cap_actual.stdout_lines[0] == "True" + +- name: test create a new CAP (idempotent) + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + user_groups: + - administrators + - users@builtin + state: present + register: new_cap_again + +- name: get result of create a new CAP (idempotent) + win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\CAP\{{ test_win_rds_cap_name }}") + register: new_cap_actual_again + +- name: assert results of create a new CAP (idempotent) + assert: + that: + - new_cap_again.changed == false + - new_cap_actual_again.stdout_lines[0] == "True" + +- name: test edit a CAP + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + user_groups: + # Test with different group name formats + - users@builtin + - .\guests + computer_groups: + - administrators + auth_method: both + session_timeout: 20 + session_timeout_action: reauth + allow_only_sdrts_servers: true + idle_timeout: 10 + redirect_clipboard: false + redirect_drives: false + redirect_printers: false + redirect_serial: false + redirect_pnp: false + state: disabled + register: edit_cap + +- name: get result of edit a CAP + win_shell: | + Import-Module RemoteDesktopServices; + $cap_path = "RDS:\GatewayServer\CAP\{{ test_win_rds_cap_name }}" + $cap = @{} + Get-ChildItem -Path "$cap_path" | foreach { $cap.Add($_.Name,$_.CurrentValue) } + $cap.DeviceRedirection = @{} + Get-ChildItem -Path "$cap_path\DeviceRedirection" | foreach { $cap.DeviceRedirection.Add($_.Name, ($_.CurrentValue -eq 1)) } + $cap.UserGroups = @(Get-ChildItem -Path "$cap_path\UserGroups" | Select -ExpandProperty Name) + $cap.ComputerGroups = @(Get-ChildItem -Path "$cap_path\ComputerGroups" | Select -ExpandProperty Name) + $cap | ConvertTo-Json + register: edit_cap_actual_json + +- name: parse result of edit a CAP. + set_fact: + edit_cap_actual: '{{ edit_cap_actual_json.stdout | from_json }}' + +- name: assert results of edit a CAP + assert: + that: + - edit_cap.changed == true + - edit_cap_actual.Status == "0" + - edit_cap_actual.EvaluationOrder == "1" + - edit_cap_actual.AllowOnlySDRTSServers == "1" + - edit_cap_actual.AuthMethod == "3" + - edit_cap_actual.IdleTimeout == "10" + - edit_cap_actual.SessionTimeoutAction == "1" + - edit_cap_actual.SessionTimeout == "20" + - edit_cap_actual.DeviceRedirection.Clipboard == false + - edit_cap_actual.DeviceRedirection.DiskDrives == false + - edit_cap_actual.DeviceRedirection.PlugAndPlayDevices == false + - edit_cap_actual.DeviceRedirection.Printers == false + - edit_cap_actual.DeviceRedirection.SerialPorts == false + - edit_cap_actual.UserGroups | length == 2 + - edit_cap_actual.UserGroups[0] == "Users@BUILTIN" + - edit_cap_actual.UserGroups[1] == "Guests@BUILTIN" + - edit_cap_actual.ComputerGroups | length == 1 + - edit_cap_actual.ComputerGroups[0] == "Administrators@BUILTIN" + +- name: test remove all computer groups of CAP + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + computer_groups: [] + register: remove_computer_groups_cap + +- name: get result of remove all computer groups of CAP + win_shell: | + Import-Module RemoteDesktopServices; + $cap_path = "RDS:\GatewayServer\CAP\{{ test_win_rds_cap_name }}" + Write-Host @(Get-ChildItem -Path "$cap_path\ComputerGroups" | Select -ExpandProperty Name).Count + register: remove_computer_groups_cap_actual + +- name: assert results of remove all computer groups of CAP + assert: + that: + - remove_computer_groups_cap.changed == true + - remove_computer_groups_cap_actual.stdout_lines[0] == "0" + +- name: test create a CAP in second position + win_rds_cap: + name: '{{ test_win_rds_cap_name }} Second' + user_groups: + - users@builtin + order: 2 + state: present + register: second_cap + +- name: get result of create a CAP in second position + win_shell: Import-Module RemoteDesktopServices; Write-Host (Get-Item "RDS:\GatewayServer\CAP\{{ test_win_rds_cap_name }} Second\EvaluationOrder").CurrentValue + register: second_cap_actual + +- name: assert results of create a CAP in second position + assert: + that: + - second_cap.changed == true + - second_cap.warnings is not defined + - second_cap_actual.stdout_lines[0] == "2" + +- name: test create a CAP with order greater than existing CAP count + win_rds_cap: + name: '{{ test_win_rds_cap_name }} Last' + user_groups: + - users@builtin + order: 50 + state: present + register: cap_big_order + +- name: get result of create a CAP with order greater than existing CAP count + win_shell: Import-Module RemoteDesktopServices; Write-Host (Get-Item "RDS:\GatewayServer\CAP\{{ test_win_rds_cap_name }} Last\EvaluationOrder").CurrentValue + register: cap_big_order_actual + +- name: assert results of create a CAP with order greater than existing CAP count + assert: + that: + - cap_big_order.changed == true + - cap_big_order.warnings | length == 1 + - cap_big_order_actual.stdout_lines[0] == "3" + +- name: test remove CAP (check mode) + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + state: absent + register: remove_cap_check + check_mode: yes + +- name: get result of remove CAP (check mode) + win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\CAP\{{ test_win_rds_cap_name }}") + register: remove_cap_actual_check + +- name: assert results of remove CAP (check mode) + assert: + that: + - remove_cap_check.changed == true + - remove_cap_actual_check.stdout_lines[0] == "True" + +- name: test remove CAP + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + state: absent + register: remove_cap_check + +- name: get result of remove CAP + win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\CAP\{{ test_win_rds_cap_name }}") + register: remove_cap_actual_check + +- name: assert results of remove CAP + assert: + that: + - remove_cap_check.changed == true + - remove_cap_actual_check.stdout_lines[0] == "False" + +- name: test remove CAP (idempotent) + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + state: absent + register: remove_cap_check + +- name: get result of remove CAP (idempotent) + win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\CAP\{{ test_win_rds_cap_name }}") + register: remove_cap_actual_check + +- name: assert results of remove CAP (idempotent) + assert: + that: + - remove_cap_check.changed == false + - remove_cap_actual_check.stdout_lines[0] == "False" + +- name: fail when create a new CAP without user group + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + state: present + register: new_cap_without_group + check_mode: yes + failed_when: "new_cap_without_group.msg != 'User groups must be defined to create a new CAP.'" + +- name: fail when create a new CAP with an empty user group list + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + user_groups: [] + state: present + register: new_cap_empty_group_list + check_mode: yes + failed_when: "new_cap_empty_group_list.msg is not search('cannot be an empty list')" + +- name: fail when create a new CAP with an invalid user group + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + user_groups: + - fake_group + state: present + register: new_cap_invalid_user_group + check_mode: yes + failed_when: new_cap_invalid_user_group.changed != false or new_cap_invalid_user_group.msg is not search('is not a valid account') + +- name: fail when create a new CAP with an invalid computer group + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + computer_groups: + - fake_group + state: present + register: new_cap_invalid_computer_group + check_mode: yes + failed_when: new_cap_invalid_computer_group.changed != false or new_cap_invalid_computer_group.msg is not search('is not a valid account') diff --git a/test/integration/targets/win_rds_rap/aliases b/test/integration/targets/win_rds_rap/aliases new file mode 100644 index 00000000000..215e0b06920 --- /dev/null +++ b/test/integration/targets/win_rds_rap/aliases @@ -0,0 +1 @@ +shippable/windows/group4 diff --git a/test/integration/targets/win_rds_rap/defaults/main.yml b/test/integration/targets/win_rds_rap/defaults/main.yml new file mode 100644 index 00000000000..ada12c20c38 --- /dev/null +++ b/test/integration/targets/win_rds_rap/defaults/main.yml @@ -0,0 +1 @@ +test_win_rds_rap_name: Ansible Test RAP \ No newline at end of file diff --git a/test/integration/targets/win_rds_rap/tasks/main.yml b/test/integration/targets/win_rds_rap/tasks/main.yml new file mode 100644 index 00000000000..081cd69cadd --- /dev/null +++ b/test/integration/targets/win_rds_rap/tasks/main.yml @@ -0,0 +1,30 @@ +--- +# Cannot use win_feature to install RDS on Server 2008. +# Run a brief check and skip hosts that don't support +# that operation +- name: check if win_feature will work on test host + win_command: powershell.exe "exit (-not (Get-Command -Name Add-WindowsFeature -ErrorAction SilentlyContinue))" + register: module_available + failed_when: False + +# Run actual tests +- block: + - name: ensure Remote Desktop Gateway services are installed + win_feature: + name: RDS-Gateway,RDS-Licensing,RDS-RD-Server + state: present + include_management_tools: True + register: rds_install + + - name: reboot server if needed + win_reboot: + post_reboot_delay: 10 + when: rds_install.reboot_required + + - include_tasks: tests.yml + + always: + - name: delete all RAPs + win_shell: Import-Module RemoteDesktopServices; Remove-Item -Path RDS:\GatewayServer\RAP\* -Recurse + + when: module_available.rc == 0 diff --git a/test/integration/targets/win_rds_rap/tasks/tests.yml b/test/integration/targets/win_rds_rap/tasks/tests.yml new file mode 100644 index 00000000000..a8645965b79 --- /dev/null +++ b/test/integration/targets/win_rds_rap/tasks/tests.yml @@ -0,0 +1,254 @@ +--- +- name: test create a new RAP (check mode) + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + user_groups: + - administrators + - users@builtin + state: present + register: new_rap_check + check_mode: yes + +- name: get result of create a new RAP (check mode) + win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\RAP\{{ test_win_rds_rap_name }}") + register: new_rap_actual_check + +- name: assert results of create a new RAP (check mode) + assert: + that: + - new_rap_check.changed == true + - new_rap_actual_check.stdout_lines[0] == "False" + +- name: test create a new RAP + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + user_groups: + - administrators + - users@builtin + state: present + register: new_rap + +- name: get result of create a new RAP + win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\RAP\{{ test_win_rds_rap_name }}") + register: new_rap_actual + +- name: assert results of create a new RAP + assert: + that: + - new_rap.changed == true + - new_rap_actual.stdout_lines[0] == "True" + +- name: test create a new RAP (idempotent) + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + user_groups: + - administrators + - users@builtin + state: present + register: new_rap_again + +- name: get result of create a new RAP (idempotent) + win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\RAP\{{ test_win_rds_rap_name }}") + register: new_rap_actual_again + +- name: assert results of create a new RAP (idempotent) + assert: + that: + - new_rap_again.changed == false + - new_rap_actual_again.stdout_lines[0] == "True" + +- name: test edit a RAP + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + description: 'Description of {{ test_win_rds_rap_name }}' + user_groups: + # Test with different group name formats + - users@builtin + - .\guests + computer_group_type: ad_network_resource_group + computer_group: administrators + allowed_ports: + - 3389 + - 3390 + - 3391 + state: disabled + register: edit_rap + +- name: get result of edit a RAP + win_shell: | + Import-Module RemoteDesktopServices; + $rap_path = "RDS:\GatewayServer\RAP\{{ test_win_rds_rap_name }}" + $rap = @{} + Get-ChildItem -Path "$rap_path" | foreach { $rap.Add($_.Name,$_.CurrentValue) } + $rap.UserGroups = @(Get-ChildItem -Path "$rap_path\UserGroups" | Select -ExpandProperty Name) + $rap | ConvertTo-Json + register: edit_rap_actual_json + +- name: parse result of edit a RAP. + set_fact: + edit_rap_actual: '{{ edit_rap_actual_json.stdout | from_json }}' + +- name: assert results of edit a RAP + assert: + that: + - edit_rap.changed == true + - edit_rap_actual.Status == "0" + - edit_rap_actual.Description == "Description of {{ test_win_rds_rap_name }}" + - edit_rap_actual.PortNumbers == "3389,3390,3391" + - edit_rap_actual.UserGroups | length == 2 + - edit_rap_actual.UserGroups[0] == "Users@BUILTIN" + - edit_rap_actual.UserGroups[1] == "Guests@BUILTIN" + - edit_rap_actual.ComputerGroupType == "1" + - edit_rap_actual.ComputerGroup == "Administrators@BUILTIN" + +- name: test edit a RAP (indempotent) + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + description: 'Description of {{ test_win_rds_rap_name }}' + user_groups: + - users@builtin + - guests@builtin + computer_group_type: ad_network_resource_group + computer_group: Administrators@BUILTIN + allowed_ports: + - 3389 + - 3390 + - 3391 + state: disabled + register: edit_rap_again + +- name: assert results of edit a RAP (indempotent) + assert: + that: + - edit_rap_again.changed == false + +- name: test allow all ports + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + allowed_ports: any + register: edit_rap_allow_all_ports + +- name: get result of allow all ports + win_shell: Import-Module RemoteDesktopServices; Write-Host (Get-Item "RDS:\GatewayServer\RAP\{{ test_win_rds_rap_name }}\PortNumbers").CurrentValue + register: edit_rap_allow_all_ports_actual + +- name: assert results of allow all ports + assert: + that: + - edit_rap_allow_all_ports.changed == true + - edit_rap_allow_all_ports_actual.stdout_lines[0] == "*" + +- name: test remove RAP (check mode) + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + state: absent + register: remove_rap_check + check_mode: yes + +- name: get result of remove RAP (check mode) + win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\RAP\{{ test_win_rds_rap_name }}") + register: remove_rap_actual_check + +- name: assert results of remove RAP (check mode) + assert: + that: + - remove_rap_check.changed == true + - remove_rap_actual_check.stdout_lines[0] == "True" + +- name: test remove RAP + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + state: absent + register: remove_rap + +- name: get result of remove RAP + win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\RAP\{{ test_win_rds_rap_name }}") + register: remove_rap_actual + +- name: assert results of remove RAP + assert: + that: + - remove_rap.changed == true + - remove_rap_actual.stdout_lines[0] == "False" + +- name: test remove RAP (idempotent) + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + state: absent + register: remove_rap_again + +- name: get result of remove RAP (idempotent) + win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\RAP\{{ test_win_rds_rap_name }}") + register: remove_rap_actual_again + +- name: assert results of remove RAP (idempotent) + assert: + that: + - remove_rap_again.changed == false + - remove_rap_actual_again.stdout_lines[0] == "False" + +- name: fail when create a new RAP without user group + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + state: present + register: new_rap_without_group + check_mode: yes + failed_when: "new_rap_without_group.msg != 'User groups must be defined to create a new RAP.'" + +- name: fail when create a new RAP with an empty user group list + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + user_groups: [] + state: present + register: new_rap_empty_group_list + check_mode: yes + failed_when: "new_rap_empty_group_list.msg is not search('cannot be an empty list')" + +- name: fail when create a new RAP with an invalid user group + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + user_groups: + - fake_group + state: present + register: new_rap_invalid_group + check_mode: yes + failed_when: new_rap_invalid_group.changed != false or new_rap_invalid_group.msg is not search('is not a valid account') + +- name: fail when create a new RAP with an invalid AD computer group + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + user_groups: + - administrators + computer_group_type: ad_network_resource_group + computer_group: fake_ad_group + state: present + register: new_rap_invalid_ad_computer_group + check_mode: yes + failed_when: new_rap_invalid_ad_computer_group.changed != false or new_rap_invalid_ad_computer_group.msg is not search('is not a valid account') + +- name: fail when create a new RAP with an invalid gateway managed computer group + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + user_groups: + - administrators + computer_group_type: rdg_group + computer_group: fake_rdg_group + state: present + register: new_rap_invalid_rdg_computer_group + check_mode: yes + failed_when: new_rap_invalid_rdg_computer_group.changed != false or new_rap_invalid_rdg_computer_group.msg is not search('is not a valid gateway managed computer group') + +- name: fail when create a new RAP with invalid port numbers + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + user_groups: + - administrators + allowed_ports: + - '{{ item }}' + state: present + loop: + - invalid_port_number + - 65536 + register: new_rap_invalid_port + check_mode: yes + failed_when: new_rap_invalid_port.changed != false or new_rap_invalid_port.msg is not search('is not a valid port number') diff --git a/test/integration/targets/win_rds_settings/aliases b/test/integration/targets/win_rds_settings/aliases new file mode 100644 index 00000000000..215e0b06920 --- /dev/null +++ b/test/integration/targets/win_rds_settings/aliases @@ -0,0 +1 @@ +shippable/windows/group4 diff --git a/test/integration/targets/win_rds_settings/defaults/main.yml b/test/integration/targets/win_rds_settings/defaults/main.yml new file mode 100644 index 00000000000..dfdc9647684 --- /dev/null +++ b/test/integration/targets/win_rds_settings/defaults/main.yml @@ -0,0 +1,2 @@ +test_win_rds_settings_path: '{{win_output_dir}}\win_rds_settings' +rds_cert_suject: rdg.test.com \ No newline at end of file diff --git a/test/integration/targets/win_rds_settings/tasks/main.yml b/test/integration/targets/win_rds_settings/tasks/main.yml new file mode 100644 index 00000000000..0d2ab7180b5 --- /dev/null +++ b/test/integration/targets/win_rds_settings/tasks/main.yml @@ -0,0 +1,88 @@ +--- +# Cannot use win_feature to install RDS on Server 2008. +# Run a brief check and skip hosts that don't support +# that operation +- name: check if win_feature will work on test host + win_command: powershell.exe "exit (-not (Get-Command -Name Add-WindowsFeature -ErrorAction SilentlyContinue))" + register: module_available + failed_when: False + +# Run actual tests +- block: + - name: gather facts + setup: + filter: ansible_hostname + + - name: ensure Remote Desktop Gateway services are installed + win_feature: + name: RDS-Gateway,RDS-Licensing,RDS-RD-Server + state: present + include_management_tools: True + register: rds_install + + - name: reboot server if needed + win_reboot: + post_reboot_delay: 10 + when: rds_install.reboot_required + + - name: ensure testing folders exists + win_file: + path: '{{test_win_rds_settings_path}}' + state: directory + + - name: deploy test artifacts + win_template: + src: '{{item}}.j2' + dest: '{{test_win_rds_settings_path}}\{{item | basename}}' + with_items: + - rds_base_cfg.xml + + - name: import RDS test configuration + win_shell: | + $ts = Get-WmiObject Win32_TSGatewayServer -namespace root\cimv2\TerminalServices + $import_xml = Get-Content {{test_win_rds_settings_path}}\rds_base_cfg.xml + $import_result = $ts.Import(45, $import_xml) + exit $import_result.ReturnValue + + - name: write certreq file + win_copy: + content: |- + [NewRequest] + Subject = "CN={{ rds_cert_suject }}" + KeyLength = 2048 + KeyAlgorithm = RSA + MachineKeySet = true + RequestType = Cert + KeyUsage = 0xA0 ; Digital Signature, Key Encipherment + [EnhancedKeyUsageExtension] + OID=1.3.6.1.5.5.7.3.1 ; Server Authentication + dest: '{{test_win_rds_settings_path}}\certreq.txt' + + - name: create self signed cert from certreq + win_command: certreq -new -machine {{test_win_rds_settings_path}}\certreq.txt {{test_win_rds_settings_path}}\certreqresp.txt + + - name: register certificate thumbprint + raw: '(gci Cert:\LocalMachine\my | ? {$_.subject -eq "CN={{ rds_cert_suject }}"})[0].Thumbprint' + register: rds_cert_thumbprint + + - include_tasks: tests.yml + + always: + - name: restore RDS base configuration + win_shell: | + $ts = Get-WmiObject Win32_TSGatewayServer -namespace root\cimv2\TerminalServices + $import_xml = Get-Content {{test_win_rds_settings_path}}\rds_base_cfg.xml + $import_result = $ts.Import(45, $import_xml) + exit $import_result.ReturnValue + + - name: remove certificate + raw: 'remove-item cert:\localmachine\my\{{ item }} -force -ea silentlycontinue' + with_items: + - "{{ rds_cert_thumbprint.stdout_lines[0] }}" + + - name: cleanup test artifacts + win_file: + path: '{{test_win_rds_settings_path}}' + state: absent + + when: module_available.rc == 0 diff --git a/test/integration/targets/win_rds_settings/tasks/tests.yml b/test/integration/targets/win_rds_settings/tasks/tests.yml new file mode 100644 index 00000000000..63fa815003f --- /dev/null +++ b/test/integration/targets/win_rds_settings/tasks/tests.yml @@ -0,0 +1,89 @@ +--- +- name: test change RDS settings (check mode) + win_rds_settings: + max_connections: 50 + certificate_hash: '{{rds_cert_thumbprint.stdout_lines[0]}}' + ssl_bridging: https_https + enable_only_messaging_capable_clients: yes + register: configure_rds_check + check_mode: yes + +- name: get result of change RDS settings (check mode) + win_shell: | + Import-Module RemoteDesktopServices + (Get-Item RDS:\GatewayServer\MaxConnections).CurrentValue + (Get-Item RDS:\GatewayServer\SSLCertificate\Thumbprint).CurrentValue + (Get-Item RDS:\GatewayServer\SSLBridging).CurrentValue + (Get-Item RDS:\GatewayServer\EnableOnlyMessagingCapableClients).CurrentValue + register: configure_rds_actual_check + +- name: assert results of change RDS settings (check mode) + assert: + that: + - configure_rds_check.changed == true + - configure_rds_actual_check.stdout_lines[0] != "50" + - configure_rds_actual_check.stdout_lines[1] != rds_cert_thumbprint.stdout_lines[0] + - configure_rds_actual_check.stdout_lines[2] == "0" + - configure_rds_actual_check.stdout_lines[3] == "0" + +- name: test change RDS settings + win_rds_settings: + max_connections: 50 + certificate_hash: '{{rds_cert_thumbprint.stdout_lines[0]}}' + ssl_bridging: https_https + enable_only_messaging_capable_clients: yes + register: configure_rds + +- name: get result of change RDS settings + win_shell: | + Import-Module RemoteDesktopServices + (Get-Item RDS:\GatewayServer\MaxConnections).CurrentValue + (Get-Item RDS:\GatewayServer\SSLCertificate\Thumbprint).CurrentValue + (Get-Item RDS:\GatewayServer\SSLBridging).CurrentValue + (Get-Item RDS:\GatewayServer\EnableOnlyMessagingCapableClients).CurrentValue + register: configure_rds_actual + +- name: assert results of change RDS settings + assert: + that: + - configure_rds.changed == true + - configure_rds_actual.stdout_lines[0] == "50" + - configure_rds_actual.stdout_lines[1] == rds_cert_thumbprint.stdout_lines[0] + - configure_rds_actual.stdout_lines[2] == "2" + - configure_rds_actual.stdout_lines[3] == "1" + +- name: test change RDS settings (idempotent) + win_rds_settings: + max_connections: 50 + certificate_hash: '{{rds_cert_thumbprint.stdout_lines[0]}}' + ssl_bridging: https_https + enable_only_messaging_capable_clients: yes + register: configure_rds_again + +- name: assert results of change RDS settings (idempotent) + assert: + that: + - configure_rds_again.changed == false + +- name: test disable connection limit + win_rds_settings: + max_connections: -1 + register: disable_limit + +- name: get result of disable connection limit + win_shell: | + Import-Module RemoteDesktopServices + (Get-Item RDS:\GatewayServer\MaxConnections).CurrentValue -eq (Get-Item RDS:\GatewayServer\MaxConnectionsAllowed).CurrentValue + register: disable_limit_actual + +- name: assert results of disable connection limit + assert: + that: + - disable_limit.changed == true + - disable_limit_actual.stdout_lines[0] == "True" + +- name: fail with invalid certificate thumbprint + win_rds_settings: + certificate_hash: 72E8BD0216FA14100192A3E8B7B150C65B4B0817 + register: fail_invalid_cert + failed_when: fail_invalid_cert.msg is not search('Unable to locate certificate') \ No newline at end of file diff --git a/test/integration/targets/win_rds_settings/templates/rds_base_cfg.xml.j2 b/test/integration/targets/win_rds_settings/templates/rds_base_cfg.xml.j2 new file mode 100644 index 00000000000..5aa48ed84ff --- /dev/null +++ b/test/integration/targets/win_rds_settings/templates/rds_base_cfg.xml.j2 @@ -0,0 +1,58 @@ + + + + {{ ansible_hostname }} + + 4294967295 + 1 + 0 + 0 + 0 + + + LogChannelDisconnect + 1 + + + LogFailureChannelConnect + 1 + + + LogFailureConnectionAuthorizationCheck + 1 + + + LogFailureResourceAuthorizationCheck + 1 + + + LogSuccessfulChannelConnect + 1 + + + LogSuccessfulConnectionAuthorizationCheck + 1 + + + LogSuccessfulResourceAuthorizationCheck + 1 + + + native + native + + + + + 0 + * + * + 443 + 3391 + 1 + 1 + + + + + \ No newline at end of file