Skip to content
Open
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
8 changes: 4 additions & 4 deletions registry/coder/modules/windows-rdp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Enable Remote Desktop + a web based client on Windows workspaces, powered by [de
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windows-rdp/coder"
version = "1.3.0"
version = "1.3.1"
agent_id = coder_agent.main.id
}
```
Expand All @@ -32,7 +32,7 @@ module "windows_rdp" {
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windows-rdp/coder"
version = "1.3.0"
version = "1.3.1"
agent_id = coder_agent.main.id
}
```
Expand All @@ -43,7 +43,7 @@ module "windows_rdp" {
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windows-rdp/coder"
version = "1.3.0"
version = "1.3.1"
agent_id = coder_agent.main.id
}
```
Expand All @@ -54,7 +54,7 @@ module "windows_rdp" {
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windows-rdp/coder"
version = "1.3.0"
version = "1.3.1"
agent_id = coder_agent.main.id
devolutions_gateway_version = "2025.2.2" # Specify a specific version
}
Expand Down
6 changes: 4 additions & 2 deletions registry/coder/modules/windows-rdp/devolutions-patch.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@
*
* All properties should be defined as placeholder templates in the form
* VALUE_NAME. The Coder module, when spun up, should then run some logic to
* replace the template slots with actual values. These values should never
* change from within JavaScript itself.
* replace the template slots with actual values. The module JSON-escapes each
* value before injecting it, so characters like backslashes and quotes stay
* intact inside these string literals. These values should never change from
* within JavaScript itself.
*
* @satisfies {FormFieldEntries}
*/
Expand Down
87 changes: 57 additions & 30 deletions registry/coder/modules/windows-rdp/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,31 @@ function findWindowsRdpScript(state: TerraformState): string | null {
return null;
}

/**
* Extracts the username and password the module injected into the JS patch
* file.
*
* The values are injected as JSON-escaped content inside double-quoted JS
* string literals, so the matched literals are parsed with JSON.parse to get
* the original values back. The regex stays verbose and pedantic on purpose:
* it validates the structure of the form entries object and, by only matching
* non-quote characters or escape pairs, it cannot overshoot into later content.
*/
function extractFormFieldValues(rdpScript: string): {
username?: string;
password?: string;
} {
const formEntryValuesRe =
/username:\s*\{[\s\S]*?value:\s*(?<username>"(?:[^"\\]|\\.)*")[\s\S]*?password:\s*\{[\s\S]*?value:\s*(?<password>"(?:[^"\\]|\\.)*")/;

const groups = formEntryValuesRe.exec(rdpScript)?.groups ?? {};

return {
username: groups.username && JSON.parse(groups.username),
password: groups.password && JSON.parse(groups.password),
};
}

/**
* @todo It would be nice if we had a way to verify that the Devolutions root
* HTML file is modified to include the import for the patched Coder script,
Expand Down Expand Up @@ -69,27 +94,6 @@ describe("Web RDP", async () => {
});

it("Injects Terraform's username and password into the JS patch file", async () => {
/**
* Using a regex as a quick-and-dirty way to get at the username and
* password values.
*
* Tried going through the trouble of extracting out the form entries
* variable from the main output, converting it from Prettier/JS-based JSON
* text to universal JSON text, and exposing it as a parsed JSON value. That
* got to be a bit too much, though.
*
* Regex is a little bit more verbose and pedantic than normal. Want to
* have some basic safety nets for validating the structure of the form
* entries variable after the JS file has had values injected. Even with all
* the wildcard classes set to lazy mode, we want to make sure that they
* don't overshoot and grab too much content.
*
* Written and tested via Regex101
* @see {@link https://regex101.com/r/UMgQpv/2}
*/
const formEntryValuesRe =
/username:\s*\{[\s\S]*?value:\s*"(?<username>[^"]+)"[\s\S]*?password:\s*\{[\s\S]*?value:\s*"(?<password>[^"]+)"/;

// Test that things work with the default username/password
const defaultState = await runTerraformApply<TestVariables>(
import.meta.dir,
Expand All @@ -101,11 +105,10 @@ describe("Web RDP", async () => {
const defaultRdpScript = findWindowsRdpScript(defaultState);
expect(defaultRdpScript).toBeString();

const defaultResultsGroup =
formEntryValuesRe.exec(defaultRdpScript ?? "")?.groups ?? {};

expect(defaultResultsGroup.username).toBe("Administrator");
expect(defaultResultsGroup.password).toBe("coderRDP!");
expect(extractFormFieldValues(defaultRdpScript ?? "")).toEqual({
username: "Administrator",
password: "coderRDP!",
});

// Test that custom usernames/passwords are also forwarded correctly
const customAdminUsername = "crouton";
Expand All @@ -122,10 +125,34 @@ describe("Web RDP", async () => {
const customRdpScript = findWindowsRdpScript(customizedState);
expect(customRdpScript).toBeString();

const customResultsGroup =
formEntryValuesRe.exec(customRdpScript ?? "")?.groups ?? {};
expect(extractFormFieldValues(customRdpScript ?? "")).toEqual({
username: customAdminUsername,
password: customAdminPassword,
});
});

it("Preserves special characters in the password", async () => {
// Covers the characters that break naive string interpolation in either the
// JS patch file or the PowerShell installation script.
const specialPassword = "N;JVO*U\\mL^a*P\"'`$&<>|{}[]%@:~";

const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "foo",
admin_password: specialPassword,
});

expect(customResultsGroup.username).toBe(customAdminUsername);
expect(customResultsGroup.password).toBe(customAdminPassword);
const rdpScript = findWindowsRdpScript(state);
expect(rdpScript).toBeString();

// The JS patch file must receive the password verbatim once parsed.
expect(extractFormFieldValues(rdpScript ?? "").password).toBe(
specialPassword,
);

// PowerShell single-quoted strings are literal, and a literal single quote
// is escaped by doubling it.
expect(rdpScript).toContain(
`Set-AdminPassword -adminPassword '${specialPassword.replaceAll("'", "''")}'`,
);
});
});
23 changes: 19 additions & 4 deletions registry/coder/modules/windows-rdp/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -70,22 +70,37 @@ variable "devolutions_gateway_version" {
description = "Version of Devolutions Gateway to install. Use 'latest' for the most recent version, or specify a version like '2025.3.2'."
}

locals {
# The Devolutions patch script embeds these values inside double-quoted JS
# string literals. jsonencode escapes backslashes, quotes, control characters,
# and HTML-significant characters; the outer quotes are trimmed because the JS
# file supplies its own.
js_admin_username = trimsuffix(trimprefix(jsonencode(var.admin_username), "\""), "\"")
js_admin_password = trimsuffix(trimprefix(jsonencode(var.admin_password), "\""), "\"")

# The installation 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_admin_username = replace(var.admin_username, "'", "''")
ps_admin_password = replace(var.admin_password, "'", "''")
}

resource "coder_script" "windows-rdp" {
agent_id = var.agent_id
display_name = "windows-rdp"
icon = "/icon/rdp.svg"

script = templatefile("${path.module}/powershell-installation-script.tftpl", {
admin_username = var.admin_username
admin_password = var.admin_password
admin_username = local.ps_admin_username
admin_password = local.ps_admin_password
devolutions_gateway_version = var.devolutions_gateway_version

# Wanted to have this be in the powershell template file, but Terraform
# doesn't allow recursive calls to the templatefile function. Have to feed
# results of the JS template replace into the powershell template
patch_file_contents = templatefile("${path.module}/devolutions-patch.js", {
CODER_USERNAME = var.admin_username
CODER_PASSWORD = var.admin_password
CODER_USERNAME = local.js_admin_username
CODER_PASSWORD = local.js_admin_password
})
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ function Set-AdminPassword {
Import-Module Microsoft.PowerShell.LocalAccounts -ErrorAction SilentlyContinue

# Set admin password
Get-LocalUser -Name "${admin_username}" | Set-LocalUser -Password (ConvertTo-SecureString -AsPlainText $adminPassword -Force)
Get-LocalUser -Name '${admin_username}' | Set-LocalUser -Password (ConvertTo-SecureString -AsPlainText $adminPassword -Force)
# Enable admin user
Get-LocalUser -Name "${admin_username}" | Enable-LocalUser
Get-LocalUser -Name '${admin_username}' | Enable-LocalUser
}

function Configure-RDP {
Expand Down Expand Up @@ -125,7 +125,7 @@ if ($isPatched -eq $null) {
}
}

Set-AdminPassword -adminPassword "${admin_password}"
Set-AdminPassword -adminPassword '${admin_password}'
Configure-RDP
Install-DevolutionsGateway
Patch-Devolutions-HTML