diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index f814b25d99337..51d0dc61bf2cf 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -9327,6 +9327,17 @@ const docTemplate = `{ "AgentSubsystemExectrace" ] }, + "codersdk.AppCORSBehavior": { + "type": "string", + "enum": [ + "simple", + "passthru" + ], + "x-enum-varnames": [ + "AppCORSBehaviorSimple", + "AppCORSBehaviorPassthru" + ] + }, "codersdk.AppHostResponse": { "type": "object", "properties": { @@ -9933,6 +9944,14 @@ const docTemplate = `{ } ] }, + "cors_behavior": { + "description": "CORSBehavior allows optionally specifying the CORS behavior for all shared ports.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.AppCORSBehavior" + } + ] + }, "default_ttl_ms": { "description": "DefaultTTLMillis allows optionally specifying the default TTL\nfor all workspaces created from this template.", "type": "integer" @@ -13144,6 +13163,9 @@ const docTemplate = `{ "build_time_stats": { "$ref": "#/definitions/codersdk.TemplateBuildTimeStats" }, + "cors_behavior": { + "$ref": "#/definitions/codersdk.AppCORSBehavior" + }, "created_at": { "type": "string", "format": "date-time" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 4f439e472fa7b..7d5516225ca4a 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -8263,6 +8263,11 @@ "AgentSubsystemExectrace" ] }, + "codersdk.AppCORSBehavior": { + "type": "string", + "enum": ["simple", "passthru"], + "x-enum-varnames": ["AppCORSBehaviorSimple", "AppCORSBehaviorPassthru"] + }, "codersdk.AppHostResponse": { "type": "object", "properties": { @@ -8836,6 +8841,14 @@ } ] }, + "cors_behavior": { + "description": "CORSBehavior allows optionally specifying the CORS behavior for all shared ports.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.AppCORSBehavior" + } + ] + }, "default_ttl_ms": { "description": "DefaultTTLMillis allows optionally specifying the default TTL\nfor all workspaces created from this template.", "type": "integer" @@ -11909,6 +11922,9 @@ "build_time_stats": { "$ref": "#/definitions/codersdk.TemplateBuildTimeStats" }, + "cors_behavior": { + "$ref": "#/definitions/codersdk.AppCORSBehavior" + }, "created_at": { "type": "string", "format": "date-time" diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 7be45e76c2b79..248f7831b5525 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -7680,6 +7680,7 @@ func (q *FakeQuerier) InsertTemplate(_ context.Context, arg database.InsertTempl AllowUserAutostart: true, AllowUserAutostop: true, MaxPortSharingLevel: arg.MaxPortSharingLevel, + CORSBehavior: arg.CORSBehavior, } q.templates = append(q.templates, template) return nil @@ -9213,6 +9214,7 @@ func (q *FakeQuerier) UpdateTemplateMetaByID(_ context.Context, arg database.Upd tpl.GroupACL = arg.GroupACL tpl.AllowUserCancelWorkspaceJobs = arg.AllowUserCancelWorkspaceJobs tpl.MaxPortSharingLevel = arg.MaxPortSharingLevel + tpl.CORSBehavior = arg.CORSBehavior q.templates[idx] = tpl return nil } diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 0d0a613d1f187..031a1620db26b 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1293,7 +1293,8 @@ CREATE TABLE templates ( require_active_version boolean DEFAULT false NOT NULL, deprecated text DEFAULT ''::text NOT NULL, activity_bump bigint DEFAULT '3600000000000'::bigint NOT NULL, - max_port_sharing_level app_sharing_level DEFAULT 'owner'::app_sharing_level NOT NULL + max_port_sharing_level app_sharing_level DEFAULT 'owner'::app_sharing_level NOT NULL, + cors_behavior app_cors_behavior DEFAULT 'simple'::app_cors_behavior NOT NULL ); COMMENT ON COLUMN templates.default_ttl IS 'The default duration for autostop for workspaces created from this template.'; @@ -1343,6 +1344,7 @@ CREATE VIEW template_with_names AS templates.deprecated, templates.activity_bump, templates.max_port_sharing_level, + templates.cors_behavior, COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url, COALESCE(visible_users.username, ''::text) AS created_by_username, COALESCE(organizations.name, ''::text) AS organization_name, diff --git a/coderd/database/migrations/000279_template_level_cors.down.sql b/coderd/database/migrations/000279_template_level_cors.down.sql new file mode 100644 index 0000000000000..33784e1a87311 --- /dev/null +++ b/coderd/database/migrations/000279_template_level_cors.down.sql @@ -0,0 +1,42 @@ +DROP VIEW IF EXISTS template_with_names; +CREATE VIEW template_with_names AS + SELECT templates.id, + templates.created_at, + templates.updated_at, + templates.organization_id, + templates.deleted, + templates.name, + templates.provisioner, + templates.active_version_id, + templates.description, + templates.default_ttl, + templates.created_by, + templates.icon, + templates.user_acl, + templates.group_acl, + templates.display_name, + templates.allow_user_cancel_workspace_jobs, + templates.allow_user_autostart, + templates.allow_user_autostop, + templates.failure_ttl, + templates.time_til_dormant, + templates.time_til_dormant_autodelete, + templates.autostop_requirement_days_of_week, + templates.autostop_requirement_weeks, + templates.autostart_block_days_of_week, + templates.require_active_version, + templates.deprecated, + templates.activity_bump, + templates.max_port_sharing_level, + COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url, + COALESCE(visible_users.username, ''::text) AS created_by_username, + COALESCE(organizations.name, ''::text) AS organization_name, + COALESCE(organizations.display_name, ''::text) AS organization_display_name, + COALESCE(organizations.icon, ''::text) AS organization_icon + FROM ((templates + LEFT JOIN visible_users ON ((templates.created_by = visible_users.id))) + LEFT JOIN organizations ON ((templates.organization_id = organizations.id))); + +COMMENT ON VIEW template_with_names IS 'Joins in the display name information such as username, avatar, and organization name.'; + +ALTER TABLE templates DROP COLUMN cors_behavior; diff --git a/coderd/database/migrations/000279_template_level_cors.up.sql b/coderd/database/migrations/000279_template_level_cors.up.sql new file mode 100644 index 0000000000000..3d3526a5490d9 --- /dev/null +++ b/coderd/database/migrations/000279_template_level_cors.up.sql @@ -0,0 +1,45 @@ +ALTER TABLE templates +ADD COLUMN cors_behavior app_cors_behavior NOT NULL DEFAULT 'simple'::app_cors_behavior; + +-- Update the template_with_users view by recreating it. +DROP VIEW IF EXISTS template_with_names; +CREATE VIEW template_with_names AS + SELECT templates.id, + templates.created_at, + templates.updated_at, + templates.organization_id, + templates.deleted, + templates.name, + templates.provisioner, + templates.active_version_id, + templates.description, + templates.default_ttl, + templates.created_by, + templates.icon, + templates.user_acl, + templates.group_acl, + templates.display_name, + templates.allow_user_cancel_workspace_jobs, + templates.allow_user_autostart, + templates.allow_user_autostop, + templates.failure_ttl, + templates.time_til_dormant, + templates.time_til_dormant_autodelete, + templates.autostop_requirement_days_of_week, + templates.autostop_requirement_weeks, + templates.autostart_block_days_of_week, + templates.require_active_version, + templates.deprecated, + templates.activity_bump, + templates.max_port_sharing_level, + templates.cors_behavior, -- <--- adding this column + COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url, + COALESCE(visible_users.username, ''::text) AS created_by_username, + COALESCE(organizations.name, ''::text) AS organization_name, + COALESCE(organizations.display_name, ''::text) AS organization_display_name, + COALESCE(organizations.icon, ''::text) AS organization_icon + FROM ((templates + LEFT JOIN visible_users ON ((templates.created_by = visible_users.id))) + LEFT JOIN organizations ON ((templates.organization_id = organizations.id))); + +COMMENT ON VIEW template_with_names IS 'Joins in the display name information such as username, avatar, and organization name.'; diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index ff77012755fa2..3c06d977fe323 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -117,6 +117,7 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate &i.Deprecated, &i.ActivityBump, &i.MaxPortSharingLevel, + &i.CORSBehavior, &i.CreatedByAvatarURL, &i.CreatedByUsername, &i.OrganizationName, diff --git a/coderd/database/models.go b/coderd/database/models.go index 821116d0da6cc..1d0124be15f30 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -2742,6 +2742,7 @@ type Template struct { Deprecated string `db:"deprecated" json:"deprecated"` ActivityBump int64 `db:"activity_bump" json:"activity_bump"` MaxPortSharingLevel AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"` + CORSBehavior AppCORSBehavior `db:"cors_behavior" json:"cors_behavior"` CreatedByAvatarURL string `db:"created_by_avatar_url" json:"created_by_avatar_url"` CreatedByUsername string `db:"created_by_username" json:"created_by_username"` OrganizationName string `db:"organization_name" json:"organization_name"` @@ -2787,6 +2788,7 @@ type TemplateTable struct { Deprecated string `db:"deprecated" json:"deprecated"` ActivityBump int64 `db:"activity_bump" json:"activity_bump"` MaxPortSharingLevel AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"` + CORSBehavior AppCORSBehavior `db:"cors_behavior" json:"cors_behavior"` } // Records aggregated usage statistics for templates/users. All usage is rounded up to the nearest minute. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 7fee1e0d2ebd2..9c59d5ff21e17 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -8296,7 +8296,7 @@ func (q *sqlQuerier) GetTemplateAverageBuildTime(ctx context.Context, arg GetTem const getTemplateByID = `-- name: GetTemplateByID :one SELECT - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, created_by_avatar_url, created_by_username, organization_name, organization_display_name, organization_icon + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, cors_behavior, created_by_avatar_url, created_by_username, organization_name, organization_display_name, organization_icon FROM template_with_names WHERE @@ -8337,6 +8337,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat &i.Deprecated, &i.ActivityBump, &i.MaxPortSharingLevel, + &i.CORSBehavior, &i.CreatedByAvatarURL, &i.CreatedByUsername, &i.OrganizationName, @@ -8348,7 +8349,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one SELECT - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, created_by_avatar_url, created_by_username, organization_name, organization_display_name, organization_icon + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, cors_behavior, created_by_avatar_url, created_by_username, organization_name, organization_display_name, organization_icon FROM template_with_names AS templates WHERE @@ -8397,6 +8398,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G &i.Deprecated, &i.ActivityBump, &i.MaxPortSharingLevel, + &i.CORSBehavior, &i.CreatedByAvatarURL, &i.CreatedByUsername, &i.OrganizationName, @@ -8407,7 +8409,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G } const getTemplates = `-- name: GetTemplates :many -SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, created_by_avatar_url, created_by_username, organization_name, organization_display_name, organization_icon FROM template_with_names AS templates +SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, cors_behavior, created_by_avatar_url, created_by_username, organization_name, organization_display_name, organization_icon FROM template_with_names AS templates ORDER BY (name, id) ASC ` @@ -8449,6 +8451,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) { &i.Deprecated, &i.ActivityBump, &i.MaxPortSharingLevel, + &i.CORSBehavior, &i.CreatedByAvatarURL, &i.CreatedByUsername, &i.OrganizationName, @@ -8470,7 +8473,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) { const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many SELECT - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, created_by_avatar_url, created_by_username, organization_name, organization_display_name, organization_icon + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, cors_behavior, created_by_avatar_url, created_by_username, organization_name, organization_display_name, organization_icon FROM template_with_names AS templates WHERE @@ -8570,6 +8573,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate &i.Deprecated, &i.ActivityBump, &i.MaxPortSharingLevel, + &i.CORSBehavior, &i.CreatedByAvatarURL, &i.CreatedByUsername, &i.OrganizationName, @@ -8606,10 +8610,11 @@ INSERT INTO group_acl, display_name, allow_user_cancel_workspace_jobs, - max_port_sharing_level + max_port_sharing_level, + cors_behavior ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) ` type InsertTemplateParams struct { @@ -8628,6 +8633,7 @@ type InsertTemplateParams struct { DisplayName string `db:"display_name" json:"display_name"` AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"` MaxPortSharingLevel AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"` + CORSBehavior AppCORSBehavior `db:"cors_behavior" json:"cors_behavior"` } func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParams) error { @@ -8647,6 +8653,7 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam arg.DisplayName, arg.AllowUserCancelWorkspaceJobs, arg.MaxPortSharingLevel, + arg.CORSBehavior, ) return err } @@ -8746,7 +8753,8 @@ SET display_name = $6, allow_user_cancel_workspace_jobs = $7, group_acl = $8, - max_port_sharing_level = $9 + max_port_sharing_level = $9, + cors_behavior = $10 WHERE id = $1 ` @@ -8761,6 +8769,7 @@ type UpdateTemplateMetaByIDParams struct { AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"` GroupACL TemplateACL `db:"group_acl" json:"group_acl"` MaxPortSharingLevel AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"` + CORSBehavior AppCORSBehavior `db:"cors_behavior" json:"cors_behavior"` } func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error { @@ -8774,6 +8783,7 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl arg.AllowUserCancelWorkspaceJobs, arg.GroupACL, arg.MaxPortSharingLevel, + arg.CORSBehavior, ) return err } @@ -15142,7 +15152,7 @@ LEFT JOIN LATERAL ( ) latest_build ON TRUE LEFT JOIN LATERAL ( SELECT - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, cors_behavior FROM templates WHERE diff --git a/coderd/database/queries/templates.sql b/coderd/database/queries/templates.sql index 84df9633a1a53..c976c1854411c 100644 --- a/coderd/database/queries/templates.sql +++ b/coderd/database/queries/templates.sql @@ -90,10 +90,11 @@ INSERT INTO group_acl, display_name, allow_user_cancel_workspace_jobs, - max_port_sharing_level + max_port_sharing_level, + cors_behavior ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15); + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16); -- name: UpdateTemplateActiveVersionByID :exec UPDATE @@ -124,7 +125,8 @@ SET display_name = $6, allow_user_cancel_workspace_jobs = $7, group_acl = $8, - max_port_sharing_level = $9 + max_port_sharing_level = $9, + cors_behavior = $10 WHERE id = $1 ; diff --git a/coderd/templates.go b/coderd/templates.go index 4280c25607ab7..c92646f65ba50 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -316,7 +316,8 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque validErrs []codersdk.ValidationError autostopRequirementDaysOfWeekParsed uint8 autostartRequirementDaysOfWeekParsed uint8 - maxPortShareLevel = database.AppSharingLevelOwner // default + maxPortShareLevel = database.AppSharingLevelOwner // default + corsBehavior = database.AppCorsBehaviorSimple // default ) if defaultTTL < 0 { validErrs = append(validErrs, codersdk.ValidationError{Field: "default_ttl_ms", Detail: "Must be a positive integer."}) @@ -345,6 +346,14 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque maxPortShareLevel = database.AppSharingLevel(*createTemplate.MaxPortShareLevel) } } + if createTemplate.CORSBehavior != nil { + val := codersdk.AppCORSBehavior(*createTemplate.CORSBehavior) + if err := val.Validate(); err != nil { + validErrs = append(validErrs, codersdk.ValidationError{Field: "cors_behavior", Detail: err.Error()}) + } else { + corsBehavior = database.AppCORSBehavior(val) + } + } if autostopRequirementWeeks < 0 { validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.weeks", Detail: "Must be a positive integer."}) @@ -403,6 +412,7 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque Icon: createTemplate.Icon, AllowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs, MaxPortSharingLevel: maxPortShareLevel, + CORSBehavior: corsBehavior, }) if err != nil { return xerrors.Errorf("insert template: %s", err) @@ -633,6 +643,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { validErrs []codersdk.ValidationError autostopRequirementDaysOfWeekParsed uint8 autostartRequirementDaysOfWeekParsed uint8 + corsBehavior database.AppCORSBehavior ) if req.DefaultTTLMillis < 0 { validErrs = append(validErrs, codersdk.ValidationError{Field: "default_ttl_ms", Detail: "Must be a positive integer."}) @@ -705,6 +716,15 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { } } + if req.CORSBehavior != nil { + val := codersdk.AppCORSBehavior(*req.CORSBehavior) + if err := val.Validate(); err != nil { + validErrs = append(validErrs, codersdk.ValidationError{Field: "cors_behavior", Detail: err.Error()}) + } else { + corsBehavior = database.AppCORSBehavior(val) + } + } + if len(validErrs) > 0 { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Invalid request to update template metadata!", @@ -732,7 +752,8 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { req.TimeTilDormantAutoDeleteMillis == time.Duration(template.TimeTilDormantAutoDelete).Milliseconds() && req.RequireActiveVersion == template.RequireActiveVersion && (deprecationMessage == template.Deprecated) && - maxPortShareLevel == template.MaxPortSharingLevel { + maxPortShareLevel == template.MaxPortSharingLevel && + corsBehavior == template.CORSBehavior { return nil } @@ -773,6 +794,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { AllowUserCancelWorkspaceJobs: req.AllowUserCancelWorkspaceJobs, GroupACL: groupACL, MaxPortSharingLevel: maxPortShareLevel, + CORSBehavior: corsBehavior, }) if err != nil { return xerrors.Errorf("update template metadata: %w", err) @@ -1055,6 +1077,7 @@ func (api *API) convertTemplate( Deprecated: templateAccessControl.IsDeprecated(), DeprecationMessage: templateAccessControl.Deprecated, MaxPortShareLevel: maxPortShareLevel, + CORSBehavior: codersdk.AppCORSBehavior(template.CORSBehavior), } } diff --git a/coderd/workspaceapps/apptest/apptest.go b/coderd/workspaceapps/apptest/apptest.go index a677778114ceb..99eb55223d294 100644 --- a/coderd/workspaceapps/apptest/apptest.go +++ b/coderd/workspaceapps/apptest/apptest.go @@ -1463,6 +1463,151 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { require.Equal(t, http.StatusOK, resp.StatusCode) assertWorkspaceLastUsedAtUpdated(t, appDetails) }) + + t.Run("CORS", func(t *testing.T) { + t.Parallel() + + // Set up test headers that should be returned by the app + testHeaders := http.Header{ + "Access-Control-Allow-Origin": []string{"*"}, + "Access-Control-Allow-Methods": []string{"GET, POST, OPTIONS"}, + } + + unauthenticatedClient := func(t *testing.T, appDetails *Details) *codersdk.Client { + c := appDetails.AppClient(t) + c.SetSessionToken("") + return c + } + + authenticatedClient := func(t *testing.T, appDetails *Details) *codersdk.Client { + uc, _ := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, appDetails.FirstUser.OrganizationID, rbac.RoleMember()) + c := appDetails.AppClient(t) + c.SetSessionToken(uc.SessionToken()) + return c + } + + ownerClient := func(t *testing.T, appDetails *Details) *codersdk.Client { + return appDetails.SDKClient + } + + tests := []struct { + name string + shareLevel codersdk.WorkspaceAgentPortShareLevel + behavior codersdk.AppCORSBehavior + client func(t *testing.T, appDetails *Details) *codersdk.Client + expectedStatusCode int + expectedCORSHeaders bool + }{ + // Public + { + name: "Default/Public", + shareLevel: codersdk.WorkspaceAgentPortShareLevelPublic, + behavior: codersdk.AppCORSBehaviorSimple, + expectedCORSHeaders: false, + client: unauthenticatedClient, + expectedStatusCode: http.StatusOK, + }, + { + name: "Passthru/Public", + shareLevel: codersdk.WorkspaceAgentPortShareLevelPublic, + behavior: codersdk.AppCORSBehaviorPassthru, + expectedCORSHeaders: true, + client: unauthenticatedClient, + expectedStatusCode: http.StatusOK, + }, + // Authenticated + { + name: "Default/Authenticated", + shareLevel: codersdk.WorkspaceAgentPortShareLevelAuthenticated, + behavior: codersdk.AppCORSBehaviorSimple, + expectedCORSHeaders: false, + client: authenticatedClient, + expectedStatusCode: http.StatusOK, + }, + { + name: "Passthru/Authenticated", + shareLevel: codersdk.WorkspaceAgentPortShareLevelAuthenticated, + behavior: codersdk.AppCORSBehaviorPassthru, + expectedCORSHeaders: true, + client: authenticatedClient, + expectedStatusCode: http.StatusOK, + }, + { + // The CORS behavior will not affect unauthenticated requests. + // The request will be redirected to the login page. + name: "Passthru/Unauthenticated", + shareLevel: codersdk.WorkspaceAgentPortShareLevelAuthenticated, + behavior: codersdk.AppCORSBehaviorPassthru, + expectedCORSHeaders: false, + client: unauthenticatedClient, + expectedStatusCode: http.StatusSeeOther, + }, + // Owner + { + name: "Default/Owner", + shareLevel: codersdk.WorkspaceAgentPortShareLevelAuthenticated, // Owner is not a valid share level for ports. + behavior: codersdk.AppCORSBehaviorSimple, + expectedCORSHeaders: false, + client: ownerClient, + expectedStatusCode: http.StatusOK, + }, + { + name: "Passthru/Owner", + shareLevel: codersdk.WorkspaceAgentPortShareLevelAuthenticated, // Owner is not a valid share level for ports. + behavior: codersdk.AppCORSBehaviorPassthru, + expectedCORSHeaders: true, + client: ownerClient, + expectedStatusCode: http.StatusOK, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + appDetails := setupProxyTest(t, &DeploymentOptions{ + headers: testHeaders, + }) + port, err := strconv.ParseInt(appDetails.Apps.Port.AppSlugOrPort, 10, 32) + require.NoError(t, err) + + // Update the template CORS behavior. + b := codersdk.AppCORSBehavior(tc.behavior) + template, err := appDetails.SDKClient.UpdateTemplateMeta(ctx, appDetails.Workspace.TemplateID, codersdk.UpdateTemplateMeta{ + CORSBehavior: &b, + }) + require.NoError(t, err) + require.Equal(t, tc.behavior, template.CORSBehavior) + + // Set the port we have to be shared. + _, err = appDetails.SDKClient.UpsertWorkspaceAgentPortShare(ctx, appDetails.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{ + AgentName: proxyTestAgentName, + Port: int32(port), + ShareLevel: tc.shareLevel, + Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP, + }) + require.NoError(t, err) + + client := tc.client(t, appDetails) + + resp, err := requestWithRetries(ctx, t, client, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.Port).String(), nil) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, tc.expectedStatusCode, resp.StatusCode) + + if tc.expectedCORSHeaders { + require.Equal(t, testHeaders.Get("Access-Control-Allow-Origin"), resp.Header.Get("Access-Control-Allow-Origin")) + require.Equal(t, testHeaders.Get("Access-Control-Allow-Methods"), resp.Header.Get("Access-Control-Allow-Methods")) + } else { + require.Empty(t, resp.Header.Get("Access-Control-Allow-Origin")) + require.Empty(t, resp.Header.Get("Access-Control-Allow-Methods")) + } + }) + } + }) }) t.Run("AppSharing", func(t *testing.T) { diff --git a/coderd/workspaceapps/request.go b/coderd/workspaceapps/request.go index ce99d4ccdbcf8..ccd4d97f67161 100644 --- a/coderd/workspaceapps/request.go +++ b/coderd/workspaceapps/request.go @@ -299,9 +299,6 @@ func (r Request) getDatabase(ctx context.Context, db database.Store) (*databaseR ) //nolint:nestif if portUintErr == nil { - // TODO: handle CORS passthru for port sharing use-case. - appCORSBehavior = database.AppCorsBehaviorSimple - protocol := "http" if strings.HasSuffix(r.AppSlugOrPort, "s") { protocol = "https" @@ -358,6 +355,12 @@ func (r Request) getDatabase(ctx context.Context, db database.Store) (*databaseR } else { appSharingLevel = ps.ShareLevel } + + tmpl, err := db.GetTemplateByID(ctx, workspace.TemplateID) + if err != nil { + return nil, xerrors.Errorf("get template %q: %w", workspace.TemplateID, err) + } + appCORSBehavior = tmpl.CORSBehavior } else { for _, app := range apps { if app.Slug == r.AppSlugOrPort { diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 4966b7a41809c..c8968df1cfd2b 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -189,6 +189,9 @@ type CreateTemplateRequest struct { // MaxPortShareLevel allows optionally specifying the maximum port share level // for workspaces created from the template. MaxPortShareLevel *WorkspaceAgentPortShareLevel `json:"max_port_share_level"` + + // CORSBehavior allows optionally specifying the CORS behavior for all shared ports. + CORSBehavior *AppCORSBehavior `json:"cors_behavior"` } // CreateWorkspaceRequest provides options for creating a new workspace. diff --git a/codersdk/templates.go b/codersdk/templates.go index 378b64103be93..71c4ffaa364aa 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -61,6 +61,7 @@ type Template struct { // template version. RequireActiveVersion bool `json:"require_active_version"` MaxPortShareLevel WorkspaceAgentPortShareLevel `json:"max_port_share_level"` + CORSBehavior AppCORSBehavior `json:"cors_behavior"` } // WeekdaysToBitmap converts a list of weekdays to a bitmap in accordance with @@ -250,6 +251,7 @@ type UpdateTemplateMeta struct { // of the template. DisableEveryoneGroupAccess bool `json:"disable_everyone_group_access"` MaxPortShareLevel *WorkspaceAgentPortShareLevel `json:"max_port_share_level"` + CORSBehavior *AppCORSBehavior `json:"cors_behavior"` } type TemplateExample struct { diff --git a/docs/admin/security/audit-logs.md b/docs/admin/security/audit-logs.md index db214b0e1443e..af9beba0cfc9a 100644 --- a/docs/admin/security/audit-logs.md +++ b/docs/admin/security/audit-logs.md @@ -8,27 +8,27 @@ We track the following resources: -| Resource | | -| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| APIKey
login, logout, register, create, delete |
FieldTracked
created_attrue
expires_attrue
hashed_secretfalse
idfalse
ip_addressfalse
last_usedtrue
lifetime_secondsfalse
login_typefalse
scopefalse
token_namefalse
updated_atfalse
user_idtrue
| -| AuditOAuthConvertState
|
FieldTracked
created_attrue
expires_attrue
from_login_typetrue
to_login_typetrue
user_idtrue
| -| Group
create, write, delete |
FieldTracked
avatar_urltrue
display_nametrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
sourcefalse
| -| AuditableOrganizationMember
|
FieldTracked
created_attrue
organization_idfalse
rolestrue
updated_attrue
user_idtrue
usernametrue
| -| CustomRole
|
FieldTracked
created_atfalse
display_nametrue
idfalse
nametrue
org_permissionstrue
organization_idfalse
site_permissionstrue
updated_atfalse
user_permissionstrue
| -| GitSSHKey
create |
FieldTracked
created_atfalse
private_keytrue
public_keytrue
updated_atfalse
user_idtrue
| -| HealthSettings
|
FieldTracked
dismissed_healthcheckstrue
idfalse
| -| License
create, delete |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| -| NotificationTemplate
|
FieldTracked
actionstrue
body_templatetrue
grouptrue
idfalse
kindtrue
methodtrue
nametrue
title_templatetrue
| -| NotificationsSettings
|
FieldTracked
idfalse
notifier_pausedtrue
| -| OAuth2ProviderApp
|
FieldTracked
callback_urltrue
created_atfalse
icontrue
idfalse
nametrue
updated_atfalse
| -| OAuth2ProviderAppSecret
|
FieldTracked
app_idfalse
created_atfalse
display_secretfalse
hashed_secretfalse
idfalse
last_used_atfalse
secret_prefixfalse
| -| Organization
|
FieldTracked
created_atfalse
descriptiontrue
display_nametrue
icontrue
idfalse
is_defaulttrue
nametrue
updated_attrue
| -| Template
write, delete |
FieldTracked
active_version_idtrue
activity_bumptrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
autostart_block_days_of_weektrue
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
default_ttltrue
deletedfalse
deprecatedtrue
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_port_sharing_leveltrue
nametrue
organization_display_namefalse
organization_iconfalse
organization_idfalse
organization_namefalse
provisionertrue
require_active_versiontrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
user_acltrue
| -| TemplateVersion
create, write |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
external_auth_providersfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
source_example_idfalse
template_idtrue
updated_atfalse
| -| User
create, write, delete |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
github_com_user_idfalse
hashed_one_time_passcodefalse
hashed_passwordtrue
idtrue
last_seen_atfalse
login_typetrue
nametrue
one_time_passcode_expires_attrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
theme_preferencefalse
updated_atfalse
usernametrue
| -| WorkspaceBuild
start, stop |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
transitionfalse
updated_atfalse
workspace_idfalse
| -| WorkspaceProxy
|
FieldTracked
created_attrue
deletedfalse
derp_enabledtrue
derp_onlytrue
display_nametrue
icontrue
idtrue
nametrue
region_idtrue
token_hashed_secrettrue
updated_atfalse
urltrue
versiontrue
wildcard_hostnametrue
| -| WorkspaceTable
|
FieldTracked
automatic_updatestrue
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
dormant_attrue
favoritetrue
idtrue
last_used_atfalse
nametrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
| +| Resource | | +| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| APIKey
login, logout, register, create, delete |
FieldTracked
created_attrue
expires_attrue
hashed_secretfalse
idfalse
ip_addressfalse
last_usedtrue
lifetime_secondsfalse
login_typefalse
scopefalse
token_namefalse
updated_atfalse
user_idtrue
| +| AuditOAuthConvertState
|
FieldTracked
created_attrue
expires_attrue
from_login_typetrue
to_login_typetrue
user_idtrue
| +| Group
create, write, delete |
FieldTracked
avatar_urltrue
display_nametrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
sourcefalse
| +| AuditableOrganizationMember
|
FieldTracked
created_attrue
organization_idfalse
rolestrue
updated_attrue
user_idtrue
usernametrue
| +| CustomRole
|
FieldTracked
created_atfalse
display_nametrue
idfalse
nametrue
org_permissionstrue
organization_idfalse
site_permissionstrue
updated_atfalse
user_permissionstrue
| +| GitSSHKey
create |
FieldTracked
created_atfalse
private_keytrue
public_keytrue
updated_atfalse
user_idtrue
| +| HealthSettings
|
FieldTracked
dismissed_healthcheckstrue
idfalse
| +| License
create, delete |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| +| NotificationTemplate
|
FieldTracked
actionstrue
body_templatetrue
grouptrue
idfalse
kindtrue
methodtrue
nametrue
title_templatetrue
| +| NotificationsSettings
|
FieldTracked
idfalse
notifier_pausedtrue
| +| OAuth2ProviderApp
|
FieldTracked
callback_urltrue
created_atfalse
icontrue
idfalse
nametrue
updated_atfalse
| +| OAuth2ProviderAppSecret
|
FieldTracked
app_idfalse
created_atfalse
display_secretfalse
hashed_secretfalse
idfalse
last_used_atfalse
secret_prefixfalse
| +| Organization
|
FieldTracked
created_atfalse
descriptiontrue
display_nametrue
icontrue
idfalse
is_defaulttrue
nametrue
updated_attrue
| +| Template
write, delete |
FieldTracked
active_version_idtrue
activity_bumptrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
autostart_block_days_of_weektrue
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
cors_behaviortrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
default_ttltrue
deletedfalse
deprecatedtrue
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_port_sharing_leveltrue
nametrue
organization_display_namefalse
organization_iconfalse
organization_idfalse
organization_namefalse
provisionertrue
require_active_versiontrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
user_acltrue
| +| TemplateVersion
create, write |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
external_auth_providersfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
source_example_idfalse
template_idtrue
updated_atfalse
| +| User
create, write, delete |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
github_com_user_idfalse
hashed_one_time_passcodefalse
hashed_passwordtrue
idtrue
last_seen_atfalse
login_typetrue
nametrue
one_time_passcode_expires_attrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
theme_preferencefalse
updated_atfalse
usernametrue
| +| WorkspaceBuild
start, stop |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
transitionfalse
updated_atfalse
workspace_idfalse
| +| WorkspaceProxy
|
FieldTracked
created_attrue
deletedfalse
derp_enabledtrue
derp_onlytrue
display_nametrue
icontrue
idtrue
nametrue
region_idtrue
token_hashed_secrettrue
updated_atfalse
urltrue
versiontrue
wildcard_hostnametrue
| +| WorkspaceTable
|
FieldTracked
automatic_updatestrue
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
dormant_attrue
favoritetrue
idtrue
last_used_atfalse
nametrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
| diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 35c677bccdda0..4b4c3170e6c2e 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -415,6 +415,21 @@ | `envbuilder` | | `exectrace` | +## codersdk.AppCORSBehavior + +```json +"simple" +``` + +### Properties + +#### Enumerated Values + +| Value | +| ---------- | +| `simple` | +| `passthru` | + ## codersdk.AppHostResponse ```json @@ -1174,6 +1189,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "days_of_week": ["monday"], "weeks": 0 }, + "cors_behavior": "simple", "default_ttl_ms": 0, "delete_ttl_ms": 0, "description": "string", @@ -1199,6 +1215,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `allow_user_cancel_workspace_jobs` | boolean | false | | Allow users to cancel in-progress workspace jobs. \*bool as the default value is "true". | | `autostart_requirement` | [codersdk.TemplateAutostartRequirement](#codersdktemplateautostartrequirement) | false | | Autostart requirement allows optionally specifying the autostart allowed days for workspaces created from this template. This is an enterprise feature. | | `autostop_requirement` | [codersdk.TemplateAutostopRequirement](#codersdktemplateautostoprequirement) | false | | Autostop requirement allows optionally specifying the autostop requirement for workspaces created from this template. This is an enterprise feature. | +| `cors_behavior` | [codersdk.AppCORSBehavior](#codersdkappcorsbehavior) | false | | Cors behavior allows optionally specifying the CORS behavior for all shared ports. | | `default_ttl_ms` | integer | false | | Default ttl ms allows optionally specifying the default TTL for all workspaces created from this template. | | `delete_ttl_ms` | integer | false | | Delete ttl ms allows optionally specifying the max lifetime before Coder permanently deletes dormant workspaces created from this template. | | `description` | string | false | | Description is a description of what the template contains. It must be less than 128 bytes. | @@ -5108,6 +5125,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "p95": 146 } }, + "cors_behavior": "simple", "created_at": "2019-08-24T14:15:22Z", "created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f", "created_by_name": "string", @@ -5146,6 +5164,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `autostart_requirement` | [codersdk.TemplateAutostartRequirement](#codersdktemplateautostartrequirement) | false | | | | `autostop_requirement` | [codersdk.TemplateAutostopRequirement](#codersdktemplateautostoprequirement) | false | | Autostop requirement and AutostartRequirement are enterprise features. Its value is only used if your license is entitled to use the advanced template scheduling feature. | | `build_time_stats` | [codersdk.TemplateBuildTimeStats](#codersdktemplatebuildtimestats) | false | | | +| `cors_behavior` | [codersdk.AppCORSBehavior](#codersdkappcorsbehavior) | false | | | | `created_at` | string | false | | | | `created_by_id` | string | false | | | | `created_by_name` | string | false | | | diff --git a/docs/reference/api/templates.md b/docs/reference/api/templates.md index d7da209e94771..018bd41350143 100644 --- a/docs/reference/api/templates.md +++ b/docs/reference/api/templates.md @@ -49,6 +49,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "p95": 146 } }, + "cors_behavior": "simple", "created_at": "2019-08-24T14:15:22Z", "created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f", "created_by_name": "string", @@ -104,6 +105,7 @@ Status Code **200** | `»» [any property]` | [codersdk.TransitionStats](schemas.md#codersdktransitionstats) | false | | | | `»»» p50` | integer | false | | | | `»»» p95` | integer | false | | | +| `» cors_behavior` | [codersdk.AppCORSBehavior](schemas.md#codersdkappcorsbehavior) | false | | | | `» created_at` | string(date-time) | false | | | | `» created_by_id` | string(uuid) | false | | | | `» created_by_name` | string | false | | | @@ -131,6 +133,8 @@ Status Code **200** | Property | Value | | ---------------------- | --------------- | +| `cors_behavior` | `simple` | +| `cors_behavior` | `passthru` | | `max_port_share_level` | `owner` | | `max_port_share_level` | `authenticated` | | `max_port_share_level` | `public` | @@ -167,6 +171,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa "days_of_week": ["monday"], "weeks": 0 }, + "cors_behavior": "simple", "default_ttl_ms": 0, "delete_ttl_ms": 0, "description": "string", @@ -218,6 +223,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa "p95": 146 } }, + "cors_behavior": "simple", "created_at": "2019-08-24T14:15:22Z", "created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f", "created_by_name": "string", @@ -360,6 +366,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "p95": 146 } }, + "cors_behavior": "simple", "created_at": "2019-08-24T14:15:22Z", "created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f", "created_by_name": "string", @@ -688,6 +695,7 @@ curl -X GET http://coder-server:8080/api/v2/templates \ "p95": 146 } }, + "cors_behavior": "simple", "created_at": "2019-08-24T14:15:22Z", "created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f", "created_by_name": "string", @@ -743,6 +751,7 @@ Status Code **200** | `»» [any property]` | [codersdk.TransitionStats](schemas.md#codersdktransitionstats) | false | | | | `»»» p50` | integer | false | | | | `»»» p95` | integer | false | | | +| `» cors_behavior` | [codersdk.AppCORSBehavior](schemas.md#codersdkappcorsbehavior) | false | | | | `» created_at` | string(date-time) | false | | | | `» created_by_id` | string(uuid) | false | | | | `» created_by_name` | string | false | | | @@ -770,6 +779,8 @@ Status Code **200** | Property | Value | | ---------------------- | --------------- | +| `cors_behavior` | `simple` | +| `cors_behavior` | `passthru` | | `max_port_share_level` | `owner` | | `max_port_share_level` | `authenticated` | | `max_port_share_level` | `public` | @@ -879,6 +890,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template} \ "p95": 146 } }, + "cors_behavior": "simple", "created_at": "2019-08-24T14:15:22Z", "created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f", "created_by_name": "string", @@ -1004,6 +1016,7 @@ curl -X PATCH http://coder-server:8080/api/v2/templates/{template} \ "p95": 146 } }, + "cors_behavior": "simple", "created_at": "2019-08-24T14:15:22Z", "created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f", "created_by_name": "string", diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index 24f7dfa4b4fe0..5f8cfa05e9569 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -110,6 +110,7 @@ var auditableResourcesTypes = map[any]map[string]Action{ "require_active_version": ActionTrack, "deprecated": ActionTrack, "max_port_sharing_level": ActionTrack, + "cors_behavior": ActionTrack, "activity_bump": ActionTrack, }, &database.TemplateVersion{}: { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index dc63e7f70fd54..6a20032b8f7ee 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -292,6 +292,7 @@ export interface CreateTemplateRequest { readonly disable_everyone_group_access: boolean; readonly require_active_version: boolean; readonly max_port_share_level?: WorkspaceAgentPortShareLevel; + readonly cors_behavior?: AppCORSBehavior; } // From codersdk/templateversions.go @@ -1347,6 +1348,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: AppCORSBehavior; } // From codersdk/templates.go @@ -1620,6 +1622,7 @@ export interface UpdateTemplateMeta { readonly deprecation_message?: string; readonly disable_everyone_group_access: boolean; readonly max_port_share_level?: WorkspaceAgentPortShareLevel; + readonly cors_behavior?: AppCORSBehavior; } // From codersdk/users.go diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx index 02e2067f3b9ef..627040c162951 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx @@ -4,6 +4,7 @@ import FormHelperText from "@mui/material/FormHelperText"; import MenuItem from "@mui/material/MenuItem"; import TextField from "@mui/material/TextField"; import { + AppCORSBehaviors, type Template, type UpdateTemplateMeta, WorkspaceAppSharingLevels, @@ -47,6 +48,7 @@ export const validationSchema = Yup.object({ require_active_version: Yup.boolean(), deprecation_message: Yup.string(), max_port_sharing_level: Yup.string().oneOf(WorkspaceAppSharingLevels), + cors_behavior: Yup.string().oneOf(Object.values(AppCORSBehaviors)), }); export interface TemplateSettingsForm { @@ -87,6 +89,7 @@ export const TemplateSettingsForm: FC = ({ deprecation_message: template.deprecation_message, disable_everyone_group_access: false, max_port_share_level: template.max_port_share_level, + cors_behavior: template.cors_behavior, }, validationSchema, onSubmit, @@ -290,6 +293,28 @@ export const TemplateSettingsForm: FC = ({ + + + + Simple + Passthru + + + + ); diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx index cfe52db26a5a8..e86e1eea5b658 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx @@ -55,6 +55,7 @@ const validFormValues: FormValues = { require_active_version: false, disable_everyone_group_access: false, max_port_share_level: "owner", + cors_behavior: "simple", }; const renderTemplateSettingsPage = async () => { diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 1593790e9792d..80f9c2184678b 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -779,6 +779,7 @@ export const MockTemplate: TypesGen.Template = { deprecated: false, deprecation_message: "", max_port_share_level: "public", + cors_behavior: "simple", }; export const MockTemplateVersionFiles: TemplateVersionFiles = {