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 = { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy