diff --git a/registry/coder/modules/amazon-dcv-windows/README.md b/registry/coder/modules/amazon-dcv-windows/README.md index 63f6797f4..63b14d10e 100644 --- a/registry/coder/modules/amazon-dcv-windows/README.md +++ b/registry/coder/modules/amazon-dcv-windows/README.md @@ -18,7 +18,7 @@ Enable DCV Server and Web Client on Windows workspaces. module "dcv" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/amazon-dcv-windows/coder" - version = "1.1.1" + version = "1.1.2" agent_id = coder_agent.main.id } diff --git a/registry/coder/modules/amazon-dcv-windows/amazon-dcv-windows.tftest.hcl b/registry/coder/modules/amazon-dcv-windows/amazon-dcv-windows.tftest.hcl new file mode 100644 index 000000000..dd1b64651 --- /dev/null +++ b/registry/coder/modules/amazon-dcv-windows/amazon-dcv-windows.tftest.hcl @@ -0,0 +1,36 @@ +run "plan_with_defaults" { + command = plan + + variables { + agent_id = "test-agent-id" + } + + assert { + condition = resource.coder_app.web-dcv.url == "https://localhost:8443/?username=Administrator&password=coderDCV%21" + error_message = "The default DCV app URL should preserve its credentials." + } + + assert { + condition = strcontains(resource.coder_script.install-dcv.script, "$adminPassword = 'coderDCV!'") + error_message = "The default password should use a PowerShell single-quoted string." + } +} + +run "plan_with_special_characters" { + command = plan + + variables { + agent_id = "test-agent-id" + admin_password = "N;JVO*U\\mL^a*P\"'`$&<>|#%+" + } + + assert { + condition = endswith(resource.coder_app.web-dcv.url, "username=Administrator&password=${urlencode(var.admin_password)}") + error_message = "The DCV app URL should encode each credential parameter." + } + + assert { + condition = strcontains(resource.coder_script.install-dcv.script, format("$adminPassword = '%s'", replace(var.admin_password, "'", "''"))) + error_message = "The PowerShell script should preserve special characters." + } +} diff --git a/registry/coder/modules/amazon-dcv-windows/install-dcv.ps1 b/registry/coder/modules/amazon-dcv-windows/install-dcv.ps1 index 2b1c9f4b2..101a979bb 100644 --- a/registry/coder/modules/amazon-dcv-windows/install-dcv.ps1 +++ b/registry/coder/modules/amazon-dcv-windows/install-dcv.ps1 @@ -1,5 +1,5 @@ # Terraform variables -$adminPassword = "${admin_password}" +$adminPassword = '${admin_password}' $port = "${port}" $webURLPath = "${web_url_path}" diff --git a/registry/coder/modules/amazon-dcv-windows/main.test.ts b/registry/coder/modules/amazon-dcv-windows/main.test.ts new file mode 100644 index 000000000..b56077a1e --- /dev/null +++ b/registry/coder/modules/amazon-dcv-windows/main.test.ts @@ -0,0 +1,105 @@ +import { describe, it } from "bun:test"; +import { + findResourceInstance, + runTerraformApply, + runTerraformInit, + testRequiredVariables, +} from "~test"; + +type TestVariables = Readonly<{ + agent_id: string; + admin_password?: string; +}>; + +const assertCredentialRoundTrip = ( + actual: string | null, + expected: string, + context: string, +) => { + if (actual !== expected) { + throw new Error(`${context} did not preserve the credential`); + } +}; + +const extractPowerShellPassword = (script: string): string | null => { + const assignment = /^\$adminPassword = '((?:[^']|'')*)'[ \t]*\r?$/m.exec( + script, + ); + + return assignment?.[1]?.replaceAll("''", "'") ?? null; +}; + +const renderCredentials = async (adminPassword?: string) => { + const variables: TestVariables = { + agent_id: "test-agent-id", + ...(adminPassword === undefined ? {} : { admin_password: adminPassword }), + }; + const state = await runTerraformApply( + import.meta.dir, + variables, + ); + const script = findResourceInstance(state, "coder_script", "install-dcv"); + const app = findResourceInstance(state, "coder_app", "web-dcv"); + + return { + appUrl: new URL(app.url), + password: extractPowerShellPassword(script.script), + }; +}; + +describe("amazon-dcv-windows", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "test-agent-id", + }); + + it("preserves default credentials", async () => { + const rendered = await renderCredentials(); + + assertCredentialRoundTrip( + rendered.password, + "coderDCV!", + "Default PowerShell rendering", + ); + assertCredentialRoundTrip( + rendered.appUrl.searchParams.get("username"), + "Administrator", + "Default URL username decoding", + ); + assertCredentialRoundTrip( + rendered.appUrl.searchParams.get("password"), + "coderDCV!", + "Default URL password decoding", + ); + }); + + it("preserves special characters in the PowerShell script", async () => { + const specialPassword = + String.raw`N;JVO*U\mL^a*P"` + "'" + "`" + "$&<>|#%+"; + const rendered = await renderCredentials(specialPassword); + + assertCredentialRoundTrip( + rendered.password, + specialPassword, + "PowerShell rendering", + ); + }); + + it("preserves special characters in URL parameters", async () => { + const specialPassword = + String.raw`N;JVO*U\mL^a*P"` + "'" + "`" + "$&<>|#%+"; + const rendered = await renderCredentials(specialPassword); + + assertCredentialRoundTrip( + rendered.appUrl.searchParams.get("username"), + "Administrator", + "URL username decoding", + ); + assertCredentialRoundTrip( + rendered.appUrl.searchParams.get("password"), + specialPassword, + "URL password decoding", + ); + }); +}); diff --git a/registry/coder/modules/amazon-dcv-windows/main.tf b/registry/coder/modules/amazon-dcv-windows/main.tf index 223e3b785..3c3ae5ab8 100644 --- a/registry/coder/modules/amazon-dcv-windows/main.tf +++ b/registry/coder/modules/amazon-dcv-windows/main.tf @@ -54,7 +54,7 @@ resource "coder_app" "web-dcv" { agent_id = var.agent_id slug = var.slug display_name = "Web DCV" - url = "https://localhost:${var.port}${local.web_url_path}?username=${local.admin_username}&password=${var.admin_password}" + url = "https://localhost:${var.port}${local.web_url_path}?username=${urlencode(local.admin_username)}&password=${urlencode(var.admin_password)}" icon = "/icon/dcv.svg" subdomain = var.subdomain order = var.order @@ -67,7 +67,7 @@ resource "coder_script" "install-dcv" { icon = "/icon/dcv.svg" run_on_start = true script = templatefile("${path.module}/install-dcv.ps1", { - admin_password : var.admin_password, + admin_password : local.ps_admin_password, port : var.port, web_url_path : local.web_url_path }) @@ -77,8 +77,9 @@ data "coder_workspace" "me" {} data "coder_workspace_owner" "me" {} locals { - web_url_path = var.subdomain ? "/" : format("/@%s/%s/apps/%s", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, var.slug) - admin_username = "Administrator" + web_url_path = var.subdomain ? "/" : format("/@%s/%s/apps/%s", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, var.slug) + admin_username = "Administrator" + ps_admin_password = replace(var.admin_password, "'", "''") } output "web_url_path" {