mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: allow bypassing current CORS magic based on template config (#18706)
Solves https://github.com/coder/coder/issues/15096 This is a slight rework/refactor of the earlier PRs from @dannykopping and @Emyrk: - https://github.com/coder/coder/pull/15669 - https://github.com/coder/coder/pull/15684 - https://github.com/coder/coder/pull/17596 Rather than having a per-app CORS behaviour setting and additionally a template level setting for ports, this PR adds a single template level CORS behaviour setting that is then used by all apps/ports for workspaces created from that template. The main changes are in `proxy.go` and `request.go` to: a) get the CORS behaviour setting from the template b) have `HandleSubdomain` bypass the CORS middleware handler if the selected behaviour is `passthru` c) in `proxyWorkspaceApp`, do not modify the response if the selected behaviour is `passthru` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for configuring CORS behavior ("simple" or "passthru") at the template level for all shared ports. * Introduced a new "CORS Behavior" setting in the template creation and settings forms. * API endpoints and responses now include the optional `cors_behavior` property for templates. * Workspace apps and proxy now honor the specified CORS behavior, enabling conditional CORS middleware application. * Enhanced workspace app tests with comprehensive scenarios covering CORS behaviors and authentication states. * **Bug Fixes** * None. * **Documentation** * Updated API and admin documentation to describe the new `cors_behavior` property and its usage. * Added examples and schema references for CORS behavior in relevant API docs. * **Tests** * Extended automated tests to cover different CORS behavior scenarios for templates and workspace apps. * **Chores** * Updated audit logging to track changes to the `cors_behavior` field on templates. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Callum Styan <callumstyan@gmail.com>
This commit is contained in:
Generated
+22
@@ -11467,6 +11467,17 @@ const docTemplate = `{
|
|||||||
"BuildReasonJetbrainsConnection"
|
"BuildReasonJetbrainsConnection"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"codersdk.CORSBehavior": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"simple",
|
||||||
|
"passthru"
|
||||||
|
],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"CORSBehaviorSimple",
|
||||||
|
"CORSBehaviorPassthru"
|
||||||
|
]
|
||||||
|
},
|
||||||
"codersdk.ChangePasswordWithOneTimePasscodeRequest": {
|
"codersdk.ChangePasswordWithOneTimePasscodeRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@@ -11808,6 +11819,14 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"cors_behavior": {
|
||||||
|
"description": "CORSBehavior allows optionally specifying the CORS behavior for all shared ports.",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/codersdk.CORSBehavior"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"default_ttl_ms": {
|
"default_ttl_ms": {
|
||||||
"description": "DefaultTTLMillis allows optionally specifying the default TTL\nfor all workspaces created from this template.",
|
"description": "DefaultTTLMillis allows optionally specifying the default TTL\nfor all workspaces created from this template.",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
@@ -16215,6 +16234,9 @@ const docTemplate = `{
|
|||||||
"build_time_stats": {
|
"build_time_stats": {
|
||||||
"$ref": "#/definitions/codersdk.TemplateBuildTimeStats"
|
"$ref": "#/definitions/codersdk.TemplateBuildTimeStats"
|
||||||
},
|
},
|
||||||
|
"cors_behavior": {
|
||||||
|
"$ref": "#/definitions/codersdk.CORSBehavior"
|
||||||
|
},
|
||||||
"created_at": {
|
"created_at": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
|
|||||||
Generated
+16
@@ -10202,6 +10202,11 @@
|
|||||||
"BuildReasonJetbrainsConnection"
|
"BuildReasonJetbrainsConnection"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"codersdk.CORSBehavior": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["simple", "passthru"],
|
||||||
|
"x-enum-varnames": ["CORSBehaviorSimple", "CORSBehaviorPassthru"]
|
||||||
|
},
|
||||||
"codersdk.ChangePasswordWithOneTimePasscodeRequest": {
|
"codersdk.ChangePasswordWithOneTimePasscodeRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["email", "one_time_passcode", "password"],
|
"required": ["email", "one_time_passcode", "password"],
|
||||||
@@ -10525,6 +10530,14 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"cors_behavior": {
|
||||||
|
"description": "CORSBehavior allows optionally specifying the CORS behavior for all shared ports.",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/codersdk.CORSBehavior"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"default_ttl_ms": {
|
"default_ttl_ms": {
|
||||||
"description": "DefaultTTLMillis allows optionally specifying the default TTL\nfor all workspaces created from this template.",
|
"description": "DefaultTTLMillis allows optionally specifying the default TTL\nfor all workspaces created from this template.",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
@@ -14774,6 +14787,9 @@
|
|||||||
"build_time_stats": {
|
"build_time_stats": {
|
||||||
"$ref": "#/definitions/codersdk.TemplateBuildTimeStats"
|
"$ref": "#/definitions/codersdk.TemplateBuildTimeStats"
|
||||||
},
|
},
|
||||||
|
"cors_behavior": {
|
||||||
|
"$ref": "#/definitions/codersdk.CORSBehavior"
|
||||||
|
},
|
||||||
"created_at": {
|
"created_at": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
|
|||||||
@@ -1462,6 +1462,7 @@ func (s *MethodTestSuite) TestTemplate() {
|
|||||||
Provisioner: "echo",
|
Provisioner: "echo",
|
||||||
OrganizationID: orgID,
|
OrganizationID: orgID,
|
||||||
MaxPortSharingLevel: database.AppSharingLevelOwner,
|
MaxPortSharingLevel: database.AppSharingLevelOwner,
|
||||||
|
CorsBehavior: database.CorsBehaviorSimple,
|
||||||
}).Asserts(rbac.ResourceTemplate.InOrg(orgID), policy.ActionCreate)
|
}).Asserts(rbac.ResourceTemplate.InOrg(orgID), policy.ActionCreate)
|
||||||
}))
|
}))
|
||||||
s.Run("InsertTemplateVersion", s.Subtest(func(db database.Store, check *expects) {
|
s.Run("InsertTemplateVersion", s.Subtest(func(db database.Store, check *expects) {
|
||||||
@@ -1582,6 +1583,7 @@ func (s *MethodTestSuite) TestTemplate() {
|
|||||||
check.Args(database.UpdateTemplateMetaByIDParams{
|
check.Args(database.UpdateTemplateMetaByIDParams{
|
||||||
ID: t1.ID,
|
ID: t1.ID,
|
||||||
MaxPortSharingLevel: "owner",
|
MaxPortSharingLevel: "owner",
|
||||||
|
CorsBehavior: database.CorsBehaviorSimple,
|
||||||
}).Asserts(t1, policy.ActionUpdate)
|
}).Asserts(t1, policy.ActionUpdate)
|
||||||
}))
|
}))
|
||||||
s.Run("UpdateTemplateVersionByID", s.Subtest(func(db database.Store, check *expects) {
|
s.Run("UpdateTemplateVersionByID", s.Subtest(func(db database.Store, check *expects) {
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ func Template(t testing.TB, db database.Store, seed database.Template) database.
|
|||||||
AllowUserCancelWorkspaceJobs: seed.AllowUserCancelWorkspaceJobs,
|
AllowUserCancelWorkspaceJobs: seed.AllowUserCancelWorkspaceJobs,
|
||||||
MaxPortSharingLevel: takeFirst(seed.MaxPortSharingLevel, database.AppSharingLevelOwner),
|
MaxPortSharingLevel: takeFirst(seed.MaxPortSharingLevel, database.AppSharingLevelOwner),
|
||||||
UseClassicParameterFlow: takeFirst(seed.UseClassicParameterFlow, false),
|
UseClassicParameterFlow: takeFirst(seed.UseClassicParameterFlow, false),
|
||||||
|
CorsBehavior: takeFirst(seed.CorsBehavior, database.CorsBehaviorSimple),
|
||||||
})
|
})
|
||||||
require.NoError(t, err, "insert template")
|
require.NoError(t, err, "insert template")
|
||||||
|
|
||||||
|
|||||||
Generated
+8
-1
@@ -73,6 +73,11 @@ CREATE TYPE connection_type AS ENUM (
|
|||||||
'port_forwarding'
|
'port_forwarding'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TYPE cors_behavior AS ENUM (
|
||||||
|
'simple',
|
||||||
|
'passthru'
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TYPE crypto_key_feature AS ENUM (
|
CREATE TYPE crypto_key_feature AS ENUM (
|
||||||
'workspace_apps_token',
|
'workspace_apps_token',
|
||||||
'workspace_apps_api_key',
|
'workspace_apps_api_key',
|
||||||
@@ -1750,7 +1755,8 @@ CREATE TABLE templates (
|
|||||||
deprecated text DEFAULT ''::text NOT NULL,
|
deprecated text DEFAULT ''::text NOT NULL,
|
||||||
activity_bump bigint DEFAULT '3600000000000'::bigint 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,
|
||||||
use_classic_parameter_flow boolean DEFAULT false NOT NULL
|
use_classic_parameter_flow boolean DEFAULT false NOT NULL,
|
||||||
|
cors_behavior cors_behavior DEFAULT 'simple'::cors_behavior NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
COMMENT ON COLUMN templates.default_ttl IS 'The default duration for autostop for workspaces created from this template.';
|
COMMENT ON COLUMN templates.default_ttl IS 'The default duration for autostop for workspaces created from this template.';
|
||||||
@@ -1803,6 +1809,7 @@ CREATE VIEW template_with_names AS
|
|||||||
templates.activity_bump,
|
templates.activity_bump,
|
||||||
templates.max_port_sharing_level,
|
templates.max_port_sharing_level,
|
||||||
templates.use_classic_parameter_flow,
|
templates.use_classic_parameter_flow,
|
||||||
|
templates.cors_behavior,
|
||||||
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
|
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
|
||||||
COALESCE(visible_users.username, ''::text) AS created_by_username,
|
COALESCE(visible_users.username, ''::text) AS created_by_username,
|
||||||
COALESCE(visible_users.name, ''::text) AS created_by_name,
|
COALESCE(visible_users.name, ''::text) AS created_by_name,
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
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.use_classic_parameter_flow,
|
||||||
|
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
|
||||||
|
COALESCE(visible_users.username, ''::text) AS created_by_username,
|
||||||
|
COALESCE(visible_users.name, ''::text) AS created_by_name,
|
||||||
|
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;
|
||||||
|
|
||||||
|
DROP TYPE IF EXISTS cors_behavior;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
CREATE TYPE cors_behavior AS ENUM (
|
||||||
|
'simple',
|
||||||
|
'passthru'
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE templates
|
||||||
|
ADD COLUMN cors_behavior cors_behavior NOT NULL DEFAULT 'simple'::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.use_classic_parameter_flow,
|
||||||
|
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(visible_users.name, ''::text) AS created_by_name,
|
||||||
|
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.';
|
||||||
@@ -120,6 +120,7 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate
|
|||||||
&i.ActivityBump,
|
&i.ActivityBump,
|
||||||
&i.MaxPortSharingLevel,
|
&i.MaxPortSharingLevel,
|
||||||
&i.UseClassicParameterFlow,
|
&i.UseClassicParameterFlow,
|
||||||
|
&i.CorsBehavior,
|
||||||
&i.CreatedByAvatarURL,
|
&i.CreatedByAvatarURL,
|
||||||
&i.CreatedByUsername,
|
&i.CreatedByUsername,
|
||||||
&i.CreatedByName,
|
&i.CreatedByName,
|
||||||
|
|||||||
@@ -559,6 +559,64 @@ func AllConnectionTypeValues() []ConnectionType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CorsBehavior string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CorsBehaviorSimple CorsBehavior = "simple"
|
||||||
|
CorsBehaviorPassthru CorsBehavior = "passthru"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *CorsBehavior) Scan(src interface{}) error {
|
||||||
|
switch s := src.(type) {
|
||||||
|
case []byte:
|
||||||
|
*e = CorsBehavior(s)
|
||||||
|
case string:
|
||||||
|
*e = CorsBehavior(s)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported scan type for CorsBehavior: %T", src)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type NullCorsBehavior struct {
|
||||||
|
CorsBehavior CorsBehavior `json:"cors_behavior"`
|
||||||
|
Valid bool `json:"valid"` // Valid is true if CorsBehavior is not NULL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan implements the Scanner interface.
|
||||||
|
func (ns *NullCorsBehavior) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
ns.CorsBehavior, ns.Valid = "", false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ns.Valid = true
|
||||||
|
return ns.CorsBehavior.Scan(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver Valuer interface.
|
||||||
|
func (ns NullCorsBehavior) Value() (driver.Value, error) {
|
||||||
|
if !ns.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return string(ns.CorsBehavior), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e CorsBehavior) Valid() bool {
|
||||||
|
switch e {
|
||||||
|
case CorsBehaviorSimple,
|
||||||
|
CorsBehaviorPassthru:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func AllCorsBehaviorValues() []CorsBehavior {
|
||||||
|
return []CorsBehavior{
|
||||||
|
CorsBehaviorSimple,
|
||||||
|
CorsBehaviorPassthru,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type CryptoKeyFeature string
|
type CryptoKeyFeature string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -3474,6 +3532,7 @@ type Template struct {
|
|||||||
ActivityBump int64 `db:"activity_bump" json:"activity_bump"`
|
ActivityBump int64 `db:"activity_bump" json:"activity_bump"`
|
||||||
MaxPortSharingLevel AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"`
|
MaxPortSharingLevel AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"`
|
||||||
UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"`
|
UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"`
|
||||||
|
CorsBehavior CorsBehavior `db:"cors_behavior" json:"cors_behavior"`
|
||||||
CreatedByAvatarURL string `db:"created_by_avatar_url" json:"created_by_avatar_url"`
|
CreatedByAvatarURL string `db:"created_by_avatar_url" json:"created_by_avatar_url"`
|
||||||
CreatedByUsername string `db:"created_by_username" json:"created_by_username"`
|
CreatedByUsername string `db:"created_by_username" json:"created_by_username"`
|
||||||
CreatedByName string `db:"created_by_name" json:"created_by_name"`
|
CreatedByName string `db:"created_by_name" json:"created_by_name"`
|
||||||
@@ -3521,7 +3580,8 @@ type TemplateTable struct {
|
|||||||
ActivityBump int64 `db:"activity_bump" json:"activity_bump"`
|
ActivityBump int64 `db:"activity_bump" json:"activity_bump"`
|
||||||
MaxPortSharingLevel AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"`
|
MaxPortSharingLevel AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"`
|
||||||
// Determines whether to default to the dynamic parameter creation flow for this template or continue using the legacy classic parameter creation flow.This is a template wide setting, the template admin can revert to the classic flow if there are any issues. An escape hatch is required, as workspace creation is a core workflow and cannot break. This column will be removed when the dynamic parameter creation flow is stable.
|
// Determines whether to default to the dynamic parameter creation flow for this template or continue using the legacy classic parameter creation flow.This is a template wide setting, the template admin can revert to the classic flow if there are any issues. An escape hatch is required, as workspace creation is a core workflow and cannot break. This column will be removed when the dynamic parameter creation flow is stable.
|
||||||
UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"`
|
UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"`
|
||||||
|
CorsBehavior CorsBehavior `db:"cors_behavior" json:"cors_behavior"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Records aggregated usage statistics for templates/users. All usage is rounded up to the nearest minute.
|
// Records aggregated usage statistics for templates/users. All usage is rounded up to the nearest minute.
|
||||||
|
|||||||
@@ -11768,7 +11768,7 @@ func (q *sqlQuerier) GetTemplateAverageBuildTime(ctx context.Context, arg GetTem
|
|||||||
|
|
||||||
const getTemplateByID = `-- name: GetTemplateByID :one
|
const getTemplateByID = `-- name: GetTemplateByID :one
|
||||||
SELECT
|
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, use_classic_parameter_flow, created_by_avatar_url, created_by_username, created_by_name, 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, use_classic_parameter_flow, cors_behavior, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon
|
||||||
FROM
|
FROM
|
||||||
template_with_names
|
template_with_names
|
||||||
WHERE
|
WHERE
|
||||||
@@ -11810,6 +11810,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
|
|||||||
&i.ActivityBump,
|
&i.ActivityBump,
|
||||||
&i.MaxPortSharingLevel,
|
&i.MaxPortSharingLevel,
|
||||||
&i.UseClassicParameterFlow,
|
&i.UseClassicParameterFlow,
|
||||||
|
&i.CorsBehavior,
|
||||||
&i.CreatedByAvatarURL,
|
&i.CreatedByAvatarURL,
|
||||||
&i.CreatedByUsername,
|
&i.CreatedByUsername,
|
||||||
&i.CreatedByName,
|
&i.CreatedByName,
|
||||||
@@ -11822,7 +11823,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
|
|||||||
|
|
||||||
const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one
|
const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one
|
||||||
SELECT
|
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, use_classic_parameter_flow, created_by_avatar_url, created_by_username, created_by_name, 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, use_classic_parameter_flow, cors_behavior, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon
|
||||||
FROM
|
FROM
|
||||||
template_with_names AS templates
|
template_with_names AS templates
|
||||||
WHERE
|
WHERE
|
||||||
@@ -11872,6 +11873,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
|
|||||||
&i.ActivityBump,
|
&i.ActivityBump,
|
||||||
&i.MaxPortSharingLevel,
|
&i.MaxPortSharingLevel,
|
||||||
&i.UseClassicParameterFlow,
|
&i.UseClassicParameterFlow,
|
||||||
|
&i.CorsBehavior,
|
||||||
&i.CreatedByAvatarURL,
|
&i.CreatedByAvatarURL,
|
||||||
&i.CreatedByUsername,
|
&i.CreatedByUsername,
|
||||||
&i.CreatedByName,
|
&i.CreatedByName,
|
||||||
@@ -11883,7 +11885,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getTemplates = `-- name: GetTemplates :many
|
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, use_classic_parameter_flow, created_by_avatar_url, created_by_username, created_by_name, 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, use_classic_parameter_flow, cors_behavior, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon FROM template_with_names AS templates
|
||||||
ORDER BY (name, id) ASC
|
ORDER BY (name, id) ASC
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -11926,6 +11928,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
|
|||||||
&i.ActivityBump,
|
&i.ActivityBump,
|
||||||
&i.MaxPortSharingLevel,
|
&i.MaxPortSharingLevel,
|
||||||
&i.UseClassicParameterFlow,
|
&i.UseClassicParameterFlow,
|
||||||
|
&i.CorsBehavior,
|
||||||
&i.CreatedByAvatarURL,
|
&i.CreatedByAvatarURL,
|
||||||
&i.CreatedByUsername,
|
&i.CreatedByUsername,
|
||||||
&i.CreatedByName,
|
&i.CreatedByName,
|
||||||
@@ -11948,7 +11951,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
|
|||||||
|
|
||||||
const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many
|
const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many
|
||||||
SELECT
|
SELECT
|
||||||
t.id, t.created_at, t.updated_at, t.organization_id, t.deleted, t.name, t.provisioner, t.active_version_id, t.description, t.default_ttl, t.created_by, t.icon, t.user_acl, t.group_acl, t.display_name, t.allow_user_cancel_workspace_jobs, t.allow_user_autostart, t.allow_user_autostop, t.failure_ttl, t.time_til_dormant, t.time_til_dormant_autodelete, t.autostop_requirement_days_of_week, t.autostop_requirement_weeks, t.autostart_block_days_of_week, t.require_active_version, t.deprecated, t.activity_bump, t.max_port_sharing_level, t.use_classic_parameter_flow, t.created_by_avatar_url, t.created_by_username, t.created_by_name, t.organization_name, t.organization_display_name, t.organization_icon
|
t.id, t.created_at, t.updated_at, t.organization_id, t.deleted, t.name, t.provisioner, t.active_version_id, t.description, t.default_ttl, t.created_by, t.icon, t.user_acl, t.group_acl, t.display_name, t.allow_user_cancel_workspace_jobs, t.allow_user_autostart, t.allow_user_autostop, t.failure_ttl, t.time_til_dormant, t.time_til_dormant_autodelete, t.autostop_requirement_days_of_week, t.autostop_requirement_weeks, t.autostart_block_days_of_week, t.require_active_version, t.deprecated, t.activity_bump, t.max_port_sharing_level, t.use_classic_parameter_flow, t.cors_behavior, t.created_by_avatar_url, t.created_by_username, t.created_by_name, t.organization_name, t.organization_display_name, t.organization_icon
|
||||||
FROM
|
FROM
|
||||||
template_with_names AS t
|
template_with_names AS t
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
@@ -12059,6 +12062,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate
|
|||||||
&i.ActivityBump,
|
&i.ActivityBump,
|
||||||
&i.MaxPortSharingLevel,
|
&i.MaxPortSharingLevel,
|
||||||
&i.UseClassicParameterFlow,
|
&i.UseClassicParameterFlow,
|
||||||
|
&i.CorsBehavior,
|
||||||
&i.CreatedByAvatarURL,
|
&i.CreatedByAvatarURL,
|
||||||
&i.CreatedByUsername,
|
&i.CreatedByUsername,
|
||||||
&i.CreatedByName,
|
&i.CreatedByName,
|
||||||
@@ -12097,10 +12101,11 @@ INSERT INTO
|
|||||||
display_name,
|
display_name,
|
||||||
allow_user_cancel_workspace_jobs,
|
allow_user_cancel_workspace_jobs,
|
||||||
max_port_sharing_level,
|
max_port_sharing_level,
|
||||||
use_classic_parameter_flow
|
use_classic_parameter_flow,
|
||||||
|
cors_behavior
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
||||||
`
|
`
|
||||||
|
|
||||||
type InsertTemplateParams struct {
|
type InsertTemplateParams struct {
|
||||||
@@ -12120,6 +12125,7 @@ type InsertTemplateParams struct {
|
|||||||
AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"`
|
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"`
|
MaxPortSharingLevel AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"`
|
||||||
UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"`
|
UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"`
|
||||||
|
CorsBehavior CorsBehavior `db:"cors_behavior" json:"cors_behavior"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParams) error {
|
func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParams) error {
|
||||||
@@ -12140,6 +12146,7 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam
|
|||||||
arg.AllowUserCancelWorkspaceJobs,
|
arg.AllowUserCancelWorkspaceJobs,
|
||||||
arg.MaxPortSharingLevel,
|
arg.MaxPortSharingLevel,
|
||||||
arg.UseClassicParameterFlow,
|
arg.UseClassicParameterFlow,
|
||||||
|
arg.CorsBehavior,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -12240,7 +12247,8 @@ SET
|
|||||||
allow_user_cancel_workspace_jobs = $7,
|
allow_user_cancel_workspace_jobs = $7,
|
||||||
group_acl = $8,
|
group_acl = $8,
|
||||||
max_port_sharing_level = $9,
|
max_port_sharing_level = $9,
|
||||||
use_classic_parameter_flow = $10
|
use_classic_parameter_flow = $10,
|
||||||
|
cors_behavior = $11
|
||||||
WHERE
|
WHERE
|
||||||
id = $1
|
id = $1
|
||||||
`
|
`
|
||||||
@@ -12256,6 +12264,7 @@ type UpdateTemplateMetaByIDParams struct {
|
|||||||
GroupACL TemplateACL `db:"group_acl" json:"group_acl"`
|
GroupACL TemplateACL `db:"group_acl" json:"group_acl"`
|
||||||
MaxPortSharingLevel AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"`
|
MaxPortSharingLevel AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"`
|
||||||
UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"`
|
UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"`
|
||||||
|
CorsBehavior CorsBehavior `db:"cors_behavior" json:"cors_behavior"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error {
|
func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error {
|
||||||
@@ -12270,6 +12279,7 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl
|
|||||||
arg.GroupACL,
|
arg.GroupACL,
|
||||||
arg.MaxPortSharingLevel,
|
arg.MaxPortSharingLevel,
|
||||||
arg.UseClassicParameterFlow,
|
arg.UseClassicParameterFlow,
|
||||||
|
arg.CorsBehavior,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -19911,7 +19921,7 @@ LEFT JOIN LATERAL (
|
|||||||
) latest_build ON TRUE
|
) latest_build ON TRUE
|
||||||
LEFT JOIN LATERAL (
|
LEFT JOIN LATERAL (
|
||||||
SELECT
|
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, use_classic_parameter_flow
|
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, use_classic_parameter_flow, cors_behavior
|
||||||
FROM
|
FROM
|
||||||
templates
|
templates
|
||||||
WHERE
|
WHERE
|
||||||
|
|||||||
@@ -99,10 +99,11 @@ INSERT INTO
|
|||||||
display_name,
|
display_name,
|
||||||
allow_user_cancel_workspace_jobs,
|
allow_user_cancel_workspace_jobs,
|
||||||
max_port_sharing_level,
|
max_port_sharing_level,
|
||||||
use_classic_parameter_flow
|
use_classic_parameter_flow,
|
||||||
|
cors_behavior
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16);
|
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17);
|
||||||
|
|
||||||
-- name: UpdateTemplateActiveVersionByID :exec
|
-- name: UpdateTemplateActiveVersionByID :exec
|
||||||
UPDATE
|
UPDATE
|
||||||
@@ -134,7 +135,8 @@ SET
|
|||||||
allow_user_cancel_workspace_jobs = $7,
|
allow_user_cancel_workspace_jobs = $7,
|
||||||
group_acl = $8,
|
group_acl = $8,
|
||||||
max_port_sharing_level = $9,
|
max_port_sharing_level = $9,
|
||||||
use_classic_parameter_flow = $10
|
use_classic_parameter_flow = $10,
|
||||||
|
cors_behavior = $11
|
||||||
WHERE
|
WHERE
|
||||||
id = $1
|
id = $1
|
||||||
;
|
;
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ sql:
|
|||||||
has_ai_task: HasAITask
|
has_ai_task: HasAITask
|
||||||
ai_task_sidebar_app_id: AITaskSidebarAppID
|
ai_task_sidebar_app_id: AITaskSidebarAppID
|
||||||
latest_build_has_ai_task: LatestBuildHasAITask
|
latest_build_has_ai_task: LatestBuildHasAITask
|
||||||
|
cors_behavior: CorsBehavior
|
||||||
rules:
|
rules:
|
||||||
- name: do-not-use-public-schema-in-queries
|
- name: do-not-use-public-schema-in-queries
|
||||||
message: "do not use public schema in queries"
|
message: "do not use public schema in queries"
|
||||||
|
|||||||
@@ -744,6 +744,7 @@ func insertTemplates(t *testing.T, db database.Store, u database.User, org datab
|
|||||||
MaxPortSharingLevel: database.AppSharingLevelAuthenticated,
|
MaxPortSharingLevel: database.AppSharingLevelAuthenticated,
|
||||||
CreatedBy: u.ID,
|
CreatedBy: u.ID,
|
||||||
OrganizationID: org.ID,
|
OrganizationID: org.ID,
|
||||||
|
CorsBehavior: database.CorsBehaviorSimple,
|
||||||
}))
|
}))
|
||||||
pj := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{})
|
pj := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{})
|
||||||
|
|
||||||
@@ -763,6 +764,7 @@ func insertTemplates(t *testing.T, db database.Store, u database.User, org datab
|
|||||||
MaxPortSharingLevel: database.AppSharingLevelAuthenticated,
|
MaxPortSharingLevel: database.AppSharingLevelAuthenticated,
|
||||||
CreatedBy: u.ID,
|
CreatedBy: u.ID,
|
||||||
OrganizationID: org.ID,
|
OrganizationID: org.ID,
|
||||||
|
CorsBehavior: database.CorsBehaviorSimple,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
require.NoError(t, db.InsertTemplateVersion(context.Background(), database.InsertTemplateVersionParams{
|
require.NoError(t, db.InsertTemplateVersion(context.Background(), database.InsertTemplateVersionParams{
|
||||||
|
|||||||
+35
-1
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
@@ -29,6 +30,7 @@ import (
|
|||||||
"github.com/coder/coder/v2/coderd/searchquery"
|
"github.com/coder/coder/v2/coderd/searchquery"
|
||||||
"github.com/coder/coder/v2/coderd/telemetry"
|
"github.com/coder/coder/v2/coderd/telemetry"
|
||||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||||
|
"github.com/coder/coder/v2/coderd/util/slice"
|
||||||
"github.com/coder/coder/v2/coderd/workspacestats"
|
"github.com/coder/coder/v2/coderd/workspacestats"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
"github.com/coder/coder/v2/examples"
|
"github.com/coder/coder/v2/examples"
|
||||||
@@ -322,6 +324,7 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
|
|||||||
autostopRequirementDaysOfWeekParsed uint8
|
autostopRequirementDaysOfWeekParsed uint8
|
||||||
autostartRequirementDaysOfWeekParsed uint8
|
autostartRequirementDaysOfWeekParsed uint8
|
||||||
maxPortShareLevel = database.AppSharingLevelOwner // default
|
maxPortShareLevel = database.AppSharingLevelOwner // default
|
||||||
|
corsBehavior = database.CorsBehaviorSimple // default
|
||||||
)
|
)
|
||||||
if defaultTTL < 0 {
|
if defaultTTL < 0 {
|
||||||
validErrs = append(validErrs, codersdk.ValidationError{Field: "default_ttl_ms", Detail: "Must be a positive integer."})
|
validErrs = append(validErrs, codersdk.ValidationError{Field: "default_ttl_ms", Detail: "Must be a positive integer."})
|
||||||
@@ -351,6 +354,20 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default the CORS behavior here to Simple so we don't break all existing templates.
|
||||||
|
val := database.CorsBehaviorSimple
|
||||||
|
if createTemplate.CORSBehavior != nil {
|
||||||
|
val = database.CorsBehavior(*createTemplate.CORSBehavior)
|
||||||
|
}
|
||||||
|
if !val.Valid() {
|
||||||
|
validErrs = append(validErrs, codersdk.ValidationError{
|
||||||
|
Field: "cors_behavior",
|
||||||
|
Detail: fmt.Sprintf("Invalid CORS behavior %q. Must be one of [%s]", *createTemplate.CORSBehavior, strings.Join(slice.ToStrings(database.AllCorsBehaviorValues()), ", ")),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
corsBehavior = val
|
||||||
|
}
|
||||||
|
|
||||||
if autostopRequirementWeeks < 0 {
|
if autostopRequirementWeeks < 0 {
|
||||||
validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.weeks", Detail: "Must be a positive integer."})
|
validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.weeks", Detail: "Must be a positive integer."})
|
||||||
}
|
}
|
||||||
@@ -409,6 +426,7 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
|
|||||||
AllowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs,
|
AllowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs,
|
||||||
MaxPortSharingLevel: maxPortShareLevel,
|
MaxPortSharingLevel: maxPortShareLevel,
|
||||||
UseClassicParameterFlow: useClassicParameterFlow,
|
UseClassicParameterFlow: useClassicParameterFlow,
|
||||||
|
CorsBehavior: corsBehavior,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("insert template: %s", err)
|
return xerrors.Errorf("insert template: %s", err)
|
||||||
@@ -725,6 +743,19 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
corsBehavior := template.CorsBehavior
|
||||||
|
if req.CORSBehavior != nil && *req.CORSBehavior != "" {
|
||||||
|
val := database.CorsBehavior(*req.CORSBehavior)
|
||||||
|
if !val.Valid() {
|
||||||
|
validErrs = append(validErrs, codersdk.ValidationError{
|
||||||
|
Field: "cors_behavior",
|
||||||
|
Detail: fmt.Sprintf("Invalid CORS behavior %q. Must be one of [%s]", *req.CORSBehavior, strings.Join(slice.ToStrings(database.AllCorsBehaviorValues()), ", ")),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
corsBehavior = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(validErrs) > 0 {
|
if len(validErrs) > 0 {
|
||||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
Message: "Invalid request to update template metadata!",
|
Message: "Invalid request to update template metadata!",
|
||||||
@@ -759,7 +790,8 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
|||||||
req.RequireActiveVersion == template.RequireActiveVersion &&
|
req.RequireActiveVersion == template.RequireActiveVersion &&
|
||||||
(deprecationMessage == template.Deprecated) &&
|
(deprecationMessage == template.Deprecated) &&
|
||||||
(classicTemplateFlow == template.UseClassicParameterFlow) &&
|
(classicTemplateFlow == template.UseClassicParameterFlow) &&
|
||||||
maxPortShareLevel == template.MaxPortSharingLevel {
|
maxPortShareLevel == template.MaxPortSharingLevel &&
|
||||||
|
corsBehavior == template.CorsBehavior {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -801,6 +833,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
|||||||
GroupACL: groupACL,
|
GroupACL: groupACL,
|
||||||
MaxPortSharingLevel: maxPortShareLevel,
|
MaxPortSharingLevel: maxPortShareLevel,
|
||||||
UseClassicParameterFlow: classicTemplateFlow,
|
UseClassicParameterFlow: classicTemplateFlow,
|
||||||
|
CorsBehavior: corsBehavior,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("update template metadata: %w", err)
|
return xerrors.Errorf("update template metadata: %w", err)
|
||||||
@@ -1084,6 +1117,7 @@ func (api *API) convertTemplate(
|
|||||||
DeprecationMessage: templateAccessControl.Deprecated,
|
DeprecationMessage: templateAccessControl.Deprecated,
|
||||||
MaxPortShareLevel: maxPortShareLevel,
|
MaxPortShareLevel: maxPortShareLevel,
|
||||||
UseClassicParameterFlow: template.UseClassicParameterFlow,
|
UseClassicParameterFlow: template.UseClassicParameterFlow,
|
||||||
|
CORSBehavior: codersdk.CORSBehavior(template.CorsBehavior),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -472,6 +472,409 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("WorkspaceApplicationCORS", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const external = "https://example.com"
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
ownSubdomain := func(details *Details, app App) string {
|
||||||
|
url := details.SubdomainAppURL(app)
|
||||||
|
return url.Scheme + "://" + url.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
externalOrigin := func(*Details, App) string {
|
||||||
|
return external
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
app func(details *Details) App
|
||||||
|
client func(t *testing.T, appDetails *Details) *codersdk.Client
|
||||||
|
behavior codersdk.CORSBehavior
|
||||||
|
httpMethod string
|
||||||
|
origin func(details *Details, app App) string
|
||||||
|
expectedStatusCode int
|
||||||
|
checkRequestHeaders func(t *testing.T, origin string, req http.Header)
|
||||||
|
checkResponseHeaders func(t *testing.T, origin string, resp http.Header)
|
||||||
|
}{
|
||||||
|
// Public
|
||||||
|
{ // fails
|
||||||
|
// The default behavior is to accept preflight requests from the request origin if it matches the app's own subdomain.
|
||||||
|
name: "Default/Public/Preflight/Subdomain",
|
||||||
|
app: func(details *Details) App { return details.Apps.PublicCORSDefault },
|
||||||
|
behavior: codersdk.CORSBehaviorSimple,
|
||||||
|
client: unauthenticatedClient,
|
||||||
|
httpMethod: http.MethodOptions,
|
||||||
|
origin: ownSubdomain,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
|
||||||
|
assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
|
||||||
|
assert.Contains(t, resp.Get("Access-Control-Allow-Methods"), http.MethodGet)
|
||||||
|
assert.Equal(t, "true", resp.Get("Access-Control-Allow-Credentials"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ // passes
|
||||||
|
// The default behavior is to reject preflight requests from origins other than the app's own subdomain.
|
||||||
|
name: "Default/Public/Preflight/External",
|
||||||
|
app: func(details *Details) App { return details.Apps.PublicCORSDefault },
|
||||||
|
behavior: codersdk.CORSBehaviorSimple,
|
||||||
|
client: unauthenticatedClient,
|
||||||
|
httpMethod: http.MethodOptions,
|
||||||
|
origin: externalOrigin,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
|
||||||
|
// We don't add a valid Allow-Origin header for requests we won't proxy.
|
||||||
|
assert.Empty(t, resp.Get("Access-Control-Allow-Origin"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ // fails
|
||||||
|
// A request without an Origin header would be rejected by an actual browser since it lacks CORS headers.
|
||||||
|
name: "Default/Public/GET/NoOrigin",
|
||||||
|
app: func(details *Details) App { return details.Apps.PublicCORSDefault },
|
||||||
|
behavior: codersdk.CORSBehaviorSimple,
|
||||||
|
client: unauthenticatedClient,
|
||||||
|
origin: func(*Details, App) string { return "" },
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
|
||||||
|
assert.Empty(t, resp.Get("Access-Control-Allow-Origin"))
|
||||||
|
assert.Empty(t, resp.Get("Access-Control-Allow-Headers"))
|
||||||
|
assert.Empty(t, resp.Get("Access-Control-Allow-Credentials"))
|
||||||
|
// Added by the app handler.
|
||||||
|
assert.Equal(t, "simple", resp.Get("X-CORS-Handler"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ // fails
|
||||||
|
// The passthru behavior will pass through the request headers to the upstream app.
|
||||||
|
name: "Passthru/Public/Preflight/Subdomain",
|
||||||
|
app: func(details *Details) App { return details.Apps.PublicCORSPassthru },
|
||||||
|
behavior: codersdk.CORSBehaviorPassthru,
|
||||||
|
client: unauthenticatedClient,
|
||||||
|
origin: ownSubdomain,
|
||||||
|
httpMethod: http.MethodOptions,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
checkRequestHeaders: func(t *testing.T, origin string, req http.Header) {
|
||||||
|
assert.Equal(t, origin, req.Get("Origin"))
|
||||||
|
assert.Equal(t, "GET", req.Get("Access-Control-Request-Method"))
|
||||||
|
},
|
||||||
|
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
|
||||||
|
assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
|
||||||
|
assert.Equal(t, http.MethodGet, resp.Get("Access-Control-Allow-Methods"))
|
||||||
|
// Added by the app handler.
|
||||||
|
assert.Equal(t, "passthru", resp.Get("X-CORS-Handler"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ // fails
|
||||||
|
// Identical to the previous test, but the origin is different.
|
||||||
|
name: "Passthru/Public/PreflightOther",
|
||||||
|
app: func(details *Details) App { return details.Apps.PublicCORSPassthru },
|
||||||
|
behavior: codersdk.CORSBehaviorPassthru,
|
||||||
|
client: unauthenticatedClient,
|
||||||
|
origin: externalOrigin,
|
||||||
|
httpMethod: http.MethodOptions,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
checkRequestHeaders: func(t *testing.T, origin string, req http.Header) {
|
||||||
|
assert.Equal(t, origin, req.Get("Origin"))
|
||||||
|
assert.Equal(t, "GET", req.Get("Access-Control-Request-Method"))
|
||||||
|
assert.Equal(t, "X-Got-Host", req.Get("Access-Control-Request-Headers"))
|
||||||
|
},
|
||||||
|
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
|
||||||
|
assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
|
||||||
|
assert.Equal(t, http.MethodGet, resp.Get("Access-Control-Allow-Methods"))
|
||||||
|
// Added by the app handler.
|
||||||
|
assert.Equal(t, "passthru", resp.Get("X-CORS-Handler"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// A request without an Origin header would be rejected by an actual browser since it lacks CORS headers.
|
||||||
|
name: "Passthru/Public/GET/NoOrigin",
|
||||||
|
app: func(details *Details) App { return details.Apps.PublicCORSPassthru },
|
||||||
|
behavior: codersdk.CORSBehaviorPassthru,
|
||||||
|
client: unauthenticatedClient,
|
||||||
|
origin: func(*Details, App) string { return "" },
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
|
||||||
|
assert.Empty(t, resp.Get("Access-Control-Allow-Origin"))
|
||||||
|
assert.Empty(t, resp.Get("Access-Control-Allow-Headers"))
|
||||||
|
assert.Empty(t, resp.Get("Access-Control-Allow-Credentials"))
|
||||||
|
// Added by the app handler.
|
||||||
|
assert.Equal(t, "passthru", resp.Get("X-CORS-Handler"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Authenticated
|
||||||
|
{
|
||||||
|
// Same behavior as Default/Public/Preflight/Subdomain.
|
||||||
|
name: "Default/Authenticated/Preflight/Subdomain",
|
||||||
|
app: func(details *Details) App { return details.Apps.AuthenticatedCORSDefault },
|
||||||
|
behavior: codersdk.CORSBehaviorSimple,
|
||||||
|
client: authenticatedClient,
|
||||||
|
origin: ownSubdomain,
|
||||||
|
httpMethod: http.MethodOptions,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
|
||||||
|
assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
|
||||||
|
assert.Contains(t, resp.Get("Access-Control-Allow-Methods"), http.MethodGet)
|
||||||
|
assert.Equal(t, "true", resp.Get("Access-Control-Allow-Credentials"))
|
||||||
|
assert.Equal(t, "X-Got-Host", resp.Get("Access-Control-Allow-Headers"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Same behavior as Default/Public/Preflight/External.
|
||||||
|
name: "Default/Authenticated/Preflight/External",
|
||||||
|
app: func(details *Details) App { return details.Apps.AuthenticatedCORSDefault },
|
||||||
|
behavior: codersdk.CORSBehaviorSimple,
|
||||||
|
client: authenticatedClient,
|
||||||
|
origin: externalOrigin,
|
||||||
|
httpMethod: http.MethodOptions,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
|
||||||
|
assert.Empty(t, resp.Get("Access-Control-Allow-Origin"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// An authenticated request to the app is allowed from its own subdomain.
|
||||||
|
name: "Default/Authenticated/GET/Subdomain",
|
||||||
|
app: func(details *Details) App { return details.Apps.AuthenticatedCORSDefault },
|
||||||
|
behavior: codersdk.CORSBehaviorSimple,
|
||||||
|
client: authenticatedClient,
|
||||||
|
origin: ownSubdomain,
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
|
||||||
|
assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
|
||||||
|
assert.Equal(t, "true", resp.Get("Access-Control-Allow-Credentials"))
|
||||||
|
// Added by the app handler.
|
||||||
|
assert.Equal(t, "simple", resp.Get("X-CORS-Handler"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// An authenticated request to the app is allowed from an external origin.
|
||||||
|
// The origin doesn't match the app's own subdomain, so the CORS headers are not added.
|
||||||
|
name: "Default/Authenticated/GET/External",
|
||||||
|
app: func(details *Details) App { return details.Apps.AuthenticatedCORSDefault },
|
||||||
|
behavior: codersdk.CORSBehaviorSimple,
|
||||||
|
client: authenticatedClient,
|
||||||
|
origin: externalOrigin,
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
|
||||||
|
assert.Empty(t, resp.Get("Access-Control-Allow-Origin"))
|
||||||
|
assert.Empty(t, resp.Get("Access-Control-Allow-Headers"))
|
||||||
|
assert.Empty(t, resp.Get("Access-Control-Allow-Credentials"))
|
||||||
|
// Added by the app handler.
|
||||||
|
assert.Equal(t, "simple", resp.Get("X-CORS-Handler"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// The request is rejected because the client is unauthenticated.
|
||||||
|
name: "Passthru/Unauthenticated/Preflight/Subdomain",
|
||||||
|
app: func(details *Details) App { return details.Apps.AuthenticatedCORSPassthru },
|
||||||
|
behavior: codersdk.CORSBehaviorPassthru,
|
||||||
|
client: unauthenticatedClient,
|
||||||
|
origin: ownSubdomain,
|
||||||
|
httpMethod: http.MethodOptions,
|
||||||
|
expectedStatusCode: http.StatusSeeOther,
|
||||||
|
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
|
||||||
|
assert.NotEmpty(t, resp.Get("Location"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Same behavior as the above test, but the origin is different.
|
||||||
|
name: "Passthru/Unauthenticated/Preflight/External",
|
||||||
|
app: func(details *Details) App { return details.Apps.AuthenticatedCORSPassthru },
|
||||||
|
behavior: codersdk.CORSBehaviorPassthru,
|
||||||
|
client: unauthenticatedClient,
|
||||||
|
origin: externalOrigin,
|
||||||
|
httpMethod: http.MethodOptions,
|
||||||
|
expectedStatusCode: http.StatusSeeOther,
|
||||||
|
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
|
||||||
|
assert.NotEmpty(t, resp.Get("Location"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// The request is rejected because the client is unauthenticated.
|
||||||
|
name: "Passthru/Unauthenticated/GET/Subdomain",
|
||||||
|
app: func(details *Details) App { return details.Apps.AuthenticatedCORSPassthru },
|
||||||
|
behavior: codersdk.CORSBehaviorPassthru,
|
||||||
|
client: unauthenticatedClient,
|
||||||
|
origin: ownSubdomain,
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
expectedStatusCode: http.StatusSeeOther,
|
||||||
|
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
|
||||||
|
assert.NotEmpty(t, resp.Get("Location"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Same behavior as the above test, but the origin is different.
|
||||||
|
name: "Passthru/Unauthenticated/GET/External",
|
||||||
|
app: func(details *Details) App { return details.Apps.AuthenticatedCORSPassthru },
|
||||||
|
behavior: codersdk.CORSBehaviorPassthru,
|
||||||
|
client: unauthenticatedClient,
|
||||||
|
origin: externalOrigin,
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
expectedStatusCode: http.StatusSeeOther,
|
||||||
|
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
|
||||||
|
assert.NotEmpty(t, resp.Get("Location"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// The request is allowed because the client is authenticated.
|
||||||
|
name: "Passthru/Authenticated/Preflight/Subdomain",
|
||||||
|
app: func(details *Details) App { return details.Apps.AuthenticatedCORSPassthru },
|
||||||
|
behavior: codersdk.CORSBehaviorPassthru,
|
||||||
|
client: authenticatedClient,
|
||||||
|
origin: ownSubdomain,
|
||||||
|
httpMethod: http.MethodOptions,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
|
||||||
|
assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
|
||||||
|
assert.Equal(t, http.MethodGet, resp.Get("Access-Control-Allow-Methods"))
|
||||||
|
// Added by the app handler.
|
||||||
|
assert.Equal(t, "passthru", resp.Get("X-CORS-Handler"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Same behavior as the above test, but the origin is different.
|
||||||
|
name: "Passthru/Authenticated/Preflight/External",
|
||||||
|
app: func(details *Details) App { return details.Apps.AuthenticatedCORSPassthru },
|
||||||
|
behavior: codersdk.CORSBehaviorPassthru,
|
||||||
|
client: authenticatedClient,
|
||||||
|
origin: externalOrigin,
|
||||||
|
httpMethod: http.MethodOptions,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
|
||||||
|
assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
|
||||||
|
assert.Equal(t, http.MethodGet, resp.Get("Access-Control-Allow-Methods"))
|
||||||
|
// Added by the app handler.
|
||||||
|
assert.Equal(t, "passthru", resp.Get("X-CORS-Handler"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// The request is allowed because the client is authenticated.
|
||||||
|
name: "Passthru/Authenticated/GET/Subdomain",
|
||||||
|
app: func(details *Details) App { return details.Apps.AuthenticatedCORSPassthru },
|
||||||
|
behavior: codersdk.CORSBehaviorPassthru,
|
||||||
|
client: authenticatedClient,
|
||||||
|
origin: ownSubdomain,
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
|
||||||
|
assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
|
||||||
|
assert.Equal(t, http.MethodGet, resp.Get("Access-Control-Allow-Methods"))
|
||||||
|
// Added by the app handler.
|
||||||
|
assert.Equal(t, "passthru", resp.Get("X-CORS-Handler"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Same behavior as the above test, but the origin is different.
|
||||||
|
name: "Passthru/Authenticated/GET/External",
|
||||||
|
app: func(details *Details) App { return details.Apps.AuthenticatedCORSPassthru },
|
||||||
|
behavior: codersdk.CORSBehaviorPassthru,
|
||||||
|
client: authenticatedClient,
|
||||||
|
origin: externalOrigin,
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
|
||||||
|
assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
|
||||||
|
assert.Equal(t, http.MethodGet, resp.Get("Access-Control-Allow-Methods"))
|
||||||
|
// Added by the app handler.
|
||||||
|
assert.Equal(t, "passthru", resp.Get("X-CORS-Handler"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
|
|
||||||
|
var reqHeaders http.Header
|
||||||
|
// Setup an HTTP handler which is the "app"; this handler conditionally responds
|
||||||
|
// to requests based on the CORS behavior
|
||||||
|
appDetails := setupProxyTest(t, &DeploymentOptions{
|
||||||
|
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, err := r.Cookie(codersdk.SessionTokenCookie)
|
||||||
|
assert.ErrorIs(t, err, http.ErrNoCookie)
|
||||||
|
|
||||||
|
// Store the request headers for later assertions
|
||||||
|
reqHeaders = r.Header
|
||||||
|
|
||||||
|
switch tc.behavior {
|
||||||
|
case codersdk.CORSBehaviorPassthru:
|
||||||
|
w.Header().Set("X-CORS-Handler", "passthru")
|
||||||
|
|
||||||
|
// Only allow GET and OPTIONS requests
|
||||||
|
if r.Method != http.MethodGet && r.Method != http.MethodOptions {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the Origin header is present, add the CORS headers.
|
||||||
|
if origin := r.Header.Get("Origin"); origin != "" {
|
||||||
|
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", http.MethodGet)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
case codersdk.CORSBehaviorSimple:
|
||||||
|
w.Header().Set("X-CORS-Handler", "simple")
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update the template CORS behavior.
|
||||||
|
b := 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)
|
||||||
|
|
||||||
|
// Given: a client and a workspace app
|
||||||
|
client := tc.client(t, appDetails)
|
||||||
|
path := appDetails.SubdomainAppURL(tc.app(appDetails)).String()
|
||||||
|
origin := tc.origin(appDetails, tc.app(appDetails))
|
||||||
|
|
||||||
|
fmt.Println("method: ", tc.httpMethod)
|
||||||
|
// When: a preflight request is made to an app with a specified CORS behavior
|
||||||
|
resp, err := requestWithRetries(ctx, t, client, tc.httpMethod, path, nil, func(r *http.Request) {
|
||||||
|
// Mimic non-browser clients that don't send the Origin header.
|
||||||
|
if origin != "" {
|
||||||
|
r.Header.Set("Origin", origin)
|
||||||
|
}
|
||||||
|
r.Header.Set("Access-Control-Request-Method", "GET")
|
||||||
|
r.Header.Set("Access-Control-Request-Headers", "X-Got-Host")
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Then: the request & response must match expectations
|
||||||
|
assert.Equal(t, tc.expectedStatusCode, resp.StatusCode)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if tc.checkRequestHeaders != nil {
|
||||||
|
tc.checkRequestHeaders(t, origin, reqHeaders)
|
||||||
|
}
|
||||||
|
tc.checkResponseHeaders(t, origin, resp.Header)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("WorkspaceApplicationAuth", func(t *testing.T) {
|
t.Run("WorkspaceApplicationAuth", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -1340,6 +1743,153 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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 {
|
||||||
|
c := appDetails.AppClient(t) // <-- Use same server as others
|
||||||
|
c.SetSessionToken(appDetails.SDKClient.SessionToken()) // But with owner auth
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
shareLevel codersdk.WorkspaceAgentPortShareLevel
|
||||||
|
behavior codersdk.CORSBehavior
|
||||||
|
client func(t *testing.T, appDetails *Details) *codersdk.Client
|
||||||
|
expectedStatusCode int
|
||||||
|
expectedCORSHeaders bool
|
||||||
|
}{
|
||||||
|
// Public
|
||||||
|
{
|
||||||
|
name: "Default/Public",
|
||||||
|
shareLevel: codersdk.WorkspaceAgentPortShareLevelPublic,
|
||||||
|
behavior: codersdk.CORSBehaviorSimple,
|
||||||
|
expectedCORSHeaders: false,
|
||||||
|
client: unauthenticatedClient,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{ // fails
|
||||||
|
name: "Passthru/Public",
|
||||||
|
shareLevel: codersdk.WorkspaceAgentPortShareLevelPublic,
|
||||||
|
behavior: codersdk.CORSBehaviorPassthru,
|
||||||
|
expectedCORSHeaders: true,
|
||||||
|
client: unauthenticatedClient,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
// Authenticated
|
||||||
|
{
|
||||||
|
name: "Default/Authenticated",
|
||||||
|
shareLevel: codersdk.WorkspaceAgentPortShareLevelAuthenticated,
|
||||||
|
behavior: codersdk.CORSBehaviorSimple,
|
||||||
|
expectedCORSHeaders: false,
|
||||||
|
client: authenticatedClient,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Passthru/Authenticated",
|
||||||
|
shareLevel: codersdk.WorkspaceAgentPortShareLevelAuthenticated,
|
||||||
|
behavior: codersdk.CORSBehaviorPassthru,
|
||||||
|
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.CORSBehaviorPassthru,
|
||||||
|
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.CORSBehaviorSimple,
|
||||||
|
expectedCORSHeaders: false,
|
||||||
|
client: ownerClient,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{ // fails
|
||||||
|
name: "Passthru/Owner",
|
||||||
|
shareLevel: codersdk.WorkspaceAgentPortShareLevelAuthenticated, // Owner is not a valid share level for ports.
|
||||||
|
behavior: codersdk.CORSBehaviorPassthru,
|
||||||
|
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 := 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"), "allow origin did not match")
|
||||||
|
require.Equal(t, testHeaders.Get("Access-Control-Allow-Methods"), resp.Header.Get("Access-Control-Allow-Methods"), "allow methods did not match")
|
||||||
|
} 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) {
|
t.Run("AppSharing", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -1386,7 +1936,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
|
|||||||
forceURLTransport(t, client)
|
forceURLTransport(t, client)
|
||||||
|
|
||||||
// Create workspace.
|
// Create workspace.
|
||||||
port := appServer(t, nil, false)
|
port := appServer(t, nil, false, nil)
|
||||||
workspace, _ = createWorkspaceWithApps(t, client, user.OrganizationIDs[0], user, port, false)
|
workspace, _ = createWorkspaceWithApps(t, client, user.OrganizationIDs[0], user, port, false)
|
||||||
|
|
||||||
// Verify that the apps have the correct sharing levels set.
|
// Verify that the apps have the correct sharing levels set.
|
||||||
@@ -1397,10 +1947,14 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
|
|||||||
agnt = workspaceBuild.Resources[0].Agents[0]
|
agnt = workspaceBuild.Resources[0].Agents[0]
|
||||||
found := map[string]codersdk.WorkspaceAppSharingLevel{}
|
found := map[string]codersdk.WorkspaceAppSharingLevel{}
|
||||||
expected := map[string]codersdk.WorkspaceAppSharingLevel{
|
expected := map[string]codersdk.WorkspaceAppSharingLevel{
|
||||||
proxyTestAppNameFake: codersdk.WorkspaceAppSharingLevelOwner,
|
proxyTestAppNameFake: codersdk.WorkspaceAppSharingLevelOwner,
|
||||||
proxyTestAppNameOwner: codersdk.WorkspaceAppSharingLevelOwner,
|
proxyTestAppNameOwner: codersdk.WorkspaceAppSharingLevelOwner,
|
||||||
proxyTestAppNameAuthenticated: codersdk.WorkspaceAppSharingLevelAuthenticated,
|
proxyTestAppNameAuthenticated: codersdk.WorkspaceAppSharingLevelAuthenticated,
|
||||||
proxyTestAppNamePublic: codersdk.WorkspaceAppSharingLevelPublic,
|
proxyTestAppNamePublic: codersdk.WorkspaceAppSharingLevelPublic,
|
||||||
|
proxyTestAppNameAuthenticatedCORSPassthru: codersdk.WorkspaceAppSharingLevelAuthenticated,
|
||||||
|
proxyTestAppNamePublicCORSPassthru: codersdk.WorkspaceAppSharingLevelPublic,
|
||||||
|
proxyTestAppNameAuthenticatedCORSDefault: codersdk.WorkspaceAppSharingLevelAuthenticated,
|
||||||
|
proxyTestAppNamePublicCORSDefault: codersdk.WorkspaceAppSharingLevelPublic,
|
||||||
}
|
}
|
||||||
for _, app := range agnt.Apps {
|
for _, app := range agnt.Apps {
|
||||||
found[app.DisplayName] = app.SharingLevel
|
found[app.DisplayName] = app.SharingLevel
|
||||||
|
|||||||
@@ -36,8 +36,13 @@ const (
|
|||||||
proxyTestAppNameOwner = "test-app-owner"
|
proxyTestAppNameOwner = "test-app-owner"
|
||||||
proxyTestAppNameAuthenticated = "test-app-authenticated"
|
proxyTestAppNameAuthenticated = "test-app-authenticated"
|
||||||
proxyTestAppNamePublic = "test-app-public"
|
proxyTestAppNamePublic = "test-app-public"
|
||||||
proxyTestAppQuery = "query=true"
|
// nolint:gosec // Not a secret
|
||||||
proxyTestAppBody = "hello world from apps test"
|
proxyTestAppNameAuthenticatedCORSPassthru = "test-app-authenticated-cors-passthru"
|
||||||
|
proxyTestAppNamePublicCORSPassthru = "test-app-public-cors-passthru"
|
||||||
|
proxyTestAppNameAuthenticatedCORSDefault = "test-app-authenticated-cors-default"
|
||||||
|
proxyTestAppNamePublicCORSDefault = "test-app-public-cors-default"
|
||||||
|
proxyTestAppQuery = "query=true"
|
||||||
|
proxyTestAppBody = "hello world from apps test"
|
||||||
|
|
||||||
proxyTestSubdomainRaw = "*.test.coder.com"
|
proxyTestSubdomainRaw = "*.test.coder.com"
|
||||||
proxyTestSubdomain = "test.coder.com"
|
proxyTestSubdomain = "test.coder.com"
|
||||||
@@ -60,6 +65,7 @@ type DeploymentOptions struct {
|
|||||||
noWorkspace bool
|
noWorkspace bool
|
||||||
port uint16
|
port uint16
|
||||||
headers http.Header
|
headers http.Header
|
||||||
|
handler http.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deployment is a license-agnostic deployment with all the fields that apps
|
// Deployment is a license-agnostic deployment with all the fields that apps
|
||||||
@@ -93,6 +99,9 @@ type App struct {
|
|||||||
// Prefix should have ---.
|
// Prefix should have ---.
|
||||||
Prefix string
|
Prefix string
|
||||||
Query string
|
Query string
|
||||||
|
|
||||||
|
// Control the behavior of CORS handling.
|
||||||
|
CORSBehavior codersdk.CORSBehavior
|
||||||
}
|
}
|
||||||
|
|
||||||
// Details are the full test details returned from setupProxyTestWithFactory.
|
// Details are the full test details returned from setupProxyTestWithFactory.
|
||||||
@@ -109,12 +118,16 @@ type Details struct {
|
|||||||
AppPort uint16
|
AppPort uint16
|
||||||
|
|
||||||
Apps struct {
|
Apps struct {
|
||||||
Fake App
|
Fake App
|
||||||
Owner App
|
Owner App
|
||||||
Authenticated App
|
Authenticated App
|
||||||
Public App
|
Public App
|
||||||
Port App
|
Port App
|
||||||
PortHTTPS App
|
PortHTTPS App
|
||||||
|
PublicCORSPassthru App
|
||||||
|
AuthenticatedCORSPassthru App
|
||||||
|
PublicCORSDefault App
|
||||||
|
AuthenticatedCORSDefault App
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +214,7 @@ func setupProxyTestWithFactory(t *testing.T, factory DeploymentFactory, opts *De
|
|||||||
}
|
}
|
||||||
|
|
||||||
if opts.port == 0 {
|
if opts.port == 0 {
|
||||||
opts.port = appServer(t, opts.headers, opts.ServeHTTPS)
|
opts.port = appServer(t, opts.headers, opts.ServeHTTPS, opts.handler)
|
||||||
}
|
}
|
||||||
workspace, agnt := createWorkspaceWithApps(t, deployment.SDKClient, deployment.FirstUser.OrganizationID, me, opts.port, opts.ServeHTTPS)
|
workspace, agnt := createWorkspaceWithApps(t, deployment.SDKClient, deployment.FirstUser.OrganizationID, me, opts.port, opts.ServeHTTPS)
|
||||||
|
|
||||||
@@ -252,30 +265,64 @@ func setupProxyTestWithFactory(t *testing.T, factory DeploymentFactory, opts *De
|
|||||||
AgentName: agnt.Name,
|
AgentName: agnt.Name,
|
||||||
AppSlugOrPort: strconv.Itoa(int(opts.port)) + "s",
|
AppSlugOrPort: strconv.Itoa(int(opts.port)) + "s",
|
||||||
}
|
}
|
||||||
|
details.Apps.PublicCORSPassthru = App{
|
||||||
|
Username: me.Username,
|
||||||
|
WorkspaceName: workspace.Name,
|
||||||
|
AgentName: agnt.Name,
|
||||||
|
AppSlugOrPort: proxyTestAppNamePublicCORSPassthru,
|
||||||
|
CORSBehavior: codersdk.CORSBehaviorPassthru,
|
||||||
|
Query: proxyTestAppQuery,
|
||||||
|
}
|
||||||
|
details.Apps.AuthenticatedCORSPassthru = App{
|
||||||
|
Username: me.Username,
|
||||||
|
WorkspaceName: workspace.Name,
|
||||||
|
AgentName: agnt.Name,
|
||||||
|
AppSlugOrPort: proxyTestAppNameAuthenticatedCORSPassthru,
|
||||||
|
CORSBehavior: codersdk.CORSBehaviorPassthru,
|
||||||
|
Query: proxyTestAppQuery,
|
||||||
|
}
|
||||||
|
details.Apps.PublicCORSDefault = App{
|
||||||
|
Username: me.Username,
|
||||||
|
WorkspaceName: workspace.Name,
|
||||||
|
AgentName: agnt.Name,
|
||||||
|
AppSlugOrPort: proxyTestAppNamePublicCORSDefault,
|
||||||
|
Query: proxyTestAppQuery,
|
||||||
|
}
|
||||||
|
details.Apps.AuthenticatedCORSDefault = App{
|
||||||
|
Username: me.Username,
|
||||||
|
WorkspaceName: workspace.Name,
|
||||||
|
AgentName: agnt.Name,
|
||||||
|
AppSlugOrPort: proxyTestAppNameAuthenticatedCORSDefault,
|
||||||
|
Query: proxyTestAppQuery,
|
||||||
|
}
|
||||||
|
|
||||||
return details
|
return details
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:revive
|
//nolint:revive
|
||||||
func appServer(t *testing.T, headers http.Header, isHTTPS bool) uint16 {
|
func appServer(t *testing.T, headers http.Header, isHTTPS bool, handler http.Handler) uint16 {
|
||||||
server := httptest.NewUnstartedServer(
|
defaultHandler := http.HandlerFunc(
|
||||||
http.HandlerFunc(
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
_, err := r.Cookie(codersdk.SessionTokenCookie)
|
||||||
_, err := r.Cookie(codersdk.SessionTokenCookie)
|
assert.ErrorIs(t, err, http.ErrNoCookie)
|
||||||
assert.ErrorIs(t, err, http.ErrNoCookie)
|
w.Header().Set("X-Forwarded-For", r.Header.Get("X-Forwarded-For"))
|
||||||
w.Header().Set("X-Forwarded-For", r.Header.Get("X-Forwarded-For"))
|
w.Header().Set("X-Got-Host", r.Host)
|
||||||
w.Header().Set("X-Got-Host", r.Host)
|
for name, values := range headers {
|
||||||
for name, values := range headers {
|
for _, value := range values {
|
||||||
for _, value := range values {
|
w.Header().Add(name, value)
|
||||||
w.Header().Add(name, value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusOK)
|
}
|
||||||
_, _ = w.Write([]byte(proxyTestAppBody))
|
w.WriteHeader(http.StatusOK)
|
||||||
},
|
_, _ = w.Write([]byte(proxyTestAppBody))
|
||||||
),
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if handler == nil {
|
||||||
|
handler = defaultHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
server := httptest.NewUnstartedServer(handler)
|
||||||
|
|
||||||
server.Config.ReadHeaderTimeout = time.Minute
|
server.Config.ReadHeaderTimeout = time.Minute
|
||||||
if isHTTPS {
|
if isHTTPS {
|
||||||
server.StartTLS()
|
server.StartTLS()
|
||||||
@@ -361,6 +408,36 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
|
|||||||
Url: appURL,
|
Url: appURL,
|
||||||
Subdomain: true,
|
Subdomain: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Slug: proxyTestAppNamePublicCORSPassthru,
|
||||||
|
DisplayName: proxyTestAppNamePublicCORSPassthru,
|
||||||
|
SharingLevel: proto.AppSharingLevel_PUBLIC,
|
||||||
|
Url: appURL,
|
||||||
|
Subdomain: true,
|
||||||
|
// CorsBehavior: proto.AppCORSBehavior_PASSTHRU,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Slug: proxyTestAppNameAuthenticatedCORSPassthru,
|
||||||
|
DisplayName: proxyTestAppNameAuthenticatedCORSPassthru,
|
||||||
|
SharingLevel: proto.AppSharingLevel_AUTHENTICATED,
|
||||||
|
Url: appURL,
|
||||||
|
Subdomain: true,
|
||||||
|
// CorsBehavior: proto.AppCORSBehavior_PASSTHRU,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Slug: proxyTestAppNamePublicCORSDefault,
|
||||||
|
DisplayName: proxyTestAppNamePublicCORSDefault,
|
||||||
|
SharingLevel: proto.AppSharingLevel_PUBLIC,
|
||||||
|
Url: appURL,
|
||||||
|
Subdomain: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Slug: proxyTestAppNameAuthenticatedCORSDefault,
|
||||||
|
DisplayName: proxyTestAppNameAuthenticatedCORSDefault,
|
||||||
|
SharingLevel: proto.AppSharingLevel_AUTHENTICATED,
|
||||||
|
Url: appURL,
|
||||||
|
Subdomain: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{
|
version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{
|
||||||
Parse: echo.ParseComplete,
|
Parse: echo.ParseComplete,
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package cors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contextKeyBehavior struct{}
|
||||||
|
|
||||||
|
// WithBehavior sets the CORS behavior for the given context.
|
||||||
|
func WithBehavior(ctx context.Context, behavior codersdk.CORSBehavior) context.Context {
|
||||||
|
return context.WithValue(ctx, contextKeyBehavior{}, behavior)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasBehavior returns true if the given context has the specified CORS behavior.
|
||||||
|
func HasBehavior(ctx context.Context, behavior codersdk.CORSBehavior) bool {
|
||||||
|
val := ctx.Value(contextKeyBehavior{})
|
||||||
|
b, ok := val.(codersdk.CORSBehavior)
|
||||||
|
return ok && b == behavior
|
||||||
|
}
|
||||||
@@ -151,6 +151,7 @@ func (p *DBTokenProvider) Issue(ctx context.Context, rw http.ResponseWriter, r *
|
|||||||
if dbReq.AppURL != nil {
|
if dbReq.AppURL != nil {
|
||||||
token.AppURL = dbReq.AppURL.String()
|
token.AppURL = dbReq.AppURL.String()
|
||||||
}
|
}
|
||||||
|
token.CORSBehavior = codersdk.CORSBehavior(dbReq.CorsBehavior)
|
||||||
|
|
||||||
// Verify the user has access to the app.
|
// Verify the user has access to the app.
|
||||||
authed, warnings, err := p.authorizeRequest(r.Context(), authz, dbReq)
|
authed, warnings, err := p.authorizeRequest(r.Context(), authz, dbReq)
|
||||||
|
|||||||
@@ -301,11 +301,12 @@ func Test_ResolveRequest(t *testing.T) {
|
|||||||
RegisteredClaims: jwtutils.RegisteredClaims{
|
RegisteredClaims: jwtutils.RegisteredClaims{
|
||||||
Expiry: jwt.NewNumericDate(token.Expiry.Time()),
|
Expiry: jwt.NewNumericDate(token.Expiry.Time()),
|
||||||
},
|
},
|
||||||
Request: req,
|
Request: req,
|
||||||
UserID: me.ID,
|
UserID: me.ID,
|
||||||
WorkspaceID: workspace.ID,
|
WorkspaceID: workspace.ID,
|
||||||
AgentID: agentID,
|
AgentID: agentID,
|
||||||
AppURL: appURL,
|
AppURL: appURL,
|
||||||
|
CORSBehavior: codersdk.CORSBehaviorSimple,
|
||||||
}, token)
|
}, token)
|
||||||
require.NotZero(t, token.Expiry)
|
require.NotZero(t, token.Expiry)
|
||||||
require.WithinDuration(t, time.Now().Add(workspaceapps.DefaultTokenExpiry), token.Expiry.Time(), time.Minute)
|
require.WithinDuration(t, time.Now().Add(workspaceapps.DefaultTokenExpiry), token.Expiry.Time(), time.Minute)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/coder/coder/v2/coderd/tracing"
|
"github.com/coder/coder/v2/coderd/tracing"
|
||||||
"github.com/coder/coder/v2/coderd/util/slice"
|
"github.com/coder/coder/v2/coderd/util/slice"
|
||||||
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
||||||
|
"github.com/coder/coder/v2/coderd/workspaceapps/cors"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
||||||
"github.com/coder/coder/v2/site"
|
"github.com/coder/coder/v2/site"
|
||||||
@@ -323,6 +324,37 @@ func (s *Server) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request)
|
|||||||
s.proxyWorkspaceApp(rw, r, *token, chiPath, appurl.ApplicationURL{})
|
s.proxyWorkspaceApp(rw, r, *token, chiPath, appurl.ApplicationURL{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// determineCORSBehavior examines the given token and conditionally applies
|
||||||
|
// CORS middleware if the token specifies that behavior.
|
||||||
|
func (s *Server) determineCORSBehavior(token *SignedToken, app appurl.ApplicationURL) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
// Create the CORS middleware handler upfront.
|
||||||
|
corsHandler := httpmw.WorkspaceAppCors(s.HostnameRegex, app)(next)
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
var behavior codersdk.CORSBehavior
|
||||||
|
if token != nil {
|
||||||
|
behavior = token.CORSBehavior
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add behavior to context regardless of which handler we use,
|
||||||
|
// since we will use this later on to determine if we should strip
|
||||||
|
// CORS headers in the response.
|
||||||
|
r = r.WithContext(cors.WithBehavior(r.Context(), behavior))
|
||||||
|
|
||||||
|
switch behavior {
|
||||||
|
case codersdk.CORSBehaviorPassthru:
|
||||||
|
// Bypass the CORS middleware.
|
||||||
|
next.ServeHTTP(rw, r)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
// Apply the CORS middleware.
|
||||||
|
corsHandler.ServeHTTP(rw, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// HandleSubdomain handles subdomain-based application proxy requests (aka.
|
// HandleSubdomain handles subdomain-based application proxy requests (aka.
|
||||||
// DevURLs in Coder V1).
|
// DevURLs in Coder V1).
|
||||||
//
|
//
|
||||||
@@ -394,36 +426,36 @@ func (s *Server) HandleSubdomain(middlewares ...func(http.Handler) http.Handler)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the passed in app middlewares before checking authentication and
|
if !s.handleAPIKeySmuggling(rw, r, AccessMethodSubdomain) {
|
||||||
// passing to the proxy app.
|
return
|
||||||
mws := chi.Middlewares(append(middlewares, httpmw.WorkspaceAppCors(s.HostnameRegex, app)))
|
}
|
||||||
mws.Handler(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
if !s.handleAPIKeySmuggling(rw, r, AccessMethodSubdomain) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
token, ok := ResolveRequest(rw, r, ResolveRequestOptions{
|
// Generate a signed token for the request.
|
||||||
Logger: s.Logger,
|
token, ok := ResolveRequest(rw, r, ResolveRequestOptions{
|
||||||
CookieCfg: s.Cookies,
|
Logger: s.Logger,
|
||||||
SignedTokenProvider: s.SignedTokenProvider,
|
SignedTokenProvider: s.SignedTokenProvider,
|
||||||
DashboardURL: s.DashboardURL,
|
DashboardURL: s.DashboardURL,
|
||||||
PathAppBaseURL: s.AccessURL,
|
PathAppBaseURL: s.AccessURL,
|
||||||
AppHostname: s.Hostname,
|
AppHostname: s.Hostname,
|
||||||
AppRequest: Request{
|
AppRequest: Request{
|
||||||
AccessMethod: AccessMethodSubdomain,
|
AccessMethod: AccessMethodSubdomain,
|
||||||
BasePath: "/",
|
BasePath: "/",
|
||||||
Prefix: app.Prefix,
|
Prefix: app.Prefix,
|
||||||
UsernameOrID: app.Username,
|
UsernameOrID: app.Username,
|
||||||
WorkspaceNameOrID: app.WorkspaceName,
|
WorkspaceNameOrID: app.WorkspaceName,
|
||||||
AgentNameOrID: app.AgentName,
|
AgentNameOrID: app.AgentName,
|
||||||
AppSlugOrPort: app.AppSlugOrPort,
|
AppSlugOrPort: app.AppSlugOrPort,
|
||||||
},
|
},
|
||||||
AppPath: r.URL.Path,
|
AppPath: r.URL.Path,
|
||||||
AppQuery: r.URL.RawQuery,
|
AppQuery: r.URL.RawQuery,
|
||||||
})
|
})
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Proxy the request (possibly with the CORS middleware).
|
||||||
|
mws := chi.Middlewares(append(middlewares, s.determineCORSBehavior(token, app)))
|
||||||
|
mws.Handler(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
s.proxyWorkspaceApp(rw, r, *token, r.URL.Path, app)
|
s.proxyWorkspaceApp(rw, r, *token, r.URL.Path, app)
|
||||||
})).ServeHTTP(rw, r.WithContext(ctx))
|
})).ServeHTTP(rw, r.WithContext(ctx))
|
||||||
})
|
})
|
||||||
@@ -560,6 +592,10 @@ func (s *Server) proxyWorkspaceApp(rw http.ResponseWriter, r *http.Request, appT
|
|||||||
proxy := s.AgentProvider.ReverseProxy(appURL, s.DashboardURL, appToken.AgentID, app, s.Hostname)
|
proxy := s.AgentProvider.ReverseProxy(appURL, s.DashboardURL, appToken.AgentID, app, s.Hostname)
|
||||||
|
|
||||||
proxy.ModifyResponse = func(r *http.Response) error {
|
proxy.ModifyResponse = func(r *http.Response) error {
|
||||||
|
// If passthru behavior is set, disable our CORS header stripping.
|
||||||
|
if cors.HasBehavior(r.Request.Context(), codersdk.CORSBehaviorPassthru) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
r.Header.Del(httpmw.AccessControlAllowOriginHeader)
|
r.Header.Del(httpmw.AccessControlAllowOriginHeader)
|
||||||
r.Header.Del(httpmw.AccessControlAllowCredentialsHeader)
|
r.Header.Del(httpmw.AccessControlAllowCredentialsHeader)
|
||||||
r.Header.Del(httpmw.AccessControlAllowMethodsHeader)
|
r.Header.Del(httpmw.AccessControlAllowMethodsHeader)
|
||||||
|
|||||||
@@ -204,6 +204,9 @@ type databaseRequest struct {
|
|||||||
// AppSharingLevel is the sharing level of the app. This is forced to be set
|
// AppSharingLevel is the sharing level of the app. This is forced to be set
|
||||||
// to AppSharingLevelOwner if the access method is terminal.
|
// to AppSharingLevelOwner if the access method is terminal.
|
||||||
AppSharingLevel database.AppSharingLevel
|
AppSharingLevel database.AppSharingLevel
|
||||||
|
// CorsBehavior is set at the template level for all apps/ports in a workspace, and can
|
||||||
|
// either be the current CORS middleware 'simple' or bypass the cors middleware with 'passthru'.
|
||||||
|
CorsBehavior database.CorsBehavior
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDatabase does queries to get the owner user, workspace and agent
|
// getDatabase does queries to get the owner user, workspace and agent
|
||||||
@@ -296,7 +299,14 @@ func (r Request) getDatabase(ctx context.Context, db database.Store) (*databaseR
|
|||||||
// First check if it's a port-based URL with an optional "s" suffix for HTTPS.
|
// First check if it's a port-based URL with an optional "s" suffix for HTTPS.
|
||||||
potentialPortStr = strings.TrimSuffix(r.AppSlugOrPort, "s")
|
potentialPortStr = strings.TrimSuffix(r.AppSlugOrPort, "s")
|
||||||
portUint, portUintErr = strconv.ParseUint(potentialPortStr, 10, 16)
|
portUint, portUintErr = strconv.ParseUint(potentialPortStr, 10, 16)
|
||||||
|
corsBehavior database.CorsBehavior
|
||||||
)
|
)
|
||||||
|
|
||||||
|
tmpl, err := db.GetTemplateByID(ctx, workspace.TemplateID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerrors.Errorf("get template %q: %w", workspace.TemplateID, err)
|
||||||
|
}
|
||||||
|
corsBehavior = tmpl.CorsBehavior
|
||||||
//nolint:nestif
|
//nolint:nestif
|
||||||
if portUintErr == nil {
|
if portUintErr == nil {
|
||||||
protocol := "http"
|
protocol := "http"
|
||||||
@@ -417,6 +427,7 @@ func (r Request) getDatabase(ctx context.Context, db database.Store) (*databaseR
|
|||||||
App: app,
|
App: app,
|
||||||
AppURL: appURLParsed,
|
AppURL: appURLParsed,
|
||||||
AppSharingLevel: appSharingLevel,
|
AppSharingLevel: appSharingLevel,
|
||||||
|
CorsBehavior: corsBehavior,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ type SignedToken struct {
|
|||||||
// Request details.
|
// Request details.
|
||||||
Request `json:"request"`
|
Request `json:"request"`
|
||||||
|
|
||||||
UserID uuid.UUID `json:"user_id"`
|
UserID uuid.UUID `json:"user_id"`
|
||||||
WorkspaceID uuid.UUID `json:"workspace_id"`
|
WorkspaceID uuid.UUID `json:"workspace_id"`
|
||||||
AgentID uuid.UUID `json:"agent_id"`
|
AgentID uuid.UUID `json:"agent_id"`
|
||||||
AppURL string `json:"app_url"`
|
AppURL string `json:"app_url"`
|
||||||
|
CORSBehavior codersdk.CORSBehavior `json:"cors_behavior"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MatchesRequest returns true if the token matches the request. Any token that
|
// MatchesRequest returns true if the token matches the request. Any token that
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package codersdk
|
||||||
|
|
||||||
|
type CORSBehavior string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CORSBehaviorSimple CORSBehavior = "simple"
|
||||||
|
CORSBehaviorPassthru CORSBehavior = "passthru"
|
||||||
|
)
|
||||||
@@ -206,6 +206,9 @@ type CreateTemplateRequest struct {
|
|||||||
// true, and is why `*bool` is used here. When dynamic parameters becomes
|
// true, and is why `*bool` is used here. When dynamic parameters becomes
|
||||||
// the default, this will default to false.
|
// the default, this will default to false.
|
||||||
UseClassicParameterFlow *bool `json:"template_use_classic_parameter_flow,omitempty"`
|
UseClassicParameterFlow *bool `json:"template_use_classic_parameter_flow,omitempty"`
|
||||||
|
|
||||||
|
// CORSBehavior allows optionally specifying the CORS behavior for all shared ports.
|
||||||
|
CORSBehavior *CORSBehavior `json:"cors_behavior"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateWorkspaceRequest provides options for creating a new workspace.
|
// CreateWorkspaceRequest provides options for creating a new workspace.
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ type Template struct {
|
|||||||
// template version.
|
// template version.
|
||||||
RequireActiveVersion bool `json:"require_active_version"`
|
RequireActiveVersion bool `json:"require_active_version"`
|
||||||
MaxPortShareLevel WorkspaceAgentPortShareLevel `json:"max_port_share_level"`
|
MaxPortShareLevel WorkspaceAgentPortShareLevel `json:"max_port_share_level"`
|
||||||
|
CORSBehavior CORSBehavior `json:"cors_behavior"`
|
||||||
|
|
||||||
UseClassicParameterFlow bool `json:"use_classic_parameter_flow"`
|
UseClassicParameterFlow bool `json:"use_classic_parameter_flow"`
|
||||||
}
|
}
|
||||||
@@ -252,6 +253,7 @@ type UpdateTemplateMeta struct {
|
|||||||
// of the template.
|
// of the template.
|
||||||
DisableEveryoneGroupAccess bool `json:"disable_everyone_group_access"`
|
DisableEveryoneGroupAccess bool `json:"disable_everyone_group_access"`
|
||||||
MaxPortShareLevel *WorkspaceAgentPortShareLevel `json:"max_port_share_level,omitempty"`
|
MaxPortShareLevel *WorkspaceAgentPortShareLevel `json:"max_port_share_level,omitempty"`
|
||||||
|
CORSBehavior *CORSBehavior `json:"cors_behavior,omitempty"`
|
||||||
// UseClassicParameterFlow is a flag that switches the default behavior to use the classic
|
// UseClassicParameterFlow is a flag that switches the default behavior to use the classic
|
||||||
// parameter flow when creating a workspace. This only affects deployments with the experiment
|
// parameter flow when creating a workspace. This only affects deployments with the experiment
|
||||||
// "dynamic-parameters" enabled. This setting will live for a period after the experiment is
|
// "dynamic-parameters" enabled. This setting will live for a period after the experiment is
|
||||||
|
|||||||
@@ -13,31 +13,31 @@ We track the following resources:
|
|||||||
|
|
||||||
<!-- Code generated by 'make docs/admin/security/audit-logs.md'. DO NOT EDIT -->
|
<!-- Code generated by 'make docs/admin/security/audit-logs.md'. DO NOT EDIT -->
|
||||||
|
|
||||||
| <b>Resource<b> | | |
|
| <b>Resource<b> | | |
|
||||||
|----------------------------------------------------------|----------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|----------------------------------------------------------|----------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| APIKey<br><i>login, logout, register, create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>true</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scope</td><td>false</td></tr><tr><td>token_name</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
| APIKey<br><i>login, logout, register, create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>true</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scope</td><td>false</td></tr><tr><td>token_name</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||||
| AuditOAuthConvertState<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>from_login_type</td><td>true</td></tr><tr><td>to_login_type</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
| AuditOAuthConvertState<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>from_login_type</td><td>true</td></tr><tr><td>to_login_type</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||||
| Group<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>avatar_url</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>members</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>quota_allowance</td><td>true</td></tr><tr><td>source</td><td>false</td></tr></tbody></table> |
|
| Group<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>avatar_url</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>members</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>quota_allowance</td><td>true</td></tr><tr><td>source</td><td>false</td></tr></tbody></table> |
|
||||||
| AuditableOrganizationMember<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>roles</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
|
| AuditableOrganizationMember<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>roles</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
|
||||||
| CustomRole<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>org_permissions</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>site_permissions</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_permissions</td><td>true</td></tr></tbody></table> |
|
| CustomRole<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>org_permissions</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>site_permissions</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_permissions</td><td>true</td></tr></tbody></table> |
|
||||||
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||||
| GroupSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>auto_create_missing_groups</td><td>true</td></tr><tr><td>field</td><td>true</td></tr><tr><td>legacy_group_name_mapping</td><td>false</td></tr><tr><td>mapping</td><td>true</td></tr><tr><td>regex_filter</td><td>true</td></tr></tbody></table> |
|
| GroupSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>auto_create_missing_groups</td><td>true</td></tr><tr><td>field</td><td>true</td></tr><tr><td>legacy_group_name_mapping</td><td>false</td></tr><tr><td>mapping</td><td>true</td></tr><tr><td>regex_filter</td><td>true</td></tr></tbody></table> |
|
||||||
| HealthSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>dismissed_healthchecks</td><td>true</td></tr><tr><td>id</td><td>false</td></tr></tbody></table> |
|
| HealthSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>dismissed_healthchecks</td><td>true</td></tr><tr><td>id</td><td>false</td></tr></tbody></table> |
|
||||||
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
|
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
|
||||||
| NotificationTemplate<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>actions</td><td>true</td></tr><tr><td>body_template</td><td>true</td></tr><tr><td>enabled_by_default</td><td>true</td></tr><tr><td>group</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>kind</td><td>true</td></tr><tr><td>method</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>title_template</td><td>true</td></tr></tbody></table> |
|
| NotificationTemplate<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>actions</td><td>true</td></tr><tr><td>body_template</td><td>true</td></tr><tr><td>enabled_by_default</td><td>true</td></tr><tr><td>group</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>kind</td><td>true</td></tr><tr><td>method</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>title_template</td><td>true</td></tr></tbody></table> |
|
||||||
| NotificationsSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>id</td><td>false</td></tr><tr><td>notifier_paused</td><td>true</td></tr></tbody></table> |
|
| NotificationsSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>id</td><td>false</td></tr><tr><td>notifier_paused</td><td>true</td></tr></tbody></table> |
|
||||||
| OAuth2ProviderApp<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>callback_url</td><td>true</td></tr><tr><td>client_id_issued_at</td><td>false</td></tr><tr><td>client_secret_expires_at</td><td>true</td></tr><tr><td>client_type</td><td>true</td></tr><tr><td>client_uri</td><td>true</td></tr><tr><td>contacts</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>dynamically_registered</td><td>true</td></tr><tr><td>grant_types</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwks</td><td>true</td></tr><tr><td>jwks_uri</td><td>true</td></tr><tr><td>logo_uri</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>policy_uri</td><td>true</td></tr><tr><td>redirect_uris</td><td>true</td></tr><tr><td>registration_access_token</td><td>true</td></tr><tr><td>registration_client_uri</td><td>true</td></tr><tr><td>response_types</td><td>true</td></tr><tr><td>scope</td><td>true</td></tr><tr><td>software_id</td><td>true</td></tr><tr><td>software_version</td><td>true</td></tr><tr><td>token_endpoint_auth_method</td><td>true</td></tr><tr><td>tos_uri</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
| OAuth2ProviderApp<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>callback_url</td><td>true</td></tr><tr><td>client_id_issued_at</td><td>false</td></tr><tr><td>client_secret_expires_at</td><td>true</td></tr><tr><td>client_type</td><td>true</td></tr><tr><td>client_uri</td><td>true</td></tr><tr><td>contacts</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>dynamically_registered</td><td>true</td></tr><tr><td>grant_types</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwks</td><td>true</td></tr><tr><td>jwks_uri</td><td>true</td></tr><tr><td>logo_uri</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>policy_uri</td><td>true</td></tr><tr><td>redirect_uris</td><td>true</td></tr><tr><td>registration_access_token</td><td>true</td></tr><tr><td>registration_client_uri</td><td>true</td></tr><tr><td>response_types</td><td>true</td></tr><tr><td>scope</td><td>true</td></tr><tr><td>software_id</td><td>true</td></tr><tr><td>software_version</td><td>true</td></tr><tr><td>token_endpoint_auth_method</td><td>true</td></tr><tr><td>tos_uri</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||||
| OAuth2ProviderAppSecret<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>app_id</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>display_secret</td><td>false</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>secret_prefix</td><td>false</td></tr></tbody></table> |
|
| OAuth2ProviderAppSecret<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>app_id</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>display_secret</td><td>false</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>secret_prefix</td><td>false</td></tr></tbody></table> |
|
||||||
| Organization<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>is_default</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr></tbody></table> |
|
| Organization<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>is_default</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr></tbody></table> |
|
||||||
| OrganizationSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>assign_default</td><td>true</td></tr><tr><td>field</td><td>true</td></tr><tr><td>mapping</td><td>true</td></tr></tbody></table> |
|
| OrganizationSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>assign_default</td><td>true</td></tr><tr><td>field</td><td>true</td></tr><tr><td>mapping</td><td>true</td></tr></tbody></table> |
|
||||||
| PrebuildsSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>id</td><td>false</td></tr><tr><td>reconciliation_paused</td><td>true</td></tr></tbody></table> |
|
| PrebuildsSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>id</td><td>false</td></tr><tr><td>reconciliation_paused</td><td>true</td></tr></tbody></table> |
|
||||||
| RoleSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>field</td><td>true</td></tr><tr><td>mapping</td><td>true</td></tr></tbody></table> |
|
| RoleSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>field</td><td>true</td></tr><tr><td>mapping</td><td>true</td></tr></tbody></table> |
|
||||||
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>active_version_id</td><td>true</td></tr><tr><td>activity_bump</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_name</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deprecated</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_port_sharing_level</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_display_name</td><td>false</td></tr><tr><td>organization_icon</td><td>false</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>organization_name</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>require_active_version</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>use_classic_parameter_flow</td><td>true</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
|
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>active_version_id</td><td>true</td></tr><tr><td>activity_bump</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>cors_behavior</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_name</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deprecated</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_port_sharing_level</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_display_name</td><td>false</td></tr><tr><td>organization_icon</td><td>false</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>organization_name</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>require_active_version</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>use_classic_parameter_flow</td><td>true</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
|
||||||
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_name</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>has_ai_task</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>source_example_id</td><td>false</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_name</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>has_ai_task</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>source_example_id</td><td>false</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||||
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>github_com_user_id</td><td>false</td></tr><tr><td>hashed_one_time_passcode</td><td>false</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>is_system</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>one_time_passcode_expires_at</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
|
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>github_com_user_id</td><td>false</td></tr><tr><td>hashed_one_time_passcode</td><td>false</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>is_system</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>one_time_passcode_expires_at</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
|
||||||
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>ai_task_sidebar_app_id</td><td>false</td></tr><tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>has_ai_task</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_name</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>template_version_preset_id</td><td>false</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
|
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>ai_task_sidebar_app_id</td><td>false</td></tr><tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>has_ai_task</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_name</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>template_version_preset_id</td><td>false</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
|
||||||
| WorkspaceProxy<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>derp_enabled</td><td>true</td></tr><tr><td>derp_only</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>region_id</td><td>true</td></tr><tr><td>token_hashed_secret</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>url</td><td>true</td></tr><tr><td>version</td><td>true</td></tr><tr><td>wildcard_hostname</td><td>true</td></tr></tbody></table> |
|
| WorkspaceProxy<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>derp_enabled</td><td>true</td></tr><tr><td>derp_only</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>region_id</td><td>true</td></tr><tr><td>token_hashed_secret</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>url</td><td>true</td></tr><tr><td>version</td><td>true</td></tr><tr><td>wildcard_hostname</td><td>true</td></tr></tbody></table> |
|
||||||
| WorkspaceTable<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>favorite</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>next_start_at</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
| WorkspaceTable<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>favorite</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>next_start_at</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||||
|
|
||||||
<!-- End generated by 'make docs/admin/security/audit-logs.md'. -->
|
<!-- End generated by 'make docs/admin/security/audit-logs.md'. -->
|
||||||
|
|
||||||
|
|||||||
Generated
+19
@@ -1056,6 +1056,21 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||||||
| `vscode_connection` |
|
| `vscode_connection` |
|
||||||
| `jetbrains_connection` |
|
| `jetbrains_connection` |
|
||||||
|
|
||||||
|
## codersdk.CORSBehavior
|
||||||
|
|
||||||
|
```json
|
||||||
|
"simple"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
#### Enumerated Values
|
||||||
|
|
||||||
|
| Value |
|
||||||
|
|------------|
|
||||||
|
| `simple` |
|
||||||
|
| `passthru` |
|
||||||
|
|
||||||
## codersdk.ChangePasswordWithOneTimePasscodeRequest
|
## codersdk.ChangePasswordWithOneTimePasscodeRequest
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -1475,6 +1490,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||||||
],
|
],
|
||||||
"weeks": 0
|
"weeks": 0
|
||||||
},
|
},
|
||||||
|
"cors_behavior": "simple",
|
||||||
"default_ttl_ms": 0,
|
"default_ttl_ms": 0,
|
||||||
"delete_ttl_ms": 0,
|
"delete_ttl_ms": 0,
|
||||||
"description": "string",
|
"description": "string",
|
||||||
@@ -1501,6 +1517,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". |
|
| `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. |
|
| `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. |
|
| `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.CORSBehavior](#codersdkcorsbehavior) | 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. |
|
| `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. |
|
| `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. |
|
| `description` | string | false | | Description is a description of what the template contains. It must be less than 128 bytes. |
|
||||||
@@ -6970,6 +6987,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
|
|||||||
"p95": 146
|
"p95": 146
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"cors_behavior": "simple",
|
||||||
"created_at": "2019-08-24T14:15:22Z",
|
"created_at": "2019-08-24T14:15:22Z",
|
||||||
"created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f",
|
"created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f",
|
||||||
"created_by_name": "string",
|
"created_by_name": "string",
|
||||||
@@ -7009,6 +7027,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
|
|||||||
| `autostart_requirement` | [codersdk.TemplateAutostartRequirement](#codersdktemplateautostartrequirement) | false | | |
|
| `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. |
|
| `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 | | |
|
| `build_time_stats` | [codersdk.TemplateBuildTimeStats](#codersdktemplatebuildtimestats) | false | | |
|
||||||
|
| `cors_behavior` | [codersdk.CORSBehavior](#codersdkcorsbehavior) | false | | |
|
||||||
| `created_at` | string | false | | |
|
| `created_at` | string | false | | |
|
||||||
| `created_by_id` | string | false | | |
|
| `created_by_id` | string | false | | |
|
||||||
| `created_by_name` | string | false | | |
|
| `created_by_name` | string | false | | |
|
||||||
|
|||||||
Generated
+13
@@ -57,6 +57,7 @@ To include deprecated templates, specify `deprecated:true` in the search query.
|
|||||||
"p95": 146
|
"p95": 146
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"cors_behavior": "simple",
|
||||||
"created_at": "2019-08-24T14:15:22Z",
|
"created_at": "2019-08-24T14:15:22Z",
|
||||||
"created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f",
|
"created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f",
|
||||||
"created_by_name": "string",
|
"created_by_name": "string",
|
||||||
@@ -113,6 +114,7 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
|
|||||||
|`»» [any property]`|[codersdk.TransitionStats](schemas.md#codersdktransitionstats)|false|||
|
|`»» [any property]`|[codersdk.TransitionStats](schemas.md#codersdktransitionstats)|false|||
|
||||||
|`»»» p50`|integer|false|||
|
|`»»» p50`|integer|false|||
|
||||||
|`»»» p95`|integer|false|||
|
|`»»» p95`|integer|false|||
|
||||||
|
|`» cors_behavior`|[codersdk.CORSBehavior](schemas.md#codersdkcorsbehavior)|false|||
|
||||||
|`» created_at`|string(date-time)|false|||
|
|`» created_at`|string(date-time)|false|||
|
||||||
|`» created_by_id`|string(uuid)|false|||
|
|`» created_by_id`|string(uuid)|false|||
|
||||||
|`» created_by_name`|string|false|||
|
|`» created_by_name`|string|false|||
|
||||||
@@ -141,6 +143,8 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
|
|||||||
|
|
||||||
| Property | Value |
|
| Property | Value |
|
||||||
|------------------------|-----------------|
|
|------------------------|-----------------|
|
||||||
|
| `cors_behavior` | `simple` |
|
||||||
|
| `cors_behavior` | `passthru` |
|
||||||
| `max_port_share_level` | `owner` |
|
| `max_port_share_level` | `owner` |
|
||||||
| `max_port_share_level` | `authenticated` |
|
| `max_port_share_level` | `authenticated` |
|
||||||
| `max_port_share_level` | `organization` |
|
| `max_port_share_level` | `organization` |
|
||||||
@@ -182,6 +186,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa
|
|||||||
],
|
],
|
||||||
"weeks": 0
|
"weeks": 0
|
||||||
},
|
},
|
||||||
|
"cors_behavior": "simple",
|
||||||
"default_ttl_ms": 0,
|
"default_ttl_ms": 0,
|
||||||
"delete_ttl_ms": 0,
|
"delete_ttl_ms": 0,
|
||||||
"description": "string",
|
"description": "string",
|
||||||
@@ -238,6 +243,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa
|
|||||||
"p95": 146
|
"p95": 146
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"cors_behavior": "simple",
|
||||||
"created_at": "2019-08-24T14:15:22Z",
|
"created_at": "2019-08-24T14:15:22Z",
|
||||||
"created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f",
|
"created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f",
|
||||||
"created_by_name": "string",
|
"created_by_name": "string",
|
||||||
@@ -387,6 +393,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat
|
|||||||
"p95": 146
|
"p95": 146
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"cors_behavior": "simple",
|
||||||
"created_at": "2019-08-24T14:15:22Z",
|
"created_at": "2019-08-24T14:15:22Z",
|
||||||
"created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f",
|
"created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f",
|
||||||
"created_by_name": "string",
|
"created_by_name": "string",
|
||||||
@@ -790,6 +797,7 @@ To include deprecated templates, specify `deprecated:true` in the search query.
|
|||||||
"p95": 146
|
"p95": 146
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"cors_behavior": "simple",
|
||||||
"created_at": "2019-08-24T14:15:22Z",
|
"created_at": "2019-08-24T14:15:22Z",
|
||||||
"created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f",
|
"created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f",
|
||||||
"created_by_name": "string",
|
"created_by_name": "string",
|
||||||
@@ -846,6 +854,7 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
|
|||||||
|`»» [any property]`|[codersdk.TransitionStats](schemas.md#codersdktransitionstats)|false|||
|
|`»» [any property]`|[codersdk.TransitionStats](schemas.md#codersdktransitionstats)|false|||
|
||||||
|`»»» p50`|integer|false|||
|
|`»»» p50`|integer|false|||
|
||||||
|`»»» p95`|integer|false|||
|
|`»»» p95`|integer|false|||
|
||||||
|
|`» cors_behavior`|[codersdk.CORSBehavior](schemas.md#codersdkcorsbehavior)|false|||
|
||||||
|`» created_at`|string(date-time)|false|||
|
|`» created_at`|string(date-time)|false|||
|
||||||
|`» created_by_id`|string(uuid)|false|||
|
|`» created_by_id`|string(uuid)|false|||
|
||||||
|`» created_by_name`|string|false|||
|
|`» created_by_name`|string|false|||
|
||||||
@@ -874,6 +883,8 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
|
|||||||
|
|
||||||
| Property | Value |
|
| Property | Value |
|
||||||
|------------------------|-----------------|
|
|------------------------|-----------------|
|
||||||
|
| `cors_behavior` | `simple` |
|
||||||
|
| `cors_behavior` | `passthru` |
|
||||||
| `max_port_share_level` | `owner` |
|
| `max_port_share_level` | `owner` |
|
||||||
| `max_port_share_level` | `authenticated` |
|
| `max_port_share_level` | `authenticated` |
|
||||||
| `max_port_share_level` | `organization` |
|
| `max_port_share_level` | `organization` |
|
||||||
@@ -990,6 +1001,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template} \
|
|||||||
"p95": 146
|
"p95": 146
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"cors_behavior": "simple",
|
||||||
"created_at": "2019-08-24T14:15:22Z",
|
"created_at": "2019-08-24T14:15:22Z",
|
||||||
"created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f",
|
"created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f",
|
||||||
"created_by_name": "string",
|
"created_by_name": "string",
|
||||||
@@ -1120,6 +1132,7 @@ curl -X PATCH http://coder-server:8080/api/v2/templates/{template} \
|
|||||||
"p95": 146
|
"p95": 146
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"cors_behavior": "simple",
|
||||||
"created_at": "2019-08-24T14:15:22Z",
|
"created_at": "2019-08-24T14:15:22Z",
|
||||||
"created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f",
|
"created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f",
|
||||||
"created_by_name": "string",
|
"created_by_name": "string",
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
|
|||||||
"max_port_sharing_level": ActionTrack,
|
"max_port_sharing_level": ActionTrack,
|
||||||
"activity_bump": ActionTrack,
|
"activity_bump": ActionTrack,
|
||||||
"use_classic_parameter_flow": ActionTrack,
|
"use_classic_parameter_flow": ActionTrack,
|
||||||
|
"cors_behavior": ActionTrack,
|
||||||
},
|
},
|
||||||
&database.TemplateVersion{}: {
|
&database.TemplateVersion{}: {
|
||||||
"id": ActionTrack,
|
"id": ActionTrack,
|
||||||
|
|||||||
@@ -339,11 +339,11 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
|
|||||||
httpmw.ExtractRealIP(s.Options.RealIPConfig),
|
httpmw.ExtractRealIP(s.Options.RealIPConfig),
|
||||||
loggermw.Logger(s.Logger),
|
loggermw.Logger(s.Logger),
|
||||||
prometheusMW,
|
prometheusMW,
|
||||||
corsMW,
|
|
||||||
|
|
||||||
// HandleSubdomain is a middleware that handles all requests to the
|
// HandleSubdomain is a middleware that handles all requests to the
|
||||||
// subdomain-based workspace apps.
|
// subdomain-based workspace apps.
|
||||||
s.AppServer.HandleSubdomain(apiRateLimiter),
|
s.AppServer.HandleSubdomain(apiRateLimiter),
|
||||||
|
corsMW,
|
||||||
// Build-Version is helpful for debugging.
|
// Build-Version is helpful for debugging.
|
||||||
func(next http.Handler) http.Handler {
|
func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ test("update template schedule settings without override other settings", async
|
|||||||
disable_everyone_group_access: false,
|
disable_everyone_group_access: false,
|
||||||
require_active_version: true,
|
require_active_version: true,
|
||||||
max_port_share_level: null,
|
max_port_share_level: null,
|
||||||
|
cors_behavior: null,
|
||||||
allow_user_cancel_workspace_jobs: null,
|
allow_user_cancel_workspace_jobs: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Generated
+8
@@ -307,6 +307,11 @@ export const BypassRatelimitHeader = "X-Coder-Bypass-Ratelimit";
|
|||||||
// From codersdk/client.go
|
// From codersdk/client.go
|
||||||
export const CLITelemetryHeader = "Coder-CLI-Telemetry";
|
export const CLITelemetryHeader = "Coder-CLI-Telemetry";
|
||||||
|
|
||||||
|
// From codersdk/cors_behavior.go
|
||||||
|
export type CORSBehavior = "passthru" | "simple";
|
||||||
|
|
||||||
|
export const CORSBehaviors: CORSBehavior[] = ["passthru", "simple"];
|
||||||
|
|
||||||
// From codersdk/workspacebuilds.go
|
// From codersdk/workspacebuilds.go
|
||||||
export interface CancelWorkspaceBuildParams {
|
export interface CancelWorkspaceBuildParams {
|
||||||
readonly expect_status?: CancelWorkspaceBuildStatus;
|
readonly expect_status?: CancelWorkspaceBuildStatus;
|
||||||
@@ -492,6 +497,7 @@ export interface CreateTemplateRequest {
|
|||||||
readonly require_active_version: boolean;
|
readonly require_active_version: boolean;
|
||||||
readonly max_port_share_level: WorkspaceAgentPortShareLevel | null;
|
readonly max_port_share_level: WorkspaceAgentPortShareLevel | null;
|
||||||
readonly template_use_classic_parameter_flow?: boolean;
|
readonly template_use_classic_parameter_flow?: boolean;
|
||||||
|
readonly cors_behavior: CORSBehavior | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// From codersdk/templateversions.go
|
// From codersdk/templateversions.go
|
||||||
@@ -2816,6 +2822,7 @@ export interface Template {
|
|||||||
readonly time_til_dormant_autodelete_ms: number;
|
readonly time_til_dormant_autodelete_ms: number;
|
||||||
readonly require_active_version: boolean;
|
readonly require_active_version: boolean;
|
||||||
readonly max_port_share_level: WorkspaceAgentPortShareLevel;
|
readonly max_port_share_level: WorkspaceAgentPortShareLevel;
|
||||||
|
readonly cors_behavior: CORSBehavior;
|
||||||
readonly use_classic_parameter_flow: boolean;
|
readonly use_classic_parameter_flow: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3188,6 +3195,7 @@ export interface UpdateTemplateMeta {
|
|||||||
readonly deprecation_message?: string;
|
readonly deprecation_message?: string;
|
||||||
readonly disable_everyone_group_access: boolean;
|
readonly disable_everyone_group_access: boolean;
|
||||||
readonly max_port_share_level?: WorkspaceAgentPortShareLevel;
|
readonly max_port_share_level?: WorkspaceAgentPortShareLevel;
|
||||||
|
readonly cors_behavior?: CORSBehavior;
|
||||||
readonly use_classic_parameter_flow?: boolean;
|
readonly use_classic_parameter_flow?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const newTemplate = (
|
|||||||
const safeTemplateData = {
|
const safeTemplateData = {
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
max_port_share_level: null,
|
max_port_share_level: null,
|
||||||
|
cors_behavior: null,
|
||||||
display_name: formData.display_name,
|
display_name: formData.display_name,
|
||||||
description: formData.description,
|
description: formData.description,
|
||||||
icon: formData.icon,
|
icon: formData.icon,
|
||||||
|
|||||||
+25
@@ -4,6 +4,7 @@ import FormHelperText from "@mui/material/FormHelperText";
|
|||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import {
|
import {
|
||||||
|
CORSBehaviors,
|
||||||
type Template,
|
type Template,
|
||||||
type UpdateTemplateMeta,
|
type UpdateTemplateMeta,
|
||||||
WorkspaceAppSharingLevels,
|
WorkspaceAppSharingLevels,
|
||||||
@@ -52,6 +53,7 @@ export const validationSchema = Yup.object({
|
|||||||
use_classic_parameter_flow: Yup.boolean(),
|
use_classic_parameter_flow: Yup.boolean(),
|
||||||
deprecation_message: Yup.string(),
|
deprecation_message: Yup.string(),
|
||||||
max_port_sharing_level: Yup.string().oneOf(WorkspaceAppSharingLevels),
|
max_port_sharing_level: Yup.string().oneOf(WorkspaceAppSharingLevels),
|
||||||
|
cors_behavior: Yup.string().oneOf(Object.values(CORSBehaviors)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface TemplateSettingsForm {
|
export interface TemplateSettingsForm {
|
||||||
@@ -93,6 +95,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
|||||||
disable_everyone_group_access: false,
|
disable_everyone_group_access: false,
|
||||||
max_port_share_level: template.max_port_share_level,
|
max_port_share_level: template.max_port_share_level,
|
||||||
use_classic_parameter_flow: template.use_classic_parameter_flow,
|
use_classic_parameter_flow: template.use_classic_parameter_flow,
|
||||||
|
cors_behavior: template.cors_behavior,
|
||||||
},
|
},
|
||||||
validationSchema,
|
validationSchema,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
@@ -338,6 +341,28 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
|||||||
</FormFields>
|
</FormFields>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
|
<FormSection
|
||||||
|
title="CORS Behavior"
|
||||||
|
description="Control how Cross-Origin Resource Sharing (CORS) requests are handled for all shared ports."
|
||||||
|
>
|
||||||
|
<FormFields>
|
||||||
|
<TextField
|
||||||
|
{...getFieldHelpers("cors_behavior", {
|
||||||
|
helperText:
|
||||||
|
"Use Passthru to bypass Coder's built-in CORS protection.",
|
||||||
|
})}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
fullWidth
|
||||||
|
select
|
||||||
|
value={form.values.cors_behavior}
|
||||||
|
label="CORS Behavior"
|
||||||
|
>
|
||||||
|
<MenuItem value="simple">Simple (recommended)</MenuItem>
|
||||||
|
<MenuItem value="passthru">Passthru</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</FormFields>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
<FormFooter>
|
<FormFooter>
|
||||||
<Button onClick={onCancel} variant="outline">
|
<Button onClick={onCancel} variant="outline">
|
||||||
Cancel
|
Cancel
|
||||||
|
|||||||
+1
@@ -55,6 +55,7 @@ const validFormValues: FormValues = {
|
|||||||
disable_everyone_group_access: false,
|
disable_everyone_group_access: false,
|
||||||
max_port_share_level: "owner",
|
max_port_share_level: "owner",
|
||||||
use_classic_parameter_flow: true,
|
use_classic_parameter_flow: true,
|
||||||
|
cors_behavior: "simple",
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTemplateSettingsPage = async () => {
|
const renderTemplateSettingsPage = async () => {
|
||||||
|
|||||||
@@ -827,6 +827,7 @@ export const MockTemplate: TypesGen.Template = {
|
|||||||
deprecation_message: "",
|
deprecation_message: "",
|
||||||
max_port_share_level: "public",
|
max_port_share_level: "public",
|
||||||
use_classic_parameter_flow: false,
|
use_classic_parameter_flow: false,
|
||||||
|
cors_behavior: "simple",
|
||||||
};
|
};
|
||||||
|
|
||||||
const MockTemplateVersionFiles: TemplateVersionFiles = {
|
const MockTemplateVersionFiles: TemplateVersionFiles = {
|
||||||
|
|||||||
Reference in New Issue
Block a user