diff --git a/registry/coder/modules/local-windows-rdp/README.md b/registry/coder/modules/local-windows-rdp/README.md new file mode 100644 index 0000000..c995bf7 --- /dev/null +++ b/registry/coder/modules/local-windows-rdp/README.md @@ -0,0 +1,75 @@ +--- +display_name: Windows RDP Desktop +description: Enable RDP on Windows and add a one-click Coder Desktop button for seamless access +icon: ../../../../.icons/desktop.svg +maintainer_github: coder +verified: true +tags: [rdp, windows, desktop, remote] +--- + +# Windows RDP Desktop + +This module enables Remote Desktop Protocol (RDP) on Windows workspaces and adds a one-click button to launch RDP sessions directly through Coder Desktop. It provides a complete, standalone solution for RDP access without requiring manual configuration or port forwarding. + +```tf +module "rdp_desktop" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/local-windows-rdp/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + agent_name = coder_agent.main.name +} +``` + +## Features + +- ✅ **Standalone Solution**: Automatically configures RDP on Windows workspaces +- ✅ **One-click Access**: Launch RDP sessions directly through Coder Desktop +- ✅ **No Port Forwarding**: Uses Coder Desktop URI handling +- ✅ **Auto-configuration**: Sets up Windows firewall, services, and authentication +- ✅ **Secure**: Configurable credentials with sensitive variable handling +- ✅ **Customizable**: Display name, credentials, and UI ordering options + +## What This Module Does + +1. **Enables RDP** on the Windows workspace +2. **Sets the administrator password** for RDP authentication +3. **Configures Windows Firewall** to allow RDP connections +4. **Starts RDP services** automatically +5. **Creates a Coder Desktop button** for one-click access + +## Requirements + +- **Coder Desktop**: Must be installed on the client machine ([Download here](https://coder.com/docs/user-guides/desktop)) + +## Examples + +### Basic Usage + +Uses default credentials (Username: `Administrator`, Password: `coderRDP!`): + +```tf +module "rdp_desktop" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/local-windows-rdp/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + agent_name = coder_agent.main.name +} +``` + +### Custom display name + +Specify a custom display name for the `coder_app` button: + +```tf +module "rdp_desktop" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/local-windows-rdp/coder" + version = "1.0.0" + agent_id = coder_agent.windows.id + agent_name = "windows" + display_name = "Windows Desktop" + order = 1 +} +``` diff --git a/registry/coder/modules/local-windows-rdp/configure-rdp.ps1 b/registry/coder/modules/local-windows-rdp/configure-rdp.ps1 new file mode 100644 index 0000000..34a3b3e --- /dev/null +++ b/registry/coder/modules/local-windows-rdp/configure-rdp.ps1 @@ -0,0 +1,120 @@ +# PowerShell script to configure RDP for Coder Desktop access +# This script enables RDP, sets the admin password, and configures necessary settings + +Write-Output "[Coder RDP Setup] Starting RDP configuration..." + +# Function to set the administrator password +function Set-AdminPassword { + param ( + [string]$adminUsername, + [string]$adminPassword + ) + + Write-Output "[Coder RDP Setup] Setting password for user: $adminUsername" + + try { + # Convert password to secure string + $securePassword = ConvertTo-SecureString -AsPlainText $adminPassword -Force + + # Set the password for the user + Get-LocalUser -Name $adminUsername | Set-LocalUser -Password $securePassword + + # Enable the user account (in case it's disabled) + Get-LocalUser -Name $adminUsername | Enable-LocalUser + + Write-Output "[Coder RDP Setup] Successfully set password for $adminUsername" + } catch { + Write-Error "[Coder RDP Setup] Failed to set password: $_" + exit 1 + } +} + +# Function to enable and configure RDP +function Enable-RDP { + Write-Output "[Coder RDP Setup] Enabling Remote Desktop..." + + try { + # Enable RDP + Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name "fDenyTSConnections" -Value 0 -Force + + # Disable Network Level Authentication (NLA) for easier access + Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "UserAuthentication" -Value 0 -Force + + # Set security layer to RDP Security Layer + Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "SecurityLayer" -Value 1 -Force + + Write-Output "[Coder RDP Setup] RDP enabled successfully" + } catch { + Write-Error "[Coder RDP Setup] Failed to enable RDP: $_" + exit 1 + } +} + +# Function to configure Windows Firewall for RDP +function Configure-Firewall { + Write-Output "[Coder RDP Setup] Configuring Windows Firewall for RDP..." + + try { + # Enable RDP firewall rules + Enable-NetFirewallRule -DisplayGroup "Remote Desktop" -ErrorAction SilentlyContinue + + # If the above fails, try alternative method + if ($LASTEXITCODE -ne 0) { + netsh advfirewall firewall set rule group="remote desktop" new enable=Yes + } + + Write-Output "[Coder RDP Setup] Firewall configured successfully" + } catch { + Write-Warning "[Coder RDP Setup] Failed to configure firewall rules: $_" + # Continue anyway as RDP might still work + } +} + +# Function to ensure RDP service is running +function Start-RDPService { + Write-Output "[Coder RDP Setup] Starting Remote Desktop Services..." + + try { + # Start the Terminal Services + Set-Service -Name "TermService" -StartupType Automatic -ErrorAction SilentlyContinue + Start-Service -Name "TermService" -ErrorAction SilentlyContinue + + # Start Remote Desktop Services UserMode Port Redirector + Set-Service -Name "UmRdpService" -StartupType Automatic -ErrorAction SilentlyContinue + Start-Service -Name "UmRdpService" -ErrorAction SilentlyContinue + + Write-Output "[Coder RDP Setup] RDP services started successfully" + } catch { + Write-Warning "[Coder RDP Setup] Some RDP services may not have started: $_" + # Continue anyway + } +} + +# Main execution +try { + # Template variables from Terraform + $username = "${username}" + $password = "${password}" + + # Validate inputs + if ([string]::IsNullOrWhiteSpace($username) -or [string]::IsNullOrWhiteSpace($password)) { + Write-Error "[Coder RDP Setup] Username or password is empty" + exit 1 + } + + # Execute configuration steps + Set-AdminPassword -adminUsername $username -adminPassword $password + Enable-RDP + Configure-Firewall + Start-RDPService + + Write-Output "[Coder RDP Setup] RDP configuration completed successfully!" + Write-Output "[Coder RDP Setup] You can now connect using:" + Write-Output " Username: $username" + Write-Output " Password: [hidden]" + Write-Output " Port: 3389 (default)" + +} catch { + Write-Error "[Coder RDP Setup] An unexpected error occurred: $_" + exit 1 +} \ No newline at end of file diff --git a/registry/coder/modules/local-windows-rdp/main.test.ts b/registry/coder/modules/local-windows-rdp/main.test.ts new file mode 100644 index 0000000..75b6dde --- /dev/null +++ b/registry/coder/modules/local-windows-rdp/main.test.ts @@ -0,0 +1,184 @@ +import { describe, expect, it } from "bun:test"; +import { + type TerraformState, + runTerraformApply, + runTerraformInit, + testRequiredVariables, +} from "~test"; + +type TestVariables = Readonly<{ + agent_id: string; + agent_name: string; + username?: string; + password?: string; + display_name?: string; + order?: number; +}>; + +function findRdpApp(state: TerraformState) { + for (const resource of state.resources) { + const isRdpAppResource = + resource.type === "coder_app" && resource.name === "rdp_desktop"; + + if (!isRdpAppResource) { + continue; + } + + for (const instance of resource.instances) { + if (instance.attributes.slug === "rdp-desktop") { + return instance.attributes; + } + } + } + + return null; +} + +function findRdpScript(state: TerraformState) { + for (const resource of state.resources) { + const isRdpScriptResource = + resource.type === "coder_script" && resource.name === "rdp_setup"; + + if (!isRdpScriptResource) { + continue; + } + + for (const instance of resource.instances) { + if (instance.attributes.display_name === "Configure RDP") { + return instance.attributes; + } + } + } + + return null; +} + +describe("local-windows-rdp", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "test-agent-id", + agent_name: "test-agent", + }); + + it("should create RDP app with default values", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "test-agent-id", + agent_name: "main", + }); + + const app = findRdpApp(state); + + // Verify the app was created + expect(app).not.toBeNull(); + expect(app?.slug).toBe("rdp-desktop"); + expect(app?.display_name).toBe("RDP Desktop"); + expect(app?.icon).toBe("/icon/desktop.svg"); + expect(app?.external).toBe(true); + + // Verify the URI format + expect(app?.url).toStartWith("coder://"); + expect(app?.url).toContain("/v0/open/ws/"); + expect(app?.url).toContain("/agent/main/rdp"); + expect(app?.url).toContain("username=Administrator"); + expect(app?.url).toContain("password=coderRDP!"); + }); + + it("should create RDP configuration script", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "test-agent-id", + agent_name: "main", + }); + + const script = findRdpScript(state); + + // Verify the script was created + expect(script).not.toBeNull(); + expect(script?.display_name).toBe("Configure RDP"); + expect(script?.icon).toBe("/icon/desktop.svg"); + expect(script?.run_on_start).toBe(true); + expect(script?.run_on_stop).toBe(false); + + // Verify the script contains PowerShell configuration + expect(script?.script).toContain("Set-AdminPassword"); + expect(script?.script).toContain("Enable-RDP"); + expect(script?.script).toContain("Configure-Firewall"); + expect(script?.script).toContain("Start-RDPService"); + }); + + it("should create RDP app with custom values", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "custom-agent-id", + agent_name: "windows-agent", + username: "CustomUser", + password: "CustomPass123!", + display_name: "Custom RDP", + order: 5, + }); + + const app = findRdpApp(state); + + // Verify custom values + expect(app?.display_name).toBe("Custom RDP"); + expect(app?.order).toBe(5); + + // Verify custom credentials in URI + expect(app?.url).toContain("/agent/windows-agent/rdp"); + expect(app?.url).toContain("username=CustomUser"); + expect(app?.url).toContain("password=CustomPass123!"); + }); + + it("should pass custom credentials to PowerShell script", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "test-agent-id", + agent_name: "main", + username: "TestAdmin", + password: "TestPassword123!", + }); + + const script = findRdpScript(state); + + // Verify custom credentials are in the script + expect(script?.script).toContain('$username = "TestAdmin"'); + expect(script?.script).toContain('$password = "TestPassword123!"'); + }); + + it("should handle sensitive password variable", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "test-agent-id", + agent_name: "main", + password: "SensitivePass123!", + }); + + const app = findRdpApp(state); + + // Verify password is included in URI even when sensitive + expect(app?.url).toContain("password=SensitivePass123!"); + }); + + it("should use correct default agent name", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "test-agent-id", + agent_name: "main", + }); + + const app = findRdpApp(state); + expect(app?.url).toContain("/agent/main/rdp"); + }); + + it("should construct proper Coder URI format", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "test-agent-id", + agent_name: "test-agent", + username: "TestUser", + password: "TestPass", + }); + + const app = findRdpApp(state); + + // Verify complete URI structure + expect(app?.url).toMatch( + /^coder:\/\/[^\/]+\/v0\/open\/ws\/[^\/]+\/agent\/test-agent\/rdp\?username=TestUser&password=TestPass$/, + ); + }); +}); diff --git a/registry/coder/modules/local-windows-rdp/main.tf b/registry/coder/modules/local-windows-rdp/main.tf new file mode 100644 index 0000000..ae0136d --- /dev/null +++ b/registry/coder/modules/local-windows-rdp/main.tf @@ -0,0 +1,74 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.17" + } + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "agent_name" { + type = string + description = "The name of the Coder agent." +} + +variable "username" { + type = string + description = "The username for RDP authentication." + default = "Administrator" +} + +variable "password" { + type = string + description = "The password for RDP authentication." + default = "coderRDP!" + sensitive = true +} + +variable "display_name" { + type = string + description = "The display name for the RDP app button." + default = "RDP Desktop" +} + +variable "order" { + type = number + description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." + default = null +} + +locals { + # Extract server name from workspace access URL + server_name = regex("https?:\\/\\/([^\\/]+)", data.coder_workspace.me.access_url)[0] +} + +data "coder_workspace" "me" {} + +resource "coder_script" "rdp_setup" { + agent_id = var.agent_id + display_name = "Configure RDP" + icon = "/icon/desktop.svg" + script = templatefile("${path.module}/configure-rdp.ps1", { + username = var.username + password = var.password + }) + run_on_start = true +} + +resource "coder_app" "rdp_desktop" { + agent_id = var.agent_id + slug = "rdp-desktop" + display_name = var.display_name + url = "coder://${local.server_name}/v0/open/ws/${data.coder_workspace.me.name}/agent/${var.agent_name}/rdp?username=${var.username}&password=${var.password}" + icon = "/icon/desktop.svg" + external = true + order = var.order +} +