Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions registry/coder/modules/local-windows-rdp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ This module enables Remote Desktop Protocol (RDP) on Windows workspaces and adds
module "rdp_desktop" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/local-windows-rdp/coder"
version = "1.0.4"
version = "1.0.5"
agent_id = coder_agent.main.id
agent_name = "main"
}
Expand Down Expand Up @@ -57,7 +57,7 @@ Uses default credentials (Username: `Administrator`, Password: `coderRDP!`):
module "rdp_desktop" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/local-windows-rdp/coder"
version = "1.0.4"
version = "1.0.5"
agent_id = coder_agent.main.id
agent_name = "main"
}
Expand All @@ -71,10 +71,34 @@ Specify a custom display name for the `coder_app` button:
module "rdp_desktop" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/local-windows-rdp/coder"
version = "1.0.4"
version = "1.0.5"
agent_id = coder_agent.main.id
agent_name = "windows"
display_name = "Windows Desktop"
order = 1
}
```

### Generated credentials

Rather than relying on the shared default password, generate one per workspace
so the credential never lives in the template:

```tf
resource "random_password" "rdp" {
length = 24
special = true
}

module "rdp_desktop" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/local-windows-rdp/coder"
version = "1.0.5"
agent_id = coder_agent.main.id
agent_name = "main"
password = random_password.rdp.result
}
```

Generated passwords are passed through verbatim, including characters such as
`\`, `"`, `'`, `` ` ``, `$`, `&`, and `#`.
4 changes: 2 additions & 2 deletions registry/coder/modules/local-windows-rdp/configure-rdp.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ function Start-RDPService {
# Main execution
try {
# Template variables from Terraform
$username = "${username}"
$password = "${password}"
$username = '${username}'
$password = '${password}'

# Validate inputs
if ([string]::IsNullOrWhiteSpace($username) -or [string]::IsNullOrWhiteSpace($password)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
run "plan_with_defaults" {
command = plan

variables {
agent_id = "test-agent-id"
agent_name = "main"
}

assert {
condition = strcontains(resource.coder_script.rdp_setup.script, "$password = 'coderRDP!'")
error_message = "The default password should use a PowerShell single-quoted string."
}

assert {
condition = endswith(resource.coder_app.rdp_desktop.url, "username=Administrator&password=coderRDP%21")
error_message = "The default app URL should encode its credentials."
}
}

run "plan_with_special_characters" {
command = plan

variables {
agent_id = "test-agent-id"
agent_name = "main"
password = "N;JVO*U\\mL^a*P\"'`$&<>|#%+"
}

# A PowerShell single-quoted string is literal except for the single quote,
# which is escaped by doubling it.
assert {
condition = strcontains(resource.coder_script.rdp_setup.script, format("$password = '%s'", replace(var.password, "'", "''")))
error_message = "The setup script should preserve special characters in the password."
}

# Without encoding, the password truncates at the first & and drops
# everything after a #.
assert {
condition = endswith(resource.coder_app.rdp_desktop.url, "username=${urlencode(var.username)}&password=${urlencode(var.password)}")
error_message = "The app URL should encode each credential parameter."
}
}
12 changes: 7 additions & 5 deletions registry/coder/modules/local-windows-rdp/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ describe("local-windows-rdp", async () => {
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!");
// Credentials are URL-encoded so characters like & and # survive.
expect(app?.url).toContain("password=coderRDP%21");
});

it("should create RDP configuration script", async () => {
Expand Down Expand Up @@ -125,7 +126,7 @@ describe("local-windows-rdp", async () => {
// 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!");
expect(app?.url).toContain("password=CustomPass123%21");
});

it("should pass custom credentials to PowerShell script", async () => {
Expand All @@ -139,8 +140,9 @@ describe("local-windows-rdp", async () => {
const script = findRdpScript(state);

// Verify custom credentials are in the script
expect(script?.script).toContain('$username = "TestAdmin"');
expect(script?.script).toContain('$password = "TestPassword123!"');
// PowerShell single-quoted strings keep every character literal.
expect(script?.script).toContain("$username = 'TestAdmin'");
expect(script?.script).toContain("$password = 'TestPassword123!'");
});

it("should handle sensitive password variable", async () => {
Expand All @@ -153,7 +155,7 @@ describe("local-windows-rdp", async () => {
const app = findRdpApp(state);

// Verify password is included in URI even when sensitive
expect(app?.url).toContain("password=SensitivePass123!");
expect(app?.url).toContain("password=SensitivePass123%21");
});

it("should use correct default agent name", async () => {
Expand Down
12 changes: 9 additions & 3 deletions registry/coder/modules/local-windows-rdp/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ variable "group" {
locals {
# Extract server name from workspace access URL
server_name = regex("https?:\\/\\/([^\\/]+)", data.coder_workspace.me.access_url)[0]

# The setup script passes these values as PowerShell single-quoted strings,
# which are literal apart from the single quote itself. Doubling the single
# quotes keeps values containing $, backticks, or double quotes intact.
ps_username = replace(var.username, "'", "''")
ps_password = replace(var.password, "'", "''")
}

data "coder_workspace" "me" {}
Expand All @@ -62,8 +68,8 @@ resource "coder_script" "rdp_setup" {
display_name = "Configure RDP"
icon = "/icon/rdp.svg"
script = templatefile("${path.module}/configure-rdp.ps1", {
username = var.username
password = var.password
username = local.ps_username
password = local.ps_password
})
run_on_start = true
}
Expand All @@ -72,7 +78,7 @@ 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}"
url = "coder://${local.server_name}/v0/open/ws/${data.coder_workspace.me.name}/agent/${var.agent_name}/rdp?username=${urlencode(var.username)}&password=${urlencode(var.password)}"
icon = "/icon/rdp.svg"
external = true
order = var.order
Expand Down