feat: allow bypassing current CORS magic based on template config (#18706)

Solves https://github.com/coder/coder/issues/15096

This is a slight rework/refactor of the earlier PRs from @dannykopping
and @Emyrk:
- https://github.com/coder/coder/pull/15669
- https://github.com/coder/coder/pull/15684
- https://github.com/coder/coder/pull/17596

Rather than having a per-app CORS behaviour setting and additionally a
template level setting for ports, this PR adds a single template level
CORS behaviour setting that is then used by all apps/ports for
workspaces created from that template.

The main changes are in `proxy.go` and `request.go` to:
a) get the CORS behaviour setting from the template
b) have `HandleSubdomain` bypass the CORS middleware handler if the
selected behaviour is `passthru`
c) in `proxyWorkspaceApp`, do not modify the response if the selected
behaviour is `passthru`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added support for configuring CORS behavior ("simple" or "passthru")
at the template level for all shared ports.
* Introduced a new "CORS Behavior" setting in the template creation and
settings forms.
* API endpoints and responses now include the optional `cors_behavior`
property for templates.
* Workspace apps and proxy now honor the specified CORS behavior,
enabling conditional CORS middleware application.
* Enhanced workspace app tests with comprehensive scenarios covering
CORS behaviors and authentication states.

* **Bug Fixes**
  * None.

* **Documentation**
* Updated API and admin documentation to describe the new
`cors_behavior` property and its usage.
* Added examples and schema references for CORS behavior in relevant API
docs.

* **Tests**
* Extended automated tests to cover different CORS behavior scenarios
for templates and workspace apps.

* **Chores**
* Updated audit logging to track changes to the `cors_behavior` field on
templates.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Callum Styan <callumstyan@gmail.com>
This commit is contained in:
Callum Styan
2025-07-30 13:42:39 -07:00
committed by GitHub
parent 96e32d60a2
commit ffbfaf2a6f
36 changed files with 1149 additions and 108 deletions
@@ -30,6 +30,7 @@ test("update template schedule settings without override other settings", async
disable_everyone_group_access: false,
require_active_version: true,
max_port_share_level: null,
cors_behavior: null,
allow_user_cancel_workspace_jobs: null,
});
+8
View File
@@ -307,6 +307,11 @@ export const BypassRatelimitHeader = "X-Coder-Bypass-Ratelimit";
// From codersdk/client.go
export const CLITelemetryHeader = "Coder-CLI-Telemetry";
// From codersdk/cors_behavior.go
export type CORSBehavior = "passthru" | "simple";
export const CORSBehaviors: CORSBehavior[] = ["passthru", "simple"];
// From codersdk/workspacebuilds.go
export interface CancelWorkspaceBuildParams {
readonly expect_status?: CancelWorkspaceBuildStatus;
@@ -492,6 +497,7 @@ export interface CreateTemplateRequest {
readonly require_active_version: boolean;
readonly max_port_share_level: WorkspaceAgentPortShareLevel | null;
readonly template_use_classic_parameter_flow?: boolean;
readonly cors_behavior: CORSBehavior | null;
}
// From codersdk/templateversions.go
@@ -2816,6 +2822,7 @@ export interface Template {
readonly time_til_dormant_autodelete_ms: number;
readonly require_active_version: boolean;
readonly max_port_share_level: WorkspaceAgentPortShareLevel;
readonly cors_behavior: CORSBehavior;
readonly use_classic_parameter_flow: boolean;
}
@@ -3188,6 +3195,7 @@ export interface UpdateTemplateMeta {
readonly deprecation_message?: string;
readonly disable_everyone_group_access: boolean;
readonly max_port_share_level?: WorkspaceAgentPortShareLevel;
readonly cors_behavior?: CORSBehavior;
readonly use_classic_parameter_flow?: boolean;
}
@@ -18,6 +18,7 @@ export const newTemplate = (
const safeTemplateData = {
name: formData.name,
max_port_share_level: null,
cors_behavior: null,
display_name: formData.display_name,
description: formData.description,
icon: formData.icon,
@@ -4,6 +4,7 @@ import FormHelperText from "@mui/material/FormHelperText";
import MenuItem from "@mui/material/MenuItem";
import TextField from "@mui/material/TextField";
import {
CORSBehaviors,
type Template,
type UpdateTemplateMeta,
WorkspaceAppSharingLevels,
@@ -52,6 +53,7 @@ export const validationSchema = Yup.object({
use_classic_parameter_flow: Yup.boolean(),
deprecation_message: Yup.string(),
max_port_sharing_level: Yup.string().oneOf(WorkspaceAppSharingLevels),
cors_behavior: Yup.string().oneOf(Object.values(CORSBehaviors)),
});
export interface TemplateSettingsForm {
@@ -93,6 +95,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
disable_everyone_group_access: false,
max_port_share_level: template.max_port_share_level,
use_classic_parameter_flow: template.use_classic_parameter_flow,
cors_behavior: template.cors_behavior,
},
validationSchema,
onSubmit,
@@ -338,6 +341,28 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
</FormFields>
</FormSection>
<FormSection
title="CORS Behavior"
description="Control how Cross-Origin Resource Sharing (CORS) requests are handled for all shared ports."
>
<FormFields>
<TextField
{...getFieldHelpers("cors_behavior", {
helperText:
"Use Passthru to bypass Coder's built-in CORS protection.",
})}
disabled={isSubmitting}
fullWidth
select
value={form.values.cors_behavior}
label="CORS Behavior"
>
<MenuItem value="simple">Simple (recommended)</MenuItem>
<MenuItem value="passthru">Passthru</MenuItem>
</TextField>
</FormFields>
</FormSection>
<FormFooter>
<Button onClick={onCancel} variant="outline">
Cancel
@@ -55,6 +55,7 @@ const validFormValues: FormValues = {
disable_everyone_group_access: false,
max_port_share_level: "owner",
use_classic_parameter_flow: true,
cors_behavior: "simple",
};
const renderTemplateSettingsPage = async () => {
+1
View File
@@ -827,6 +827,7 @@ export const MockTemplate: TypesGen.Template = {
deprecation_message: "",
max_port_share_level: "public",
use_classic_parameter_flow: false,
cors_behavior: "simple",
};
const MockTemplateVersionFiles: TemplateVersionFiles = {