From 0dc7ca368f34d22dcfbf1f75501da6561e25c084 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Thu, 21 Nov 2024 16:29:40 +0000 Subject: [PATCH 01/11] Working implementation Signed-off-by: Danny Kopping --- coderd/database/dbmem/dbmem.go | 1 + coderd/database/dump.sql | 8 +- ...00277_workspace_app_cors_behavior.down.sql | 4 + .../000277_workspace_app_cors_behavior.up.sql | 10 + coderd/database/models.go | 61 +- coderd/database/queries.sql.go | 18 +- coderd/database/queries/workspaceapps.sql | 3 +- coderd/database/sqlc.yaml | 1 + coderd/httpmw/cors.go | 6 + coderd/httpmw/cors_test.go | 3 +- .../provisionerdserver/provisionerdserver.go | 10 + coderd/workspaceapps/apptest/apptest.go | 173 ++++- coderd/workspaceapps/apptest/setup.go | 99 ++- coderd/workspaceapps/cors/cors.go | 24 + coderd/workspaceapps/db.go | 4 + coderd/workspaceapps/provider.go | 1 + coderd/workspaceapps/proxy.go | 77 +- coderd/workspaceapps/request.go | 8 + coderd/workspaceapps/token.go | 10 +- provisioner/terraform/resources.go | 32 +- provisionersdk/proto/provisioner.pb.go | 715 ++++++++++-------- provisionersdk/proto/provisioner.proto | 6 + provisionersdk/proto/provisioner_drpc.pb.go | 2 +- site/e2e/provisionerGenerated.ts | 10 + 24 files changed, 891 insertions(+), 395 deletions(-) create mode 100644 coderd/database/migrations/000277_workspace_app_cors_behavior.down.sql create mode 100644 coderd/database/migrations/000277_workspace_app_cors_behavior.up.sql create mode 100644 coderd/workspaceapps/cors/cors.go diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 765573b311a84..50d8376d6db42 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -8174,6 +8174,7 @@ func (q *FakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertW Health: arg.Health, Hidden: arg.Hidden, DisplayOrder: arg.DisplayOrder, + CorsBehavior: arg.CorsBehavior, } q.workspaceApps = append(q.workspaceApps, workspaceApp) return workspaceApp, nil diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index eba9b7cf106d3..0d0a613d1f187 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -10,6 +10,11 @@ CREATE TYPE api_key_scope AS ENUM ( 'application_connect' ); +CREATE TYPE app_cors_behavior AS ENUM ( + 'simple', + 'passthru' +); + CREATE TYPE app_sharing_level AS ENUM ( 'owner', 'authenticated', @@ -1580,7 +1585,8 @@ CREATE TABLE workspace_apps ( slug text NOT NULL, external boolean DEFAULT false NOT NULL, display_order integer DEFAULT 0 NOT NULL, - hidden boolean DEFAULT false NOT NULL + hidden boolean DEFAULT false NOT NULL, + cors_behavior app_cors_behavior DEFAULT 'simple'::app_cors_behavior NOT NULL ); COMMENT ON COLUMN workspace_apps.display_order IS 'Specifies the order in which to display agent app in user interfaces.'; diff --git a/coderd/database/migrations/000277_workspace_app_cors_behavior.down.sql b/coderd/database/migrations/000277_workspace_app_cors_behavior.down.sql new file mode 100644 index 0000000000000..5f6e26306594e --- /dev/null +++ b/coderd/database/migrations/000277_workspace_app_cors_behavior.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE workspace_apps + DROP COLUMN IF EXISTS cors_behavior; + +DROP TYPE IF EXISTS app_cors_behavior; \ No newline at end of file diff --git a/coderd/database/migrations/000277_workspace_app_cors_behavior.up.sql b/coderd/database/migrations/000277_workspace_app_cors_behavior.up.sql new file mode 100644 index 0000000000000..aab91bf4024e7 --- /dev/null +++ b/coderd/database/migrations/000277_workspace_app_cors_behavior.up.sql @@ -0,0 +1,10 @@ +CREATE TYPE app_cors_behavior AS ENUM ( + 'simple', + 'passthru' +); + +-- https://www.postgresql.org/docs/16/sql-altertable.html +-- When a column is added with ADD COLUMN and a non-volatile DEFAULT is specified, the default is evaluated at the time +-- of the statement and the result stored in the table's metadata. That value will be used for the column for all existing rows. +ALTER TABLE workspace_apps + ADD COLUMN cors_behavior app_cors_behavior NOT NULL DEFAULT 'simple'::app_cors_behavior; \ No newline at end of file diff --git a/coderd/database/models.go b/coderd/database/models.go index 6b99245079950..28232ef1cfbbb 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -74,6 +74,64 @@ func AllAPIKeyScopeValues() []APIKeyScope { } } +type AppCORSBehavior string + +const ( + AppCorsBehaviorSimple AppCORSBehavior = "simple" + AppCorsBehaviorPassthru AppCORSBehavior = "passthru" +) + +func (e *AppCORSBehavior) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = AppCORSBehavior(s) + case string: + *e = AppCORSBehavior(s) + default: + return fmt.Errorf("unsupported scan type for AppCORSBehavior: %T", src) + } + return nil +} + +type NullAppCORSBehavior struct { + AppCORSBehavior AppCORSBehavior `json:"app_cors_behavior"` + Valid bool `json:"valid"` // Valid is true if AppCORSBehavior is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullAppCORSBehavior) Scan(value interface{}) error { + if value == nil { + ns.AppCORSBehavior, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.AppCORSBehavior.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullAppCORSBehavior) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.AppCORSBehavior), nil +} + +func (e AppCORSBehavior) Valid() bool { + switch e { + case AppCorsBehaviorSimple, + AppCorsBehaviorPassthru: + return true + } + return false +} + +func AllAppCORSBehaviorValues() []AppCORSBehavior { + return []AppCORSBehavior{ + AppCorsBehaviorSimple, + AppCorsBehaviorPassthru, + } +} + type AppSharingLevel string const ( @@ -3082,7 +3140,8 @@ type WorkspaceApp struct { // Specifies the order in which to display agent app in user interfaces. DisplayOrder int32 `db:"display_order" json:"display_order"` // Determines if the app is not shown in user interfaces. - Hidden bool `db:"hidden" json:"hidden"` + Hidden bool `db:"hidden" json:"hidden"` + CorsBehavior AppCORSBehavior `db:"cors_behavior" json:"cors_behavior"` } // A record of workspace app usage statistics diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 33a3ce12a444d..bb4165230548a 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -13070,7 +13070,7 @@ func (q *sqlQuerier) InsertWorkspaceAgentStats(ctx context.Context, arg InsertWo } const getWorkspaceAppByAgentIDAndSlug = `-- name: GetWorkspaceAppByAgentIDAndSlug :one -SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external, display_order, hidden FROM workspace_apps WHERE agent_id = $1 AND slug = $2 +SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external, display_order, hidden, cors_behavior FROM workspace_apps WHERE agent_id = $1 AND slug = $2 ` type GetWorkspaceAppByAgentIDAndSlugParams struct { @@ -13099,12 +13099,13 @@ func (q *sqlQuerier) GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg Ge &i.External, &i.DisplayOrder, &i.Hidden, + &i.CorsBehavior, ) return i, err } const getWorkspaceAppsByAgentID = `-- name: GetWorkspaceAppsByAgentID :many -SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external, display_order, hidden FROM workspace_apps WHERE agent_id = $1 ORDER BY slug ASC +SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external, display_order, hidden, cors_behavior FROM workspace_apps WHERE agent_id = $1 ORDER BY slug ASC ` func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceApp, error) { @@ -13134,6 +13135,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid &i.External, &i.DisplayOrder, &i.Hidden, + &i.CorsBehavior, ); err != nil { return nil, err } @@ -13149,7 +13151,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid } const getWorkspaceAppsByAgentIDs = `-- name: GetWorkspaceAppsByAgentIDs :many -SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external, display_order, hidden FROM workspace_apps WHERE agent_id = ANY($1 :: uuid [ ]) ORDER BY slug ASC +SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external, display_order, hidden, cors_behavior FROM workspace_apps WHERE agent_id = ANY($1 :: uuid [ ]) ORDER BY slug ASC ` func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceApp, error) { @@ -13179,6 +13181,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid. &i.External, &i.DisplayOrder, &i.Hidden, + &i.CorsBehavior, ); err != nil { return nil, err } @@ -13194,7 +13197,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid. } const getWorkspaceAppsCreatedAfter = `-- name: GetWorkspaceAppsCreatedAfter :many -SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external, display_order, hidden FROM workspace_apps WHERE created_at > $1 ORDER BY slug ASC +SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external, display_order, hidden, cors_behavior FROM workspace_apps WHERE created_at > $1 ORDER BY slug ASC ` func (q *sqlQuerier) GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceApp, error) { @@ -13224,6 +13227,7 @@ func (q *sqlQuerier) GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt &i.External, &i.DisplayOrder, &i.Hidden, + &i.CorsBehavior, ); err != nil { return nil, err } @@ -13252,6 +13256,7 @@ INSERT INTO external, subdomain, sharing_level, + cors_behavior, healthcheck_url, healthcheck_interval, healthcheck_threshold, @@ -13260,7 +13265,7 @@ INSERT INTO hidden ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) RETURNING id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external, display_order, hidden + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) RETURNING id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external, display_order, hidden, cors_behavior ` type InsertWorkspaceAppParams struct { @@ -13275,6 +13280,7 @@ type InsertWorkspaceAppParams struct { External bool `db:"external" json:"external"` Subdomain bool `db:"subdomain" json:"subdomain"` SharingLevel AppSharingLevel `db:"sharing_level" json:"sharing_level"` + CorsBehavior AppCORSBehavior `db:"cors_behavior" json:"cors_behavior"` HealthcheckUrl string `db:"healthcheck_url" json:"healthcheck_url"` HealthcheckInterval int32 `db:"healthcheck_interval" json:"healthcheck_interval"` HealthcheckThreshold int32 `db:"healthcheck_threshold" json:"healthcheck_threshold"` @@ -13296,6 +13302,7 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace arg.External, arg.Subdomain, arg.SharingLevel, + arg.CorsBehavior, arg.HealthcheckUrl, arg.HealthcheckInterval, arg.HealthcheckThreshold, @@ -13322,6 +13329,7 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace &i.External, &i.DisplayOrder, &i.Hidden, + &i.CorsBehavior, ) return i, err } diff --git a/coderd/database/queries/workspaceapps.sql b/coderd/database/queries/workspaceapps.sql index 9ae1367093efd..393427c1ccc68 100644 --- a/coderd/database/queries/workspaceapps.sql +++ b/coderd/database/queries/workspaceapps.sql @@ -24,6 +24,7 @@ INSERT INTO external, subdomain, sharing_level, + cors_behavior, healthcheck_url, healthcheck_interval, healthcheck_threshold, @@ -32,7 +33,7 @@ INSERT INTO hidden ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) RETURNING *; + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) RETURNING *; -- name: UpdateWorkspaceAppHealthByID :exec UPDATE diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml index fac159f71ebe3..105fceade4f1c 100644 --- a/coderd/database/sqlc.yaml +++ b/coderd/database/sqlc.yaml @@ -146,6 +146,7 @@ sql: login_type_oauth2_provider_app: LoginTypeOAuth2ProviderApp crypto_key_feature_workspace_apps_api_key: CryptoKeyFeatureWorkspaceAppsAPIKey crypto_key_feature_oidc_convert: CryptoKeyFeatureOIDCConvert + app_cors_behavior: AppCORSBehavior rules: - name: do-not-use-public-schema-in-queries message: "do not use public schema in queries" diff --git a/coderd/httpmw/cors.go b/coderd/httpmw/cors.go index dd69c714379a4..8d6b946617378 100644 --- a/coderd/httpmw/cors.go +++ b/coderd/httpmw/cors.go @@ -8,6 +8,7 @@ import ( "github.com/go-chi/cors" "github.com/coder/coder/v2/coderd/workspaceapps/appurl" + ws_cors "github.com/coder/coder/v2/coderd/workspaceapps/cors" ) const ( @@ -47,6 +48,11 @@ func Cors(allowAll bool, origins ...string) func(next http.Handler) http.Handler func WorkspaceAppCors(regex *regexp.Regexp, app appurl.ApplicationURL) func(next http.Handler) http.Handler { return cors.Handler(cors.Options{ AllowOriginFunc: func(r *http.Request, rawOrigin string) bool { + // If passthru behavior is set, disable our simplified CORS handling. + if ws_cors.HasBehavior(r.Context(), ws_cors.AppCORSBehaviorPassthru) { + return true + } + origin, err := url.Parse(rawOrigin) if rawOrigin == "" || origin.Host == "" || err != nil { return false diff --git a/coderd/httpmw/cors_test.go b/coderd/httpmw/cors_test.go index 57111799ff292..5dc746ccf7edd 100644 --- a/coderd/httpmw/cors_test.go +++ b/coderd/httpmw/cors_test.go @@ -105,7 +105,8 @@ func TestWorkspaceAppCors(t *testing.T) { r.Header.Set("Access-Control-Request-Method", method) } - handler := httpmw.WorkspaceAppCors(regex, test.app)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + // TODO: signed token provider + handler := httpmw.WorkspaceAppCors(nil, regex, test.app)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { rw.WriteHeader(http.StatusNoContent) })) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 71847b0562d0b..734dd3279e520 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1988,6 +1988,15 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. sharingLevel = database.AppSharingLevelPublic } + // TODO: consider backwards-compat where proto might not contain this field + var corsBehavior database.AppCORSBehavior + switch app.CorsBehavior { + case sdkproto.AppCORSBehavior_PASSTHRU: + corsBehavior = database.AppCorsBehaviorPassthru + default: + corsBehavior = database.AppCorsBehaviorSimple + } + dbApp, err := db.InsertWorkspaceApp(ctx, database.InsertWorkspaceAppParams{ ID: uuid.New(), CreatedAt: dbtime.Now(), @@ -2006,6 +2015,7 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. External: app.External, Subdomain: app.Subdomain, SharingLevel: sharingLevel, + CorsBehavior: corsBehavior, HealthcheckUrl: app.Healthcheck.Url, HealthcheckInterval: app.Healthcheck.Interval, HealthcheckThreshold: app.Healthcheck.Threshold, diff --git a/coderd/workspaceapps/apptest/apptest.go b/coderd/workspaceapps/apptest/apptest.go index c6e251806230d..f8448d8daad52 100644 --- a/coderd/workspaceapps/apptest/apptest.go +++ b/coderd/workspaceapps/apptest/apptest.go @@ -472,6 +472,53 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { }) }) + t.Run("CORS", func(t *testing.T) { + t.Parallel() + + t.Run("AuthenticatedPassthruProtected", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + + appDetails := setupProxyTest(t, nil) + + // Given: an unauthenticated client + client := appDetails.AppClient(t) + client.SetSessionToken("") + + // When: a request is made to an authenticated app with passthru CORS behavior + resp, err := requestWithRetries(ctx, t, client, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.AuthenticatedCORSPassthru).String(), nil) + require.NoError(t, err) + defer resp.Body.Close() + + // Then: the request is redirected to the primary access URL because even though CORS is passthru, + // the request must still be authenticated first + require.Equal(t, http.StatusSeeOther, resp.StatusCode) + gotLocation, err := resp.Location() + require.NoError(t, err) + require.Equal(t, appDetails.SDKClient.URL.Host, gotLocation.Host) + require.Equal(t, "/api/v2/applications/auth-redirect", gotLocation.Path) + }) + + t.Run("AuthenticatedPassthruOK", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + + appDetails := setupProxyTest(t, nil) + + userClient, _ := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, appDetails.FirstUser.OrganizationID, rbac.RoleMember()) + userAppClient := appDetails.AppClient(t) + userAppClient.SetSessionToken(userClient.SessionToken()) + + // Given: an authenticated app with passthru CORS behavior + resp, err := requestWithRetries(ctx, t, userAppClient, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.AuthenticatedCORSPassthru).String(), nil) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode) + }) + }) + t.Run("WorkspaceApplicationAuth", func(t *testing.T) { t.Parallel() @@ -1399,10 +1446,14 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { agnt = workspaceBuild.Resources[0].Agents[0] found := map[string]codersdk.WorkspaceAppSharingLevel{} expected := map[string]codersdk.WorkspaceAppSharingLevel{ - proxyTestAppNameFake: codersdk.WorkspaceAppSharingLevelOwner, - proxyTestAppNameOwner: codersdk.WorkspaceAppSharingLevelOwner, - proxyTestAppNameAuthenticated: codersdk.WorkspaceAppSharingLevelAuthenticated, - proxyTestAppNamePublic: codersdk.WorkspaceAppSharingLevelPublic, + proxyTestAppNameFake: codersdk.WorkspaceAppSharingLevelOwner, + proxyTestAppNameOwner: codersdk.WorkspaceAppSharingLevelOwner, + proxyTestAppNameAuthenticated: codersdk.WorkspaceAppSharingLevelAuthenticated, + proxyTestAppNamePublic: codersdk.WorkspaceAppSharingLevelPublic, + proxyTestAppNameAuthenticatedCORSPassthru: codersdk.WorkspaceAppSharingLevelAuthenticated, + proxyTestAppNamePublicCORSPassthru: codersdk.WorkspaceAppSharingLevelPublic, + proxyTestAppNameAuthenticatedCORSDefault: codersdk.WorkspaceAppSharingLevelAuthenticated, + proxyTestAppNamePublicCORSDefault: codersdk.WorkspaceAppSharingLevelPublic, } for _, app := range agnt.Apps { found[app.DisplayName] = app.SharingLevel @@ -1559,6 +1610,12 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { // Unauthenticated user should not have any access. verifyAccess(t, appDetails, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameAuthenticated, clientWithNoAuth, false, true) + + // Unauthenticated user should not have any access, regardless of CORS behavior (using passthru). + verifyAccess(t, appDetails, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameAuthenticatedCORSPassthru, clientWithNoAuth, false, true) + + // Unauthenticated user should not have any access, regardless of CORS behavior (using default). + verifyAccess(t, appDetails, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameAuthenticatedCORSDefault, clientWithNoAuth, false, true) }) t.Run("LevelPublic", func(t *testing.T) { @@ -1576,6 +1633,12 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { // Unauthenticated user should be able to access the workspace. verifyAccess(t, appDetails, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNamePublic, clientWithNoAuth, allowedUnlessSharingDisabled, !allowedUnlessSharingDisabled) + + // Unauthenticated user should have access, regardless of CORS behavior (using passthru). + verifyAccess(t, appDetails, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNamePublicCORSPassthru, clientWithNoAuth, allowedUnlessSharingDisabled, !allowedUnlessSharingDisabled) + + // Unauthenticated user should have access, regardless of CORS behavior (using default). + verifyAccess(t, appDetails, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNamePublicCORSDefault, clientWithNoAuth, allowedUnlessSharingDisabled, !allowedUnlessSharingDisabled) }) } @@ -1778,6 +1841,95 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { require.Equal(t, []string{"baz"}, resp.Header.Values("X-Foobar")) }) + // See above test for original implementation. + t.Run("CORSHeadersConditionalStrip", func(t *testing.T) { + t.Parallel() + + // Set a bunch of headers which may or may not be stripped, depending on the CORS behavior. + // See coderd/workspaceapps/proxy.go (proxyWorkspaceApp). + headers := http.Header{ + "X-Foobar": []string{"baz"}, + "Access-Control-Allow-Origin": []string{"http://localhost"}, + "access-control-allow-origin": []string{"http://localhost"}, + "Access-Control-Allow-Credentials": []string{"true"}, + "Access-Control-Allow-Methods": []string{"PUT"}, + "Access-Control-Allow-Headers": []string{"X-Foobar"}, + "Vary": []string{ + "Origin", + "origin", + "Access-Control-Request-Headers", + "access-Control-request-Headers", + "Access-Control-Request-Methods", + "ACCESS-CONTROL-REQUEST-METHODS", + "X-Foobar", + }, + } + + appDetails := setupProxyTest(t, &DeploymentOptions{ + headers: headers, + }) + + tests := []struct { + name string + app App + shouldStrip bool + }{ + { + // Uses an app which does not set CORS behavior, which *should* be equivalent to default. + name: "NormalStrip", + app: appDetails.Apps.Owner, + shouldStrip: true, + }, + { + // Explicitly uses the default CORS behavior. + name: "DefaultStrip", + app: appDetails.Apps.PublicCORSDefault, + shouldStrip: true, + }, + { + // Explicitly does not strip CORS headers. + name: "PassthruNoStrip", + app: appDetails.Apps.PublicCORSPassthru, + shouldStrip: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + + // Given: a particular app + appURL := appDetails.SubdomainAppURL(tc.app) + + // When: querying the app + resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, appURL.String(), nil) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusOK, resp.StatusCode) + + // Then: the CORS headers should be conditionally stripped or not, depending on the CORS behavior. + if tc.shouldStrip { + require.Empty(t, resp.Header.Values("Access-Control-Allow-Origin")) + require.Empty(t, resp.Header.Values("Access-Control-Allow-Credentials")) + require.Empty(t, resp.Header.Values("Access-Control-Allow-Methods")) + require.Empty(t, resp.Header.Values("Access-Control-Allow-Headers")) + } else { + for k, v := range headers { + // We dedupe the values because some headers have been set multiple times. + headerVal := dedupe(resp.Header.Values(k)) + assert.ElementsMatchf(t, headerVal, v, "header %q does not contain %q", k, v) + } + } + + // This header is not a CORS-related header, so it should always be set. + require.Equal(t, []string{"baz"}, resp.Header.Values("X-Foobar")) + }) + } + }) + t.Run("ReportStats", func(t *testing.T) { t.Parallel() @@ -1999,3 +2151,16 @@ func findCookie(cookies []*http.Cookie, name string) *http.Cookie { } return nil } + +func dedupe[T comparable](elements []T) []T { + found := map[T]bool{} + result := []T{} + + for _, v := range elements { + if !found[v] { + found[v] = true + result = append(result, v) + } + } + return result +} diff --git a/coderd/workspaceapps/apptest/setup.go b/coderd/workspaceapps/apptest/setup.go index 06544446fe6e2..06ff54eec04fc 100644 --- a/coderd/workspaceapps/apptest/setup.go +++ b/coderd/workspaceapps/apptest/setup.go @@ -22,6 +22,7 @@ import ( "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/workspaceapps" "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/agentsdk" "github.com/coder/coder/v2/cryptorand" @@ -31,13 +32,17 @@ import ( ) const ( - proxyTestAgentName = "agent-name" - proxyTestAppNameFake = "test-app-fake" - proxyTestAppNameOwner = "test-app-owner" - proxyTestAppNameAuthenticated = "test-app-authenticated" - proxyTestAppNamePublic = "test-app-public" - proxyTestAppQuery = "query=true" - proxyTestAppBody = "hello world from apps test" + proxyTestAgentName = "agent-name" + proxyTestAppNameFake = "test-app-fake" + proxyTestAppNameOwner = "test-app-owner" + proxyTestAppNameAuthenticated = "test-app-authenticated" + proxyTestAppNamePublic = "test-app-public" + 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" proxyTestSubdomain = "test.coder.com" @@ -93,6 +98,10 @@ type App struct { // Prefix should have ---. Prefix string Query string + Path string + + // Control the behavior of CORS handling. + CORSBehavior cors.AppCORSBehavior } // Details are the full test details returned from setupProxyTestWithFactory. @@ -109,12 +118,16 @@ type Details struct { AppPort uint16 Apps struct { - Fake App - Owner App - Authenticated App - Public App - Port App - PortHTTPS App + Fake App + Owner App + Authenticated App + Public App + Port App + PortHTTPS App + PublicCORSPassthru App + AuthenticatedCORSPassthru App + PublicCORSDefault App + AuthenticatedCORSDefault App } } @@ -252,6 +265,36 @@ func setupProxyTestWithFactory(t *testing.T, factory DeploymentFactory, opts *De AgentName: agnt.Name, AppSlugOrPort: strconv.Itoa(int(opts.port)) + "s", } + details.Apps.PublicCORSPassthru = App{ + Username: me.Username, + WorkspaceName: workspace.Name, + AgentName: agnt.Name, + AppSlugOrPort: proxyTestAppNamePublicCORSPassthru, + CORSBehavior: cors.AppCORSBehaviorPassthru, + Query: proxyTestAppQuery, + } + details.Apps.AuthenticatedCORSPassthru = App{ + Username: me.Username, + WorkspaceName: workspace.Name, + AgentName: agnt.Name, + AppSlugOrPort: proxyTestAppNameAuthenticatedCORSPassthru, + CORSBehavior: cors.AppCORSBehaviorPassthru, + 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 } @@ -361,6 +404,36 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U Url: appURL, 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{ Parse: echo.ParseComplete, diff --git a/coderd/workspaceapps/cors/cors.go b/coderd/workspaceapps/cors/cors.go new file mode 100644 index 0000000000000..0ee34cf390c5a --- /dev/null +++ b/coderd/workspaceapps/cors/cors.go @@ -0,0 +1,24 @@ +package cors + +import "context" + +type AppCORSBehavior string + +const ( + AppCORSBehaviorSimple AppCORSBehavior = "simple" + AppCORSBehaviorPassthru AppCORSBehavior = "passthru" +) + +type contextKeyBehavior struct{} + +// WithBehavior sets the CORS behavior for the given context. +func WithBehavior(ctx context.Context, behavior AppCORSBehavior) 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 AppCORSBehavior) bool { + val := ctx.Value(contextKeyBehavior{}) + b, ok := val.(AppCORSBehavior) + return ok && b == behavior +} diff --git a/coderd/workspaceapps/db.go b/coderd/workspaceapps/db.go index 1aa4dfe91bdd0..1a16a680dfb4d 100644 --- a/coderd/workspaceapps/db.go +++ b/coderd/workspaceapps/db.go @@ -16,6 +16,7 @@ import ( "github.com/go-jose/go-jose/v4/jwt" "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/cryptokeys" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" @@ -24,6 +25,7 @@ import ( "github.com/coder/coder/v2/coderd/jwtutils" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" + "github.com/coder/coder/v2/coderd/workspaceapps/cors" "github.com/coder/coder/v2/codersdk" ) @@ -123,12 +125,14 @@ func (p *DBTokenProvider) Issue(ctx context.Context, rw http.ResponseWriter, r * WriteWorkspaceApp500(p.Logger, p.DashboardURL, rw, r, &appReq, err, "get app details from database") return nil, "", false } + token.UserID = dbReq.User.ID token.WorkspaceID = dbReq.Workspace.ID token.AgentID = dbReq.Agent.ID if dbReq.AppURL != nil { token.AppURL = dbReq.AppURL.String() } + token.CORSBehavior = cors.AppCORSBehavior(dbReq.AppCORSBehavior) // Verify the user has access to the app. authed, warnings, err := p.authorizeRequest(r.Context(), authz, dbReq) diff --git a/coderd/workspaceapps/provider.go b/coderd/workspaceapps/provider.go index 1887036e35cbf..b2ea018c9c89b 100644 --- a/coderd/workspaceapps/provider.go +++ b/coderd/workspaceapps/provider.go @@ -7,6 +7,7 @@ import ( "time" "cdr.dev/slog" + "github.com/coder/coder/v2/codersdk" ) diff --git a/coderd/workspaceapps/proxy.go b/coderd/workspaceapps/proxy.go index a9c60357a009d..979a05d53f13c 100644 --- a/coderd/workspaceapps/proxy.go +++ b/coderd/workspaceapps/proxy.go @@ -20,6 +20,7 @@ import ( "nhooyr.io/websocket" "cdr.dev/slog" + "github.com/coder/coder/v2/agent/agentssh" "github.com/coder/coder/v2/coderd/cryptokeys" "github.com/coder/coder/v2/coderd/database/dbtime" @@ -29,6 +30,7 @@ import ( "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/coderd/util/slice" "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/workspacesdk" "github.com/coder/coder/v2/site" @@ -395,41 +397,55 @@ func (s *Server) HandleSubdomain(middlewares ...func(http.Handler) http.Handler) return } - // Use the passed in app middlewares before checking authentication and - // passing to the proxy app. - 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 - } + if !s.handleAPIKeySmuggling(rw, r, AccessMethodSubdomain) { + return + } - token, ok := ResolveRequest(rw, r, ResolveRequestOptions{ - Logger: s.Logger, - SignedTokenProvider: s.SignedTokenProvider, - DashboardURL: s.DashboardURL, - PathAppBaseURL: s.AccessURL, - AppHostname: s.Hostname, - AppRequest: Request{ - AccessMethod: AccessMethodSubdomain, - BasePath: "/", - Prefix: app.Prefix, - UsernameOrID: app.Username, - WorkspaceNameOrID: app.WorkspaceName, - AgentNameOrID: app.AgentName, - AppSlugOrPort: app.AppSlugOrPort, - }, - AppPath: r.URL.Path, - AppQuery: r.URL.RawQuery, - }) - if !ok { - return - } + // Generate a signed token for the request. + token, ok := ResolveRequest(rw, r, ResolveRequestOptions{ + Logger: s.Logger, + SignedTokenProvider: s.SignedTokenProvider, + DashboardURL: s.DashboardURL, + PathAppBaseURL: s.AccessURL, + AppHostname: s.Hostname, + AppRequest: Request{ + AccessMethod: AccessMethodSubdomain, + BasePath: "/", + Prefix: app.Prefix, + UsernameOrID: app.Username, + WorkspaceNameOrID: app.WorkspaceName, + AgentNameOrID: app.AgentName, + AppSlugOrPort: app.AppSlugOrPort, + }, + AppPath: r.URL.Path, + AppQuery: r.URL.RawQuery, + }) + if !ok { + return + } + + // Use the passed in app middlewares and CORS middleware with the token + mws := chi.Middlewares(append(middlewares, s.injectCORSBehavior(token), httpmw.WorkspaceAppCors(s.HostnameRegex, app))) + mws.Handler(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { s.proxyWorkspaceApp(rw, r, *token, r.URL.Path, app) })).ServeHTTP(rw, r.WithContext(ctx)) }) } } +func (s *Server) injectCORSBehavior(token *SignedToken) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + var behavior cors.AppCORSBehavior + if token != nil { + behavior = token.CORSBehavior + } + + next.ServeHTTP(rw, r.WithContext(cors.WithBehavior(r.Context(), behavior))) + }) + } +} + // parseHostname will return if a given request is attempting to access a // workspace app via a subdomain. If it is, the hostname of the request is parsed // into an appurl.ApplicationURL and true is returned. If the request is not @@ -560,6 +576,11 @@ func (s *Server) proxyWorkspaceApp(rw http.ResponseWriter, r *http.Request, appT proxy := s.AgentProvider.ReverseProxy(appURL, s.DashboardURL, appToken.AgentID, app, s.Hostname) proxy.ModifyResponse = func(r *http.Response) error { + // If passthru behavior is set, disable our CORS header stripping. + if cors.HasBehavior(r.Request.Context(), cors.AppCORSBehaviorPassthru) { + return nil + } + r.Header.Del(httpmw.AccessControlAllowOriginHeader) r.Header.Del(httpmw.AccessControlAllowCredentialsHeader) r.Header.Del(httpmw.AccessControlAllowMethodsHeader) diff --git a/coderd/workspaceapps/request.go b/coderd/workspaceapps/request.go index 0833ab731fe67..8304bf8bd9772 100644 --- a/coderd/workspaceapps/request.go +++ b/coderd/workspaceapps/request.go @@ -202,6 +202,8 @@ type databaseRequest struct { // AppSharingLevel is the sharing level of the app. This is forced to be set // to AppSharingLevelOwner if the access method is terminal. AppSharingLevel database.AppSharingLevel + // AppCORSBehavior defines the behavior of the CORS middleware. + AppCORSBehavior database.AppCORSBehavior } // getDatabase does queries to get the owner user, workspace and agent @@ -290,12 +292,16 @@ func (r Request) getDatabase(ctx context.Context, db database.Store) (*databaseR agentNameOrID = r.AgentNameOrID appURL string appSharingLevel database.AppSharingLevel + appCORSBehavior database.AppCORSBehavior // First check if it's a port-based URL with an optional "s" suffix for HTTPS. potentialPortStr = strings.TrimSuffix(r.AppSlugOrPort, "s") portUint, portUintErr = strconv.ParseUint(potentialPortStr, 10, 16) ) //nolint:nestif if portUintErr == nil { + // TODO: handle this branch + appCORSBehavior = database.AppCorsBehaviorSimple + protocol := "http" if strings.HasSuffix(r.AppSlugOrPort, "s") { protocol = "https" @@ -366,6 +372,7 @@ func (r Request) getDatabase(ctx context.Context, db database.Store) (*databaseR appSharingLevel = database.AppSharingLevelOwner } appURL = app.Url.String + appCORSBehavior = app.CorsBehavior break } } @@ -412,6 +419,7 @@ func (r Request) getDatabase(ctx context.Context, db database.Store) (*databaseR Agent: agent, AppURL: appURLParsed, AppSharingLevel: appSharingLevel, + AppCORSBehavior: appCORSBehavior, }, nil } diff --git a/coderd/workspaceapps/token.go b/coderd/workspaceapps/token.go index dcd8c5a0e5c34..72b8db2bf8129 100644 --- a/coderd/workspaceapps/token.go +++ b/coderd/workspaceapps/token.go @@ -11,6 +11,7 @@ import ( "github.com/coder/coder/v2/coderd/cryptokeys" "github.com/coder/coder/v2/coderd/jwtutils" + "github.com/coder/coder/v2/coderd/workspaceapps/cors" "github.com/coder/coder/v2/codersdk" ) @@ -22,10 +23,11 @@ type SignedToken struct { // Request details. Request `json:"request"` - UserID uuid.UUID `json:"user_id"` - WorkspaceID uuid.UUID `json:"workspace_id"` - AgentID uuid.UUID `json:"agent_id"` - AppURL string `json:"app_url"` + UserID uuid.UUID `json:"user_id"` + WorkspaceID uuid.UUID `json:"workspace_id"` + AgentID uuid.UUID `json:"agent_id"` + AppURL string `json:"app_url"` + CORSBehavior cors.AppCORSBehavior `json:"cors_behavior"` } // MatchesRequest returns true if the token matches the request. Any token that diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index 0ff1660eaf807..1d7b26c4fe423 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -74,16 +74,17 @@ type agentAppAttributes struct { Slug string `mapstructure:"slug"` DisplayName string `mapstructure:"display_name"` // Name is deprecated in favor of DisplayName. - Name string `mapstructure:"name"` - Icon string `mapstructure:"icon"` - URL string `mapstructure:"url"` - External bool `mapstructure:"external"` - Command string `mapstructure:"command"` - Share string `mapstructure:"share"` - Subdomain bool `mapstructure:"subdomain"` - Healthcheck []appHealthcheckAttributes `mapstructure:"healthcheck"` - Order int64 `mapstructure:"order"` - Hidden bool `mapstructure:"hidden"` + Name string `mapstructure:"name"` + Icon string `mapstructure:"icon"` + URL string `mapstructure:"url"` + External bool `mapstructure:"external"` + Command string `mapstructure:"command"` + Share string `mapstructure:"share"` + CORSBehavior string `mapstructure:"cors_behavior"` + Subdomain bool `mapstructure:"subdomain"` + Healthcheck []appHealthcheckAttributes `mapstructure:"healthcheck"` + Order int64 `mapstructure:"order"` + Hidden bool `mapstructure:"hidden"` } type agentEnvAttributes struct { @@ -432,6 +433,16 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s sharingLevel = proto.AppSharingLevel_PUBLIC } + var corsBehavior proto.AppCORSBehavior + switch strings.ToLower(attrs.CORSBehavior) { + case "simple": + corsBehavior = proto.AppCORSBehavior_SIMPLE + case "passthru": + corsBehavior = proto.AppCORSBehavior_PASSTHRU + default: + return nil, xerrors.Errorf("invalid app CORS behavior %q", attrs.CORSBehavior) + } + for _, agents := range resourceAgents { for _, agent := range agents { // Find agents with the matching ID and associate them! @@ -449,6 +460,7 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s Icon: attrs.Icon, Subdomain: attrs.Subdomain, SharingLevel: sharingLevel, + CorsBehavior: corsBehavior, Healthcheck: healthcheck, Order: attrs.Order, Hidden: attrs.Hidden, diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index 026939d17120e..70922e445b343 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.30.0 -// protoc v4.23.3 +// protoc v5.28.2 // source: provisionersdk/proto/provisioner.proto package proto @@ -126,6 +126,52 @@ func (AppSharingLevel) EnumDescriptor() ([]byte, []int) { return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{1} } +type AppCORSBehavior int32 + +const ( + AppCORSBehavior_SIMPLE AppCORSBehavior = 0 + AppCORSBehavior_PASSTHRU AppCORSBehavior = 1 +) + +// Enum value maps for AppCORSBehavior. +var ( + AppCORSBehavior_name = map[int32]string{ + 0: "SIMPLE", + 1: "PASSTHRU", + } + AppCORSBehavior_value = map[string]int32{ + "SIMPLE": 0, + "PASSTHRU": 1, + } +) + +func (x AppCORSBehavior) Enum() *AppCORSBehavior { + p := new(AppCORSBehavior) + *p = x + return p +} + +func (x AppCORSBehavior) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AppCORSBehavior) Descriptor() protoreflect.EnumDescriptor { + return file_provisionersdk_proto_provisioner_proto_enumTypes[2].Descriptor() +} + +func (AppCORSBehavior) Type() protoreflect.EnumType { + return &file_provisionersdk_proto_provisioner_proto_enumTypes[2] +} + +func (x AppCORSBehavior) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AppCORSBehavior.Descriptor instead. +func (AppCORSBehavior) EnumDescriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{2} +} + // WorkspaceTransition is the desired outcome of a build type WorkspaceTransition int32 @@ -160,11 +206,11 @@ func (x WorkspaceTransition) String() string { } func (WorkspaceTransition) Descriptor() protoreflect.EnumDescriptor { - return file_provisionersdk_proto_provisioner_proto_enumTypes[2].Descriptor() + return file_provisionersdk_proto_provisioner_proto_enumTypes[3].Descriptor() } func (WorkspaceTransition) Type() protoreflect.EnumType { - return &file_provisionersdk_proto_provisioner_proto_enumTypes[2] + return &file_provisionersdk_proto_provisioner_proto_enumTypes[3] } func (x WorkspaceTransition) Number() protoreflect.EnumNumber { @@ -173,7 +219,7 @@ func (x WorkspaceTransition) Number() protoreflect.EnumNumber { // Deprecated: Use WorkspaceTransition.Descriptor instead. func (WorkspaceTransition) EnumDescriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{2} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{3} } type TimingState int32 @@ -209,11 +255,11 @@ func (x TimingState) String() string { } func (TimingState) Descriptor() protoreflect.EnumDescriptor { - return file_provisionersdk_proto_provisioner_proto_enumTypes[3].Descriptor() + return file_provisionersdk_proto_provisioner_proto_enumTypes[4].Descriptor() } func (TimingState) Type() protoreflect.EnumType { - return &file_provisionersdk_proto_provisioner_proto_enumTypes[3] + return &file_provisionersdk_proto_provisioner_proto_enumTypes[4] } func (x TimingState) Number() protoreflect.EnumNumber { @@ -222,7 +268,7 @@ func (x TimingState) Number() protoreflect.EnumNumber { // Deprecated: Use TimingState.Descriptor instead. func (TimingState) EnumDescriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{3} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{4} } // Empty indicates a successful request/response. @@ -1394,6 +1440,7 @@ type App struct { Subdomain bool `protobuf:"varint,6,opt,name=subdomain,proto3" json:"subdomain,omitempty"` Healthcheck *Healthcheck `protobuf:"bytes,7,opt,name=healthcheck,proto3" json:"healthcheck,omitempty"` SharingLevel AppSharingLevel `protobuf:"varint,8,opt,name=sharing_level,json=sharingLevel,proto3,enum=provisioner.AppSharingLevel" json:"sharing_level,omitempty"` + CorsBehavior AppCORSBehavior `protobuf:"varint,12,opt,name=cors_behavior,json=corsBehavior,proto3,enum=provisioner.AppCORSBehavior" json:"cors_behavior,omitempty"` External bool `protobuf:"varint,9,opt,name=external,proto3" json:"external,omitempty"` Order int64 `protobuf:"varint,10,opt,name=order,proto3" json:"order,omitempty"` Hidden bool `protobuf:"varint,11,opt,name=hidden,proto3" json:"hidden,omitempty"` @@ -1487,6 +1534,13 @@ func (x *App) GetSharingLevel() AppSharingLevel { return AppSharingLevel_OWNER } +func (x *App) GetCorsBehavior() AppCORSBehavior { + if x != nil { + return x.CorsBehavior + } + return AppCORSBehavior_SIMPLE +} + func (x *App) GetExternal() bool { if x != nil { return x.External @@ -3116,7 +3170,7 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x6f, 0x67, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x50, 0x61, 0x74, 0x68, 0x22, 0xe3, 0x02, 0x0a, 0x03, 0x41, 0x70, + 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x50, 0x61, 0x74, 0x68, 0x22, 0xa6, 0x03, 0x0a, 0x03, 0x41, 0x70, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, @@ -3134,192 +3188,169 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x76, 0x65, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x0c, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, - 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x69, 0x64, 0x64, 0x65, - 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x22, - 0x59, 0x0a, 0x0b, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x10, - 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, - 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x1c, 0x0a, 0x09, - 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x22, 0x92, 0x03, 0x0a, 0x08, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, - 0x2a, 0x0a, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x67, - 0x65, 0x6e, 0x74, 0x52, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x3a, 0x0a, 0x08, 0x6d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x69, 0x64, 0x65, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x68, 0x69, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x69, - 0x63, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x12, - 0x23, 0x0a, 0x0d, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, - 0x54, 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x5f, 0x63, 0x6f, - 0x73, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x43, - 0x6f, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5f, 0x70, 0x61, - 0x74, 0x68, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, - 0x50, 0x61, 0x74, 0x68, 0x1a, 0x69, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, - 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, - 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x73, 0x5f, 0x6e, 0x75, 0x6c, - 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x4e, 0x75, 0x6c, 0x6c, 0x22, - 0x4c, 0x0a, 0x06, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0xac, 0x07, - 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x53, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x65, 0x72, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, - 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e, - 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, - 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, - 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12, 0x32, 0x0a, - 0x15, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, - 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x45, 0x6d, 0x61, 0x69, - 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, - 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, - 0x74, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x12, 0x48, 0x0a, 0x21, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, - 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1d, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x4f, 0x69, 0x64, 0x63, - 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x41, 0x0a, 0x1d, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x73, - 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0b, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, - 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1f, - 0x0a, 0x0b, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x64, 0x12, - 0x30, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, - 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, - 0x65, 0x12, 0x34, 0x0a, 0x16, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, - 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x0e, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, - 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x42, 0x0a, 0x1e, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x73, 0x73, 0x68, 0x5f, 0x70, - 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x53, - 0x73, 0x68, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x44, 0x0a, 0x1f, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x73, - 0x73, 0x68, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x10, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, - 0x77, 0x6e, 0x65, 0x72, 0x53, 0x73, 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, - 0x79, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62, - 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x12, - 0x3b, 0x0a, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, - 0x65, 0x72, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x12, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, - 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x22, 0x8a, 0x01, 0x0a, - 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x17, 0x74, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, - 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x15, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, - 0x74, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x12, - 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x65, 0x72, 0x5f, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x0e, 0x0a, 0x0c, 0x50, 0x61, 0x72, - 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xa3, 0x02, 0x0a, 0x0d, 0x50, 0x61, - 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x12, 0x4c, 0x0a, 0x12, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x61, - 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x65, 0x6d, 0x70, - 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x11, 0x74, 0x65, - 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x12, - 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x06, 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, 0x12, 0x54, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x2d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, - 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x2e, 0x57, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0d, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 0x1a, 0x40, 0x0a, - 0x12, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x41, 0x0a, 0x0d, 0x63, 0x6f, 0x72, 0x73, 0x5f, 0x62, 0x65, + 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x43, 0x4f, + 0x52, 0x53, 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x52, 0x0c, 0x63, 0x6f, 0x72, 0x73, + 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x78, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x65, 0x78, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x69, + 0x64, 0x64, 0x65, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x68, 0x69, 0x64, 0x64, + 0x65, 0x6e, 0x22, 0x59, 0x0a, 0x0b, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, + 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x75, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, + 0x1c, 0x0a, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x22, 0x92, 0x03, + 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x3a, + 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x69, + 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x68, 0x69, 0x64, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, + 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x69, 0x6e, 0x73, 0x74, 0x61, + 0x6e, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x61, 0x69, 0x6c, 0x79, + 0x5f, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x64, 0x61, 0x69, + 0x6c, 0x79, 0x43, 0x6f, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, + 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x6f, 0x64, + 0x75, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x1a, 0x69, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, - 0xb5, 0x02, 0x0a, 0x0b, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x12, 0x53, 0x0a, 0x15, 0x72, 0x69, 0x63, 0x68, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, - 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x52, 0x13, 0x72, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, - 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x76, 0x61, 0x72, 0x69, 0x61, - 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, - 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x76, 0x61, - 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x59, 0x0a, 0x17, - 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x78, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x52, 0x15, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x22, 0xd6, 0x02, 0x0a, 0x0c, 0x50, 0x6c, 0x61, 0x6e, - 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, - 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, - 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, - 0x74, 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, - 0x61, 0x0a, 0x17, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, + 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, + 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x73, 0x5f, + 0x6e, 0x75, 0x6c, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x4e, 0x75, + 0x6c, 0x6c, 0x22, 0x4c, 0x0a, 0x06, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x22, 0xac, 0x07, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, + 0x09, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x53, 0x0a, 0x14, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x12, + 0x21, 0x0a, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, + 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x49, 0x64, + 0x12, 0x32, 0x0a, 0x15, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, + 0x6e, 0x65, 0x72, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x45, + 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x65, 0x6d, + 0x70, 0x6c, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x74, 0x65, 0x6d, + 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x48, 0x0a, 0x21, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x1d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x4f, + 0x69, 0x64, 0x63, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x41, + 0x0a, 0x1d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, + 0x72, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x64, + 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, + 0x49, 0x64, 0x12, 0x30, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, + 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x34, 0x0a, 0x16, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x0e, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, + 0x77, 0x6e, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x42, 0x0a, 0x1e, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x73, 0x73, + 0x68, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0f, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, + 0x65, 0x72, 0x53, 0x73, 0x68, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x44, + 0x0a, 0x1f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, + 0x72, 0x5f, 0x73, 0x73, 0x68, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, + 0x79, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x53, 0x73, 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x4b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, + 0x49, 0x64, 0x12, 0x3b, 0x0a, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, + 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x22, + 0x8a, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x17, 0x74, 0x65, + 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x72, + 0x63, 0x68, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x15, 0x74, 0x65, 0x6d, + 0x70, 0x6c, 0x61, 0x74, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x72, 0x63, 0x68, 0x69, + 0x76, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x5f, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, + 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x0e, 0x0a, 0x0c, + 0x50, 0x61, 0x72, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xa3, 0x02, 0x0a, + 0x0d, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x12, 0x4c, 0x0a, 0x12, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, + 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, + 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, + 0x11, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, + 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x06, 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, 0x12, 0x54, 0x0a, 0x0e, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x2e, 0x57, + 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, + 0x1a, 0x40, 0x0a, 0x12, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x22, 0xb5, 0x02, 0x0a, 0x0b, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x53, 0x0a, 0x15, 0x72, 0x69, 0x63, 0x68, 0x5f, 0x70, 0x61, + 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x13, 0x72, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, + 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x76, 0x61, + 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x0e, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, + 0x59, 0x0a, 0x17, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x29, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, + 0x32, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x15, 0x65, 0x78, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x06, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, - 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x07, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, - 0x22, 0x41, 0x0a, 0x0c, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x22, 0xbe, 0x02, 0x0a, 0x0d, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x43, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x03, + 0x64, 0x65, 0x72, 0x52, 0x15, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, + 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x22, 0xd6, 0x02, 0x0a, 0x0c, 0x50, + 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, - 0x74, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x61, 0x0a, 0x17, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, - 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, + 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x15, @@ -3327,62 +3358,92 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x74, 0x69, 0x6d, - 0x69, 0x6e, 0x67, 0x73, 0x22, 0xfa, 0x01, 0x0a, 0x06, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x12, - 0x30, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, - 0x74, 0x12, 0x2c, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x12, - 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, - 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, - 0x74, 0x61, 0x67, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x67, - 0x65, 0x12, 0x2e, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, - 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, - 0x65, 0x22, 0x0f, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x22, 0x8c, 0x02, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, - 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x31, 0x0a, - 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, - 0x12, 0x2e, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x6c, 0x61, - 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, - 0x12, 0x31, 0x0a, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, - 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, - 0x70, 0x6c, 0x79, 0x12, 0x34, 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, - 0x00, 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, - 0x65, 0x22, 0xd1, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, - 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, - 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x32, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, - 0x00, 0x52, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, - 0x65, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x32, 0x0a, 0x05, 0x61, 0x70, 0x70, - 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x43, 0x6f, 0x6d, 0x70, - 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x42, 0x06, 0x0a, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, - 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, - 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, - 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x45, - 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x3b, 0x0a, 0x0f, 0x41, 0x70, 0x70, 0x53, 0x68, 0x61, - 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x57, 0x4e, - 0x45, 0x52, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, - 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, - 0x43, 0x10, 0x02, 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x69, 0x6e, 0x67, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x18, + 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x07, 0x6d, 0x6f, 0x64, 0x75, + 0x6c, 0x65, 0x73, 0x22, 0x41, 0x0a, 0x0c, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xbe, 0x02, 0x0a, 0x0d, 0x41, 0x70, 0x70, 0x6c, 0x79, + 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x61, 0x72, + 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, + 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, + 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x61, 0x0a, 0x17, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, + 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, + 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x52, 0x15, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x69, + 0x6e, 0x67, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x52, 0x07, + 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x73, 0x22, 0xfa, 0x01, 0x0a, 0x06, 0x54, 0x69, 0x6d, 0x69, + 0x6e, 0x67, 0x12, 0x30, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x73, + 0x74, 0x61, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x03, 0x65, + 0x6e, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, + 0x74, 0x61, 0x67, 0x65, 0x12, 0x2e, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x22, 0x0f, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x8c, 0x02, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x2d, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x31, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, + 0x72, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x05, 0x70, 0x61, + 0x72, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, + 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x04, 0x70, + 0x6c, 0x61, 0x6e, 0x12, 0x31, 0x0a, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, + 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x12, 0x34, 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x48, 0x00, 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x06, 0x0a, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x22, 0xd1, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, + 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, + 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x32, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x04, 0x70, + 0x6c, 0x61, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, + 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x32, 0x0a, 0x05, + 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x43, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, + 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, + 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x00, 0x12, + 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, + 0x46, 0x4f, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x03, 0x12, 0x09, + 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x3b, 0x0a, 0x0f, 0x41, 0x70, 0x70, + 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, + 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48, 0x45, + 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, + 0x42, 0x4c, 0x49, 0x43, 0x10, 0x02, 0x2a, 0x2b, 0x0a, 0x0f, 0x41, 0x70, 0x70, 0x43, 0x4f, 0x52, + 0x53, 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x49, 0x4d, + 0x50, 0x4c, 0x45, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x50, 0x41, 0x53, 0x53, 0x54, 0x48, 0x52, + 0x55, 0x10, 0x01, 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x02, 0x2a, 0x35, 0x0a, 0x0b, @@ -3412,98 +3473,100 @@ func file_provisionersdk_proto_provisioner_proto_rawDescGZIP() []byte { return file_provisionersdk_proto_provisioner_proto_rawDescData } -var file_provisionersdk_proto_provisioner_proto_enumTypes = make([]protoimpl.EnumInfo, 4) +var file_provisionersdk_proto_provisioner_proto_enumTypes = make([]protoimpl.EnumInfo, 5) var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 34) var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (LogLevel)(0), // 0: provisioner.LogLevel (AppSharingLevel)(0), // 1: provisioner.AppSharingLevel - (WorkspaceTransition)(0), // 2: provisioner.WorkspaceTransition - (TimingState)(0), // 3: provisioner.TimingState - (*Empty)(nil), // 4: provisioner.Empty - (*TemplateVariable)(nil), // 5: provisioner.TemplateVariable - (*RichParameterOption)(nil), // 6: provisioner.RichParameterOption - (*RichParameter)(nil), // 7: provisioner.RichParameter - (*RichParameterValue)(nil), // 8: provisioner.RichParameterValue - (*VariableValue)(nil), // 9: provisioner.VariableValue - (*Log)(nil), // 10: provisioner.Log - (*InstanceIdentityAuth)(nil), // 11: provisioner.InstanceIdentityAuth - (*ExternalAuthProviderResource)(nil), // 12: provisioner.ExternalAuthProviderResource - (*ExternalAuthProvider)(nil), // 13: provisioner.ExternalAuthProvider - (*Agent)(nil), // 14: provisioner.Agent - (*DisplayApps)(nil), // 15: provisioner.DisplayApps - (*Env)(nil), // 16: provisioner.Env - (*Script)(nil), // 17: provisioner.Script - (*App)(nil), // 18: provisioner.App - (*Healthcheck)(nil), // 19: provisioner.Healthcheck - (*Resource)(nil), // 20: provisioner.Resource - (*Module)(nil), // 21: provisioner.Module - (*Metadata)(nil), // 22: provisioner.Metadata - (*Config)(nil), // 23: provisioner.Config - (*ParseRequest)(nil), // 24: provisioner.ParseRequest - (*ParseComplete)(nil), // 25: provisioner.ParseComplete - (*PlanRequest)(nil), // 26: provisioner.PlanRequest - (*PlanComplete)(nil), // 27: provisioner.PlanComplete - (*ApplyRequest)(nil), // 28: provisioner.ApplyRequest - (*ApplyComplete)(nil), // 29: provisioner.ApplyComplete - (*Timing)(nil), // 30: provisioner.Timing - (*CancelRequest)(nil), // 31: provisioner.CancelRequest - (*Request)(nil), // 32: provisioner.Request - (*Response)(nil), // 33: provisioner.Response - (*Agent_Metadata)(nil), // 34: provisioner.Agent.Metadata - nil, // 35: provisioner.Agent.EnvEntry - (*Resource_Metadata)(nil), // 36: provisioner.Resource.Metadata - nil, // 37: provisioner.ParseComplete.WorkspaceTagsEntry - (*timestamppb.Timestamp)(nil), // 38: google.protobuf.Timestamp + (AppCORSBehavior)(0), // 2: provisioner.AppCORSBehavior + (WorkspaceTransition)(0), // 3: provisioner.WorkspaceTransition + (TimingState)(0), // 4: provisioner.TimingState + (*Empty)(nil), // 5: provisioner.Empty + (*TemplateVariable)(nil), // 6: provisioner.TemplateVariable + (*RichParameterOption)(nil), // 7: provisioner.RichParameterOption + (*RichParameter)(nil), // 8: provisioner.RichParameter + (*RichParameterValue)(nil), // 9: provisioner.RichParameterValue + (*VariableValue)(nil), // 10: provisioner.VariableValue + (*Log)(nil), // 11: provisioner.Log + (*InstanceIdentityAuth)(nil), // 12: provisioner.InstanceIdentityAuth + (*ExternalAuthProviderResource)(nil), // 13: provisioner.ExternalAuthProviderResource + (*ExternalAuthProvider)(nil), // 14: provisioner.ExternalAuthProvider + (*Agent)(nil), // 15: provisioner.Agent + (*DisplayApps)(nil), // 16: provisioner.DisplayApps + (*Env)(nil), // 17: provisioner.Env + (*Script)(nil), // 18: provisioner.Script + (*App)(nil), // 19: provisioner.App + (*Healthcheck)(nil), // 20: provisioner.Healthcheck + (*Resource)(nil), // 21: provisioner.Resource + (*Module)(nil), // 22: provisioner.Module + (*Metadata)(nil), // 23: provisioner.Metadata + (*Config)(nil), // 24: provisioner.Config + (*ParseRequest)(nil), // 25: provisioner.ParseRequest + (*ParseComplete)(nil), // 26: provisioner.ParseComplete + (*PlanRequest)(nil), // 27: provisioner.PlanRequest + (*PlanComplete)(nil), // 28: provisioner.PlanComplete + (*ApplyRequest)(nil), // 29: provisioner.ApplyRequest + (*ApplyComplete)(nil), // 30: provisioner.ApplyComplete + (*Timing)(nil), // 31: provisioner.Timing + (*CancelRequest)(nil), // 32: provisioner.CancelRequest + (*Request)(nil), // 33: provisioner.Request + (*Response)(nil), // 34: provisioner.Response + (*Agent_Metadata)(nil), // 35: provisioner.Agent.Metadata + nil, // 36: provisioner.Agent.EnvEntry + (*Resource_Metadata)(nil), // 37: provisioner.Resource.Metadata + nil, // 38: provisioner.ParseComplete.WorkspaceTagsEntry + (*timestamppb.Timestamp)(nil), // 39: google.protobuf.Timestamp } var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{ - 6, // 0: provisioner.RichParameter.options:type_name -> provisioner.RichParameterOption + 7, // 0: provisioner.RichParameter.options:type_name -> provisioner.RichParameterOption 0, // 1: provisioner.Log.level:type_name -> provisioner.LogLevel - 35, // 2: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry - 18, // 3: provisioner.Agent.apps:type_name -> provisioner.App - 34, // 4: provisioner.Agent.metadata:type_name -> provisioner.Agent.Metadata - 15, // 5: provisioner.Agent.display_apps:type_name -> provisioner.DisplayApps - 17, // 6: provisioner.Agent.scripts:type_name -> provisioner.Script - 16, // 7: provisioner.Agent.extra_envs:type_name -> provisioner.Env - 19, // 8: provisioner.App.healthcheck:type_name -> provisioner.Healthcheck + 36, // 2: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry + 19, // 3: provisioner.Agent.apps:type_name -> provisioner.App + 35, // 4: provisioner.Agent.metadata:type_name -> provisioner.Agent.Metadata + 16, // 5: provisioner.Agent.display_apps:type_name -> provisioner.DisplayApps + 18, // 6: provisioner.Agent.scripts:type_name -> provisioner.Script + 17, // 7: provisioner.Agent.extra_envs:type_name -> provisioner.Env + 20, // 8: provisioner.App.healthcheck:type_name -> provisioner.Healthcheck 1, // 9: provisioner.App.sharing_level:type_name -> provisioner.AppSharingLevel - 14, // 10: provisioner.Resource.agents:type_name -> provisioner.Agent - 36, // 11: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata - 2, // 12: provisioner.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition - 5, // 13: provisioner.ParseComplete.template_variables:type_name -> provisioner.TemplateVariable - 37, // 14: provisioner.ParseComplete.workspace_tags:type_name -> provisioner.ParseComplete.WorkspaceTagsEntry - 22, // 15: provisioner.PlanRequest.metadata:type_name -> provisioner.Metadata - 8, // 16: provisioner.PlanRequest.rich_parameter_values:type_name -> provisioner.RichParameterValue - 9, // 17: provisioner.PlanRequest.variable_values:type_name -> provisioner.VariableValue - 13, // 18: provisioner.PlanRequest.external_auth_providers:type_name -> provisioner.ExternalAuthProvider - 20, // 19: provisioner.PlanComplete.resources:type_name -> provisioner.Resource - 7, // 20: provisioner.PlanComplete.parameters:type_name -> provisioner.RichParameter - 12, // 21: provisioner.PlanComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource - 30, // 22: provisioner.PlanComplete.timings:type_name -> provisioner.Timing - 21, // 23: provisioner.PlanComplete.modules:type_name -> provisioner.Module - 22, // 24: provisioner.ApplyRequest.metadata:type_name -> provisioner.Metadata - 20, // 25: provisioner.ApplyComplete.resources:type_name -> provisioner.Resource - 7, // 26: provisioner.ApplyComplete.parameters:type_name -> provisioner.RichParameter - 12, // 27: provisioner.ApplyComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource - 30, // 28: provisioner.ApplyComplete.timings:type_name -> provisioner.Timing - 38, // 29: provisioner.Timing.start:type_name -> google.protobuf.Timestamp - 38, // 30: provisioner.Timing.end:type_name -> google.protobuf.Timestamp - 3, // 31: provisioner.Timing.state:type_name -> provisioner.TimingState - 23, // 32: provisioner.Request.config:type_name -> provisioner.Config - 24, // 33: provisioner.Request.parse:type_name -> provisioner.ParseRequest - 26, // 34: provisioner.Request.plan:type_name -> provisioner.PlanRequest - 28, // 35: provisioner.Request.apply:type_name -> provisioner.ApplyRequest - 31, // 36: provisioner.Request.cancel:type_name -> provisioner.CancelRequest - 10, // 37: provisioner.Response.log:type_name -> provisioner.Log - 25, // 38: provisioner.Response.parse:type_name -> provisioner.ParseComplete - 27, // 39: provisioner.Response.plan:type_name -> provisioner.PlanComplete - 29, // 40: provisioner.Response.apply:type_name -> provisioner.ApplyComplete - 32, // 41: provisioner.Provisioner.Session:input_type -> provisioner.Request - 33, // 42: provisioner.Provisioner.Session:output_type -> provisioner.Response - 42, // [42:43] is the sub-list for method output_type - 41, // [41:42] is the sub-list for method input_type - 41, // [41:41] is the sub-list for extension type_name - 41, // [41:41] is the sub-list for extension extendee - 0, // [0:41] is the sub-list for field type_name + 2, // 10: provisioner.App.cors_behavior:type_name -> provisioner.AppCORSBehavior + 15, // 11: provisioner.Resource.agents:type_name -> provisioner.Agent + 37, // 12: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata + 3, // 13: provisioner.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition + 6, // 14: provisioner.ParseComplete.template_variables:type_name -> provisioner.TemplateVariable + 38, // 15: provisioner.ParseComplete.workspace_tags:type_name -> provisioner.ParseComplete.WorkspaceTagsEntry + 23, // 16: provisioner.PlanRequest.metadata:type_name -> provisioner.Metadata + 9, // 17: provisioner.PlanRequest.rich_parameter_values:type_name -> provisioner.RichParameterValue + 10, // 18: provisioner.PlanRequest.variable_values:type_name -> provisioner.VariableValue + 14, // 19: provisioner.PlanRequest.external_auth_providers:type_name -> provisioner.ExternalAuthProvider + 21, // 20: provisioner.PlanComplete.resources:type_name -> provisioner.Resource + 8, // 21: provisioner.PlanComplete.parameters:type_name -> provisioner.RichParameter + 13, // 22: provisioner.PlanComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource + 31, // 23: provisioner.PlanComplete.timings:type_name -> provisioner.Timing + 22, // 24: provisioner.PlanComplete.modules:type_name -> provisioner.Module + 23, // 25: provisioner.ApplyRequest.metadata:type_name -> provisioner.Metadata + 21, // 26: provisioner.ApplyComplete.resources:type_name -> provisioner.Resource + 8, // 27: provisioner.ApplyComplete.parameters:type_name -> provisioner.RichParameter + 13, // 28: provisioner.ApplyComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource + 31, // 29: provisioner.ApplyComplete.timings:type_name -> provisioner.Timing + 39, // 30: provisioner.Timing.start:type_name -> google.protobuf.Timestamp + 39, // 31: provisioner.Timing.end:type_name -> google.protobuf.Timestamp + 4, // 32: provisioner.Timing.state:type_name -> provisioner.TimingState + 24, // 33: provisioner.Request.config:type_name -> provisioner.Config + 25, // 34: provisioner.Request.parse:type_name -> provisioner.ParseRequest + 27, // 35: provisioner.Request.plan:type_name -> provisioner.PlanRequest + 29, // 36: provisioner.Request.apply:type_name -> provisioner.ApplyRequest + 32, // 37: provisioner.Request.cancel:type_name -> provisioner.CancelRequest + 11, // 38: provisioner.Response.log:type_name -> provisioner.Log + 26, // 39: provisioner.Response.parse:type_name -> provisioner.ParseComplete + 28, // 40: provisioner.Response.plan:type_name -> provisioner.PlanComplete + 30, // 41: provisioner.Response.apply:type_name -> provisioner.ApplyComplete + 33, // 42: provisioner.Provisioner.Session:input_type -> provisioner.Request + 34, // 43: provisioner.Provisioner.Session:output_type -> provisioner.Response + 43, // [43:44] is the sub-list for method output_type + 42, // [42:43] is the sub-list for method input_type + 42, // [42:42] is the sub-list for extension type_name + 42, // [42:42] is the sub-list for extension extendee + 0, // [0:42] is the sub-list for field type_name } func init() { file_provisionersdk_proto_provisioner_proto_init() } @@ -3920,7 +3983,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_provisionersdk_proto_provisioner_proto_rawDesc, - NumEnums: 4, + NumEnums: 5, NumMessages: 34, NumExtensions: 0, NumServices: 1, diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index 1e1de886c7d0a..f913434731875 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -137,6 +137,11 @@ enum AppSharingLevel { PUBLIC = 2; } +enum AppCORSBehavior { + SIMPLE = 0; + PASSTHRU = 1; +} + message DisplayApps { bool vscode = 1; bool vscode_insiders = 2; @@ -175,6 +180,7 @@ message App { bool subdomain = 6; Healthcheck healthcheck = 7; AppSharingLevel sharing_level = 8; + AppCORSBehavior cors_behavior = 12; bool external = 9; int64 order = 10; bool hidden = 11; diff --git a/provisionersdk/proto/provisioner_drpc.pb.go b/provisionersdk/proto/provisioner_drpc.pb.go index de310e779dcaa..c9c54002439c2 100644 --- a/provisionersdk/proto/provisioner_drpc.pb.go +++ b/provisionersdk/proto/provisioner_drpc.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-drpc. DO NOT EDIT. -// protoc-gen-go-drpc version: v0.0.33 +// protoc-gen-go-drpc version: (devel) // source: provisionersdk/proto/provisioner.proto package proto diff --git a/site/e2e/provisionerGenerated.ts b/site/e2e/provisionerGenerated.ts index 9f238b0e47212..db7419bdefd52 100644 --- a/site/e2e/provisionerGenerated.ts +++ b/site/e2e/provisionerGenerated.ts @@ -22,6 +22,12 @@ export enum AppSharingLevel { UNRECOGNIZED = -1, } +export enum AppCORSBehavior { + SIMPLE = 0, + PASSTHRU = 1, + UNRECOGNIZED = -1, +} + /** WorkspaceTransition is the desired outcome of a build */ export enum WorkspaceTransition { START = 0, @@ -193,6 +199,7 @@ export interface App { subdomain: boolean; healthcheck: Healthcheck | undefined; sharingLevel: AppSharingLevel; + corsBehavior: AppCORSBehavior; external: boolean; order: number; hidden: boolean; @@ -746,6 +753,9 @@ export const App = { if (message.sharingLevel !== 0) { writer.uint32(64).int32(message.sharingLevel); } + if (message.corsBehavior !== 0) { + writer.uint32(96).int32(message.corsBehavior); + } if (message.external === true) { writer.uint32(72).bool(message.external); } From 3c0e33aa997d53c40e8138bb8504892d248beb63 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Tue, 26 Nov 2024 09:41:43 +0000 Subject: [PATCH 02/11] Rename CorsBehavior to CORSBehavior Signed-off-by: Danny Kopping --- coderd/database/dbmem/dbmem.go | 2 +- coderd/database/models.go | 2 +- coderd/database/queries.sql.go | 14 +++++++------- coderd/database/sqlc.yaml | 1 + coderd/provisionerdserver/provisionerdserver.go | 2 +- coderd/workspaceapps/request.go | 2 +- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 50d8376d6db42..cfab07333a6c2 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -8174,7 +8174,7 @@ func (q *FakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertW Health: arg.Health, Hidden: arg.Hidden, DisplayOrder: arg.DisplayOrder, - CorsBehavior: arg.CorsBehavior, + CORSBehavior: arg.CORSBehavior, } q.workspaceApps = append(q.workspaceApps, workspaceApp) return workspaceApp, nil diff --git a/coderd/database/models.go b/coderd/database/models.go index 28232ef1cfbbb..821116d0da6cc 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -3141,7 +3141,7 @@ type WorkspaceApp struct { DisplayOrder int32 `db:"display_order" json:"display_order"` // Determines if the app is not shown in user interfaces. Hidden bool `db:"hidden" json:"hidden"` - CorsBehavior AppCORSBehavior `db:"cors_behavior" json:"cors_behavior"` + CORSBehavior AppCORSBehavior `db:"cors_behavior" json:"cors_behavior"` } // A record of workspace app usage statistics diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index bb4165230548a..7fee1e0d2ebd2 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -13099,7 +13099,7 @@ func (q *sqlQuerier) GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg Ge &i.External, &i.DisplayOrder, &i.Hidden, - &i.CorsBehavior, + &i.CORSBehavior, ) return i, err } @@ -13135,7 +13135,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid &i.External, &i.DisplayOrder, &i.Hidden, - &i.CorsBehavior, + &i.CORSBehavior, ); err != nil { return nil, err } @@ -13181,7 +13181,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid. &i.External, &i.DisplayOrder, &i.Hidden, - &i.CorsBehavior, + &i.CORSBehavior, ); err != nil { return nil, err } @@ -13227,7 +13227,7 @@ func (q *sqlQuerier) GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt &i.External, &i.DisplayOrder, &i.Hidden, - &i.CorsBehavior, + &i.CORSBehavior, ); err != nil { return nil, err } @@ -13280,7 +13280,7 @@ type InsertWorkspaceAppParams struct { External bool `db:"external" json:"external"` Subdomain bool `db:"subdomain" json:"subdomain"` SharingLevel AppSharingLevel `db:"sharing_level" json:"sharing_level"` - CorsBehavior AppCORSBehavior `db:"cors_behavior" json:"cors_behavior"` + CORSBehavior AppCORSBehavior `db:"cors_behavior" json:"cors_behavior"` HealthcheckUrl string `db:"healthcheck_url" json:"healthcheck_url"` HealthcheckInterval int32 `db:"healthcheck_interval" json:"healthcheck_interval"` HealthcheckThreshold int32 `db:"healthcheck_threshold" json:"healthcheck_threshold"` @@ -13302,7 +13302,7 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace arg.External, arg.Subdomain, arg.SharingLevel, - arg.CorsBehavior, + arg.CORSBehavior, arg.HealthcheckUrl, arg.HealthcheckInterval, arg.HealthcheckThreshold, @@ -13329,7 +13329,7 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace &i.External, &i.DisplayOrder, &i.Hidden, - &i.CorsBehavior, + &i.CORSBehavior, ) return i, err } diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml index 105fceade4f1c..1753da4cbd0ee 100644 --- a/coderd/database/sqlc.yaml +++ b/coderd/database/sqlc.yaml @@ -147,6 +147,7 @@ sql: crypto_key_feature_workspace_apps_api_key: CryptoKeyFeatureWorkspaceAppsAPIKey crypto_key_feature_oidc_convert: CryptoKeyFeatureOIDCConvert app_cors_behavior: AppCORSBehavior + cors_behavior: CORSBehavior rules: - name: do-not-use-public-schema-in-queries message: "do not use public schema in queries" diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 734dd3279e520..d057ff623dba5 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -2015,7 +2015,7 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. External: app.External, Subdomain: app.Subdomain, SharingLevel: sharingLevel, - CorsBehavior: corsBehavior, + CORSBehavior: corsBehavior, HealthcheckUrl: app.Healthcheck.Url, HealthcheckInterval: app.Healthcheck.Interval, HealthcheckThreshold: app.Healthcheck.Threshold, diff --git a/coderd/workspaceapps/request.go b/coderd/workspaceapps/request.go index 8304bf8bd9772..b700e7d4a54cf 100644 --- a/coderd/workspaceapps/request.go +++ b/coderd/workspaceapps/request.go @@ -372,7 +372,7 @@ func (r Request) getDatabase(ctx context.Context, db database.Store) (*databaseR appSharingLevel = database.AppSharingLevelOwner } appURL = app.Url.String - appCORSBehavior = app.CorsBehavior + appCORSBehavior = app.CORSBehavior break } } From a4d3c8d29fe3ebff6dc04edfeb04e8e39ed0a519 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 27 Nov 2024 07:37:57 +0000 Subject: [PATCH 03/11] Hard & simplify CORORS handling logic Signed-off-by: Danny Kopping --- coderd/httpmw/cors.go | 6 ------ coderd/workspaceapps/proxy.go | 26 ++++++++++++++++++++++---- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/coderd/httpmw/cors.go b/coderd/httpmw/cors.go index 8d6b946617378..dd69c714379a4 100644 --- a/coderd/httpmw/cors.go +++ b/coderd/httpmw/cors.go @@ -8,7 +8,6 @@ import ( "github.com/go-chi/cors" "github.com/coder/coder/v2/coderd/workspaceapps/appurl" - ws_cors "github.com/coder/coder/v2/coderd/workspaceapps/cors" ) const ( @@ -48,11 +47,6 @@ func Cors(allowAll bool, origins ...string) func(next http.Handler) http.Handler func WorkspaceAppCors(regex *regexp.Regexp, app appurl.ApplicationURL) func(next http.Handler) http.Handler { return cors.Handler(cors.Options{ AllowOriginFunc: func(r *http.Request, rawOrigin string) bool { - // If passthru behavior is set, disable our simplified CORS handling. - if ws_cors.HasBehavior(r.Context(), ws_cors.AppCORSBehaviorPassthru) { - return true - } - origin, err := url.Parse(rawOrigin) if rawOrigin == "" || origin.Host == "" || err != nil { return false diff --git a/coderd/workspaceapps/proxy.go b/coderd/workspaceapps/proxy.go index 979a05d53f13c..da6a075957837 100644 --- a/coderd/workspaceapps/proxy.go +++ b/coderd/workspaceapps/proxy.go @@ -424,8 +424,8 @@ func (s *Server) HandleSubdomain(middlewares ...func(http.Handler) http.Handler) return } - // Use the passed in app middlewares and CORS middleware with the token - mws := chi.Middlewares(append(middlewares, s.injectCORSBehavior(token), httpmw.WorkspaceAppCors(s.HostnameRegex, app))) + // 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) })).ServeHTTP(rw, r.WithContext(ctx)) @@ -433,15 +433,33 @@ func (s *Server) HandleSubdomain(middlewares ...func(http.Handler) http.Handler) } } -func (s *Server) injectCORSBehavior(token *SignedToken) func(http.Handler) http.Handler { +// 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 cors.AppCORSBehavior if token != nil { behavior = token.CORSBehavior } - next.ServeHTTP(rw, r.WithContext(cors.WithBehavior(r.Context(), behavior))) + // 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 cors.AppCORSBehaviorPassthru: + // Bypass the CORS middleware. + next.ServeHTTP(rw, r) + return + default: + // Apply the CORS middleware. + corsHandler.ServeHTTP(rw, r) + } }) } } From 873c3e45f5162374474f6a13af6f0e854a995e83 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 27 Nov 2024 09:08:34 +0000 Subject: [PATCH 04/11] Minor fixes Signed-off-by: Danny Kopping --- coderd/httpmw/cors_test.go | 3 +- .../provisionerdserver/provisionerdserver.go | 1 - coderd/workspaceapps/apptest/apptest.go | 88 ++++++++++++++++--- coderd/workspaceapps/request.go | 2 +- provisioner/terraform/resources.go | 5 +- 5 files changed, 78 insertions(+), 21 deletions(-) diff --git a/coderd/httpmw/cors_test.go b/coderd/httpmw/cors_test.go index 5dc746ccf7edd..57111799ff292 100644 --- a/coderd/httpmw/cors_test.go +++ b/coderd/httpmw/cors_test.go @@ -105,8 +105,7 @@ func TestWorkspaceAppCors(t *testing.T) { r.Header.Set("Access-Control-Request-Method", method) } - // TODO: signed token provider - handler := httpmw.WorkspaceAppCors(nil, regex, test.app)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + handler := httpmw.WorkspaceAppCors(regex, test.app)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { rw.WriteHeader(http.StatusNoContent) })) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index d057ff623dba5..657742ca14fae 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1988,7 +1988,6 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. sharingLevel = database.AppSharingLevelPublic } - // TODO: consider backwards-compat where proto might not contain this field var corsBehavior database.AppCORSBehavior switch app.CorsBehavior { case sdkproto.AppCORSBehavior_PASSTHRU: diff --git a/coderd/workspaceapps/apptest/apptest.go b/coderd/workspaceapps/apptest/apptest.go index f8448d8daad52..d2918e571a98e 100644 --- a/coderd/workspaceapps/apptest/apptest.go +++ b/coderd/workspaceapps/apptest/apptest.go @@ -475,12 +475,20 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { t.Run("CORS", func(t *testing.T) { t.Parallel() - t.Run("AuthenticatedPassthruProtected", func(t *testing.T) { + // 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"}, + } + + t.Run("UnauthenticatedPassthruRejected", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) - appDetails := setupProxyTest(t, nil) + appDetails := setupProxyTest(t, &DeploymentOptions{ + headers: testHeaders, + }) // Given: an unauthenticated client client := appDetails.AppClient(t) @@ -491,7 +499,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { require.NoError(t, err) defer resp.Body.Close() - // Then: the request is redirected to the primary access URL because even though CORS is passthru, + // Then: the request is redirected to login because even though CORS is passthru, // the request must still be authenticated first require.Equal(t, http.StatusSeeOther, resp.StatusCode) gotLocation, err := resp.Location() @@ -505,7 +513,9 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { ctx := testutil.Context(t, testutil.WaitLong) - appDetails := setupProxyTest(t, nil) + appDetails := setupProxyTest(t, &DeploymentOptions{ + headers: testHeaders, + }) userClient, _ := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, appDetails.FirstUser.OrganizationID, rbac.RoleMember()) userAppClient := appDetails.AppClient(t) @@ -516,6 +526,65 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { require.NoError(t, err) defer resp.Body.Close() require.Equal(t, http.StatusOK, resp.StatusCode) + + // Check CORS headers are passed through + require.Equal(t, testHeaders.Get("Access-Control-Allow-Origin"), resp.Header.Get("Access-Control-Allow-Origin")) + require.Equal(t, testHeaders.Get("Access-Control-Allow-Credentials"), resp.Header.Get("Access-Control-Allow-Credentials")) + require.Equal(t, testHeaders.Get("Access-Control-Allow-Methods"), resp.Header.Get("Access-Control-Allow-Methods")) + }) + + t.Run("UnauthenticatedPublicPassthruOK", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + + appDetails := setupProxyTest(t, &DeploymentOptions{ + headers: testHeaders, + }) + + // Given: an unauthenticated client + client := appDetails.AppClient(t) + client.SetSessionToken("") + + // When: a request is made to a public app with passthru CORS behavior + resp, err := requestWithRetries(ctx, t, client, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.PublicCORSPassthru).String(), nil) + require.NoError(t, err) + defer resp.Body.Close() + + // Then: the request succeeds because the app is public + require.Equal(t, http.StatusOK, resp.StatusCode) + + // Check CORS headers are passed through + require.Equal(t, testHeaders.Get("Access-Control-Allow-Origin"), resp.Header.Get("Access-Control-Allow-Origin")) + require.Equal(t, testHeaders.Get("Access-Control-Allow-Credentials"), resp.Header.Get("Access-Control-Allow-Credentials")) + require.Equal(t, testHeaders.Get("Access-Control-Allow-Methods"), resp.Header.Get("Access-Control-Allow-Methods")) + }) + + t.Run("AuthenticatedPublicPassthruOK", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + + appDetails := setupProxyTest(t, &DeploymentOptions{ + headers: testHeaders, + }) + + userClient, _ := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, appDetails.FirstUser.OrganizationID, rbac.RoleMember()) + userAppClient := appDetails.AppClient(t) + userAppClient.SetSessionToken(userClient.SessionToken()) + + // Given: an authenticated client accessing a public app with passthru CORS behavior + resp, err := requestWithRetries(ctx, t, userAppClient, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.PublicCORSPassthru).String(), nil) + require.NoError(t, err) + defer resp.Body.Close() + + // Then: the request succeeds because the app is public + require.Equal(t, http.StatusOK, resp.StatusCode) + + // Check CORS headers are passed through + require.Equal(t, testHeaders.Get("Access-Control-Allow-Origin"), resp.Header.Get("Access-Control-Allow-Origin")) + require.Equal(t, testHeaders.Get("Access-Control-Allow-Credentials"), resp.Header.Get("Access-Control-Allow-Credentials")) + require.Equal(t, testHeaders.Get("Access-Control-Allow-Methods"), resp.Header.Get("Access-Control-Allow-Methods")) }) }) @@ -1842,7 +1911,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { }) // See above test for original implementation. - t.Run("CORSHeadersConditionalStrip", func(t *testing.T) { + t.Run("CORSHeadersConditionallyStripped", func(t *testing.T) { t.Parallel() // Set a bunch of headers which may or may not be stripped, depending on the CORS behavior. @@ -1854,15 +1923,6 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { "Access-Control-Allow-Credentials": []string{"true"}, "Access-Control-Allow-Methods": []string{"PUT"}, "Access-Control-Allow-Headers": []string{"X-Foobar"}, - "Vary": []string{ - "Origin", - "origin", - "Access-Control-Request-Headers", - "access-Control-request-Headers", - "Access-Control-Request-Methods", - "ACCESS-CONTROL-REQUEST-METHODS", - "X-Foobar", - }, } appDetails := setupProxyTest(t, &DeploymentOptions{ diff --git a/coderd/workspaceapps/request.go b/coderd/workspaceapps/request.go index b700e7d4a54cf..ce99d4ccdbcf8 100644 --- a/coderd/workspaceapps/request.go +++ b/coderd/workspaceapps/request.go @@ -299,7 +299,7 @@ func (r Request) getDatabase(ctx context.Context, db database.Store) (*databaseR ) //nolint:nestif if portUintErr == nil { - // TODO: handle this branch + // TODO: handle CORS passthru for port sharing use-case. appCORSBehavior = database.AppCorsBehaviorSimple protocol := "http" diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index 1d7b26c4fe423..2585f381943b3 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -435,12 +435,11 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s var corsBehavior proto.AppCORSBehavior switch strings.ToLower(attrs.CORSBehavior) { - case "simple": - corsBehavior = proto.AppCORSBehavior_SIMPLE case "passthru": corsBehavior = proto.AppCORSBehavior_PASSTHRU default: - return nil, xerrors.Errorf("invalid app CORS behavior %q", attrs.CORSBehavior) + corsBehavior = proto.AppCORSBehavior_SIMPLE + logger.Debug(ctx, "CORS behavior not set, defaulting to 'simple'") } for _, agents := range resourceAgents { From 65f984f87cc33c7900cf4e820968ab37b4998db0 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 27 Nov 2024 11:40:21 +0000 Subject: [PATCH 05/11] Appeasing the linter Signed-off-by: Danny Kopping --- coderd/database/dbauthz/dbauthz_test.go | 1 + coderd/database/dbgen/dbgen.go | 1 + coderd/database/dbmem/dbmem.go | 4 ++++ coderd/workspaceapps/apptest/setup.go | 11 ++++++----- coderd/workspaceapps/db_test.go | 11 ++++++----- provisioner/terraform/resources.go | 2 +- provisionersdk/proto/provisioner.pb.go | 2 +- provisionersdk/proto/provisioner_drpc.pb.go | 2 +- 8 files changed, 21 insertions(+), 13 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 638829ae24ae5..8762a22cbd883 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2567,6 +2567,7 @@ func (s *MethodTestSuite) TestSystemFunctions() { ID: uuid.New(), Health: database.WorkspaceAppHealthDisabled, SharingLevel: database.AppSharingLevelOwner, + CORSBehavior: database.AppCorsBehaviorSimple, }).Asserts(rbac.ResourceSystem, policy.ActionCreate) })) s.Run("InsertWorkspaceResourceMetadata", s.Subtest(func(db database.Store, check *expects) { diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 9c8696112dea8..271bb0056c4dc 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -618,6 +618,7 @@ func WorkspaceApp(t testing.TB, db database.Store, orig database.WorkspaceApp) d Health: takeFirst(orig.Health, database.WorkspaceAppHealthHealthy), DisplayOrder: takeFirst(orig.DisplayOrder, 1), Hidden: orig.Hidden, + CORSBehavior: takeFirst(orig.CORSBehavior, database.AppCorsBehaviorSimple), }) require.NoError(t, err, "insert app") return resource diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index cfab07333a6c2..7be45e76c2b79 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -8155,6 +8155,10 @@ func (q *FakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertW arg.SharingLevel = database.AppSharingLevelOwner } + if arg.CORSBehavior == "" { + arg.CORSBehavior = database.AppCorsBehaviorSimple + } + // nolint:gosimple workspaceApp := database.WorkspaceApp{ ID: arg.ID, diff --git a/coderd/workspaceapps/apptest/setup.go b/coderd/workspaceapps/apptest/setup.go index 06ff54eec04fc..49cbb6b366219 100644 --- a/coderd/workspaceapps/apptest/setup.go +++ b/coderd/workspaceapps/apptest/setup.go @@ -32,11 +32,12 @@ import ( ) const ( - proxyTestAgentName = "agent-name" - proxyTestAppNameFake = "test-app-fake" - proxyTestAppNameOwner = "test-app-owner" - proxyTestAppNameAuthenticated = "test-app-authenticated" - proxyTestAppNamePublic = "test-app-public" + proxyTestAgentName = "agent-name" + proxyTestAppNameFake = "test-app-fake" + proxyTestAppNameOwner = "test-app-owner" + proxyTestAppNameAuthenticated = "test-app-authenticated" + proxyTestAppNamePublic = "test-app-public" + // nolint:gosec // Not a secret proxyTestAppNameAuthenticatedCORSPassthru = "test-app-authenticated-cors-passthru" proxyTestAppNamePublicCORSPassthru = "test-app-public-cors-passthru" proxyTestAppNameAuthenticatedCORSDefault = "test-app-authenticated-cors-default" diff --git a/coderd/workspaceapps/db_test.go b/coderd/workspaceapps/db_test.go index bf364f1ce62b3..0ad7c5d99c1e7 100644 --- a/coderd/workspaceapps/db_test.go +++ b/coderd/workspaceapps/db_test.go @@ -280,11 +280,12 @@ func Test_ResolveRequest(t *testing.T) { RegisteredClaims: jwtutils.RegisteredClaims{ Expiry: jwt.NewNumericDate(token.Expiry.Time()), }, - Request: req, - UserID: me.ID, - WorkspaceID: workspace.ID, - AgentID: agentID, - AppURL: appURL, + Request: req, + UserID: me.ID, + WorkspaceID: workspace.ID, + AgentID: agentID, + AppURL: appURL, + CORSBehavior: token.CORSBehavior, }, token) require.NotZero(t, token.Expiry) require.WithinDuration(t, time.Now().Add(workspaceapps.DefaultTokenExpiry), token.Expiry.Time(), time.Minute) diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index 2585f381943b3..1111ff43d1b33 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -439,7 +439,7 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s corsBehavior = proto.AppCORSBehavior_PASSTHRU default: corsBehavior = proto.AppCORSBehavior_SIMPLE - logger.Debug(ctx, "CORS behavior not set, defaulting to 'simple'") + logger.Debug(ctx, "cors_behavior not set, defaulting to 'simple'", slog.F("address", convertAddressToLabel(resource.Address))) } for _, agents := range resourceAgents { diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index 70922e445b343..b596d9cfef7a3 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.30.0 -// protoc v5.28.2 +// protoc v4.23.3 // source: provisionersdk/proto/provisioner.proto package proto diff --git a/provisionersdk/proto/provisioner_drpc.pb.go b/provisionersdk/proto/provisioner_drpc.pb.go index c9c54002439c2..de310e779dcaa 100644 --- a/provisionersdk/proto/provisioner_drpc.pb.go +++ b/provisionersdk/proto/provisioner_drpc.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-drpc. DO NOT EDIT. -// protoc-gen-go-drpc version: (devel) +// protoc-gen-go-drpc version: v0.0.33 // source: provisionersdk/proto/provisioner.proto package proto From 1f0b34c178bd759ba36ea66bc69388f8731f4925 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 27 Nov 2024 13:01:06 +0000 Subject: [PATCH 06/11] Move type def to codersdk Signed-off-by: Danny Kopping --- coderd/workspaceapps/apptest/setup.go | 7 +++---- coderd/workspaceapps/cors/cors.go | 15 ++++++--------- coderd/workspaceapps/db.go | 3 +-- coderd/workspaceapps/proxy.go | 6 +++--- coderd/workspaceapps/token.go | 3 +-- codersdk/cors_behavior.go | 17 +++++++++++++++++ 6 files changed, 31 insertions(+), 20 deletions(-) create mode 100644 codersdk/cors_behavior.go diff --git a/coderd/workspaceapps/apptest/setup.go b/coderd/workspaceapps/apptest/setup.go index 49cbb6b366219..cb3f8139f20a0 100644 --- a/coderd/workspaceapps/apptest/setup.go +++ b/coderd/workspaceapps/apptest/setup.go @@ -22,7 +22,6 @@ import ( "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/workspaceapps" "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/agentsdk" "github.com/coder/coder/v2/cryptorand" @@ -102,7 +101,7 @@ type App struct { Path string // Control the behavior of CORS handling. - CORSBehavior cors.AppCORSBehavior + CORSBehavior codersdk.AppCORSBehavior } // Details are the full test details returned from setupProxyTestWithFactory. @@ -271,7 +270,7 @@ func setupProxyTestWithFactory(t *testing.T, factory DeploymentFactory, opts *De WorkspaceName: workspace.Name, AgentName: agnt.Name, AppSlugOrPort: proxyTestAppNamePublicCORSPassthru, - CORSBehavior: cors.AppCORSBehaviorPassthru, + CORSBehavior: codersdk.AppCORSBehaviorPassthru, Query: proxyTestAppQuery, } details.Apps.AuthenticatedCORSPassthru = App{ @@ -279,7 +278,7 @@ func setupProxyTestWithFactory(t *testing.T, factory DeploymentFactory, opts *De WorkspaceName: workspace.Name, AgentName: agnt.Name, AppSlugOrPort: proxyTestAppNameAuthenticatedCORSPassthru, - CORSBehavior: cors.AppCORSBehaviorPassthru, + CORSBehavior: codersdk.AppCORSBehaviorPassthru, Query: proxyTestAppQuery, } details.Apps.PublicCORSDefault = App{ diff --git a/coderd/workspaceapps/cors/cors.go b/coderd/workspaceapps/cors/cors.go index 0ee34cf390c5a..c204cb322f173 100644 --- a/coderd/workspaceapps/cors/cors.go +++ b/coderd/workspaceapps/cors/cors.go @@ -1,24 +1,21 @@ package cors -import "context" +import ( + "context" -type AppCORSBehavior string - -const ( - AppCORSBehaviorSimple AppCORSBehavior = "simple" - AppCORSBehaviorPassthru AppCORSBehavior = "passthru" + "github.com/coder/coder/v2/codersdk" ) type contextKeyBehavior struct{} // WithBehavior sets the CORS behavior for the given context. -func WithBehavior(ctx context.Context, behavior AppCORSBehavior) context.Context { +func WithBehavior(ctx context.Context, behavior codersdk.AppCORSBehavior) 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 AppCORSBehavior) bool { +func HasBehavior(ctx context.Context, behavior codersdk.AppCORSBehavior) bool { val := ctx.Value(contextKeyBehavior{}) - b, ok := val.(AppCORSBehavior) + b, ok := val.(codersdk.AppCORSBehavior) return ok && b == behavior } diff --git a/coderd/workspaceapps/db.go b/coderd/workspaceapps/db.go index 1a16a680dfb4d..2cd56fdd7d0dd 100644 --- a/coderd/workspaceapps/db.go +++ b/coderd/workspaceapps/db.go @@ -25,7 +25,6 @@ import ( "github.com/coder/coder/v2/coderd/jwtutils" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" - "github.com/coder/coder/v2/coderd/workspaceapps/cors" "github.com/coder/coder/v2/codersdk" ) @@ -132,7 +131,7 @@ func (p *DBTokenProvider) Issue(ctx context.Context, rw http.ResponseWriter, r * if dbReq.AppURL != nil { token.AppURL = dbReq.AppURL.String() } - token.CORSBehavior = cors.AppCORSBehavior(dbReq.AppCORSBehavior) + token.CORSBehavior = codersdk.AppCORSBehavior(dbReq.AppCORSBehavior) // Verify the user has access to the app. authed, warnings, err := p.authorizeRequest(r.Context(), authz, dbReq) diff --git a/coderd/workspaceapps/proxy.go b/coderd/workspaceapps/proxy.go index da6a075957837..87f439ad6fe24 100644 --- a/coderd/workspaceapps/proxy.go +++ b/coderd/workspaceapps/proxy.go @@ -441,7 +441,7 @@ func (s *Server) determineCORSBehavior(token *SignedToken, app appurl.Applicatio corsHandler := httpmw.WorkspaceAppCors(s.HostnameRegex, app)(next) return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - var behavior cors.AppCORSBehavior + var behavior codersdk.AppCORSBehavior if token != nil { behavior = token.CORSBehavior } @@ -452,7 +452,7 @@ func (s *Server) determineCORSBehavior(token *SignedToken, app appurl.Applicatio r = r.WithContext(cors.WithBehavior(r.Context(), behavior)) switch behavior { - case cors.AppCORSBehaviorPassthru: + case codersdk.AppCORSBehaviorPassthru: // Bypass the CORS middleware. next.ServeHTTP(rw, r) return @@ -595,7 +595,7 @@ func (s *Server) proxyWorkspaceApp(rw http.ResponseWriter, r *http.Request, appT proxy.ModifyResponse = func(r *http.Response) error { // If passthru behavior is set, disable our CORS header stripping. - if cors.HasBehavior(r.Request.Context(), cors.AppCORSBehaviorPassthru) { + if cors.HasBehavior(r.Request.Context(), codersdk.AppCORSBehaviorPassthru) { return nil } diff --git a/coderd/workspaceapps/token.go b/coderd/workspaceapps/token.go index 72b8db2bf8129..32b0641169693 100644 --- a/coderd/workspaceapps/token.go +++ b/coderd/workspaceapps/token.go @@ -11,7 +11,6 @@ import ( "github.com/coder/coder/v2/coderd/cryptokeys" "github.com/coder/coder/v2/coderd/jwtutils" - "github.com/coder/coder/v2/coderd/workspaceapps/cors" "github.com/coder/coder/v2/codersdk" ) @@ -27,7 +26,7 @@ type SignedToken struct { WorkspaceID uuid.UUID `json:"workspace_id"` AgentID uuid.UUID `json:"agent_id"` AppURL string `json:"app_url"` - CORSBehavior cors.AppCORSBehavior `json:"cors_behavior"` + CORSBehavior codersdk.AppCORSBehavior `json:"cors_behavior"` } // MatchesRequest returns true if the token matches the request. Any token that diff --git a/codersdk/cors_behavior.go b/codersdk/cors_behavior.go new file mode 100644 index 0000000000000..8fd8b9a893e37 --- /dev/null +++ b/codersdk/cors_behavior.go @@ -0,0 +1,17 @@ +package codersdk + +import "golang.org/x/xerrors" + +type AppCORSBehavior string + +const ( + AppCORSBehaviorSimple AppCORSBehavior = "simple" + AppCORSBehaviorPassthru AppCORSBehavior = "passthru" +) + +func (c AppCORSBehavior) Validate() error { + if c != AppCORSBehaviorSimple && c != AppCORSBehaviorPassthru { + return xerrors.New("Invalid CORS behavior.") + } + return nil +} From 5be547762bb9f8ad9be9d9b593b5ca6ed60f3ec8 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 27 Nov 2024 15:23:54 +0000 Subject: [PATCH 07/11] Appease the linter, again Signed-off-by: Danny Kopping --- coderd/workspaceapps/token.go | 8 ++++---- site/src/api/typesGenerated.ts | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/coderd/workspaceapps/token.go b/coderd/workspaceapps/token.go index 32b0641169693..d46f7b9335212 100644 --- a/coderd/workspaceapps/token.go +++ b/coderd/workspaceapps/token.go @@ -22,10 +22,10 @@ type SignedToken struct { // Request details. Request `json:"request"` - UserID uuid.UUID `json:"user_id"` - WorkspaceID uuid.UUID `json:"workspace_id"` - AgentID uuid.UUID `json:"agent_id"` - AppURL string `json:"app_url"` + UserID uuid.UUID `json:"user_id"` + WorkspaceID uuid.UUID `json:"workspace_id"` + AgentID uuid.UUID `json:"agent_id"` + AppURL string `json:"app_url"` CORSBehavior codersdk.AppCORSBehavior `json:"cors_behavior"` } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index c1b409013b6d7..dc63e7f70fd54 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -2137,6 +2137,10 @@ export const APIKeyScopes: APIKeyScope[] = ["all", "application_connect"] export type AgentSubsystem = "envbox" | "envbuilder" | "exectrace" export const AgentSubsystems: AgentSubsystem[] = ["envbox", "envbuilder", "exectrace"] +// From codersdk/cors_behavior.go +export type AppCORSBehavior = "passthru" | "simple" +export const AppCORSBehaviors: AppCORSBehavior[] = ["passthru", "simple"] + // From codersdk/audit.go export type AuditAction = "create" | "delete" | "login" | "logout" | "register" | "request_password_reset" | "start" | "stop" | "write" export const AuditActions: AuditAction[] = ["create", "delete", "login", "logout", "register", "request_password_reset", "start", "stop", "write"] From ddbbaa948b9e288896d7e4643cfd028371a2397c Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Thu, 28 Nov 2024 11:40:04 +0000 Subject: [PATCH 08/11] Fix migration num Signed-off-by: Danny Kopping --- ...avior.down.sql => 000278_workspace_app_cors_behavior.down.sql} | 0 ..._behavior.up.sql => 000278_workspace_app_cors_behavior.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000277_workspace_app_cors_behavior.down.sql => 000278_workspace_app_cors_behavior.down.sql} (100%) rename coderd/database/migrations/{000277_workspace_app_cors_behavior.up.sql => 000278_workspace_app_cors_behavior.up.sql} (100%) diff --git a/coderd/database/migrations/000277_workspace_app_cors_behavior.down.sql b/coderd/database/migrations/000278_workspace_app_cors_behavior.down.sql similarity index 100% rename from coderd/database/migrations/000277_workspace_app_cors_behavior.down.sql rename to coderd/database/migrations/000278_workspace_app_cors_behavior.down.sql diff --git a/coderd/database/migrations/000277_workspace_app_cors_behavior.up.sql b/coderd/database/migrations/000278_workspace_app_cors_behavior.up.sql similarity index 100% rename from coderd/database/migrations/000277_workspace_app_cors_behavior.up.sql rename to coderd/database/migrations/000278_workspace_app_cors_behavior.up.sql From 63c1852d30bbad016c3a13ddc3752dff4685e42b Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Thu, 28 Nov 2024 12:04:46 +0000 Subject: [PATCH 09/11] Removing unnecessary header check Signed-off-by: Danny Kopping --- coderd/workspaceapps/apptest/apptest.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/coderd/workspaceapps/apptest/apptest.go b/coderd/workspaceapps/apptest/apptest.go index d2918e571a98e..ac7fbbd060073 100644 --- a/coderd/workspaceapps/apptest/apptest.go +++ b/coderd/workspaceapps/apptest/apptest.go @@ -529,7 +529,6 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { // Check CORS headers are passed through require.Equal(t, testHeaders.Get("Access-Control-Allow-Origin"), resp.Header.Get("Access-Control-Allow-Origin")) - require.Equal(t, testHeaders.Get("Access-Control-Allow-Credentials"), resp.Header.Get("Access-Control-Allow-Credentials")) require.Equal(t, testHeaders.Get("Access-Control-Allow-Methods"), resp.Header.Get("Access-Control-Allow-Methods")) }) @@ -556,7 +555,6 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { // Check CORS headers are passed through require.Equal(t, testHeaders.Get("Access-Control-Allow-Origin"), resp.Header.Get("Access-Control-Allow-Origin")) - require.Equal(t, testHeaders.Get("Access-Control-Allow-Credentials"), resp.Header.Get("Access-Control-Allow-Credentials")) require.Equal(t, testHeaders.Get("Access-Control-Allow-Methods"), resp.Header.Get("Access-Control-Allow-Methods")) }) @@ -583,7 +581,6 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { // Check CORS headers are passed through require.Equal(t, testHeaders.Get("Access-Control-Allow-Origin"), resp.Header.Get("Access-Control-Allow-Origin")) - require.Equal(t, testHeaders.Get("Access-Control-Allow-Credentials"), resp.Header.Get("Access-Control-Allow-Credentials")) require.Equal(t, testHeaders.Get("Access-Control-Allow-Methods"), resp.Header.Get("Access-Control-Allow-Methods")) }) }) From 976cd78d90a878bb88fd5a1d7c9cdd3a08409d0e Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Thu, 28 Nov 2024 14:54:32 +0000 Subject: [PATCH 10/11] Improve tests Signed-off-by: Danny Kopping --- coderd/workspaceapps/apptest/apptest.go | 192 +++++++++++++----------- 1 file changed, 101 insertions(+), 91 deletions(-) diff --git a/coderd/workspaceapps/apptest/apptest.go b/coderd/workspaceapps/apptest/apptest.go index ac7fbbd060073..a677778114ceb 100644 --- a/coderd/workspaceapps/apptest/apptest.go +++ b/coderd/workspaceapps/apptest/apptest.go @@ -472,7 +472,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { }) }) - t.Run("CORS", func(t *testing.T) { + t.Run("WorkspaceApplicationCORS", func(t *testing.T) { t.Parallel() // Set up test headers that should be returned by the app @@ -481,108 +481,118 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { "Access-Control-Allow-Methods": []string{"GET, POST, OPTIONS"}, } - t.Run("UnauthenticatedPassthruRejected", func(t *testing.T) { - t.Parallel() - - ctx := testutil.Context(t, testutil.WaitLong) - - appDetails := setupProxyTest(t, &DeploymentOptions{ - headers: testHeaders, - }) - - // Given: an unauthenticated client - client := appDetails.AppClient(t) - client.SetSessionToken("") - - // When: a request is made to an authenticated app with passthru CORS behavior - resp, err := requestWithRetries(ctx, t, client, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.AuthenticatedCORSPassthru).String(), nil) - require.NoError(t, err) - defer resp.Body.Close() - - // Then: the request is redirected to login because even though CORS is passthru, - // the request must still be authenticated first - require.Equal(t, http.StatusSeeOther, resp.StatusCode) - gotLocation, err := resp.Location() - require.NoError(t, err) - require.Equal(t, appDetails.SDKClient.URL.Host, gotLocation.Host) - require.Equal(t, "/api/v2/applications/auth-redirect", gotLocation.Path) - }) - - t.Run("AuthenticatedPassthruOK", func(t *testing.T) { - t.Parallel() - - ctx := testutil.Context(t, testutil.WaitLong) - - appDetails := setupProxyTest(t, &DeploymentOptions{ - headers: testHeaders, - }) - - userClient, _ := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, appDetails.FirstUser.OrganizationID, rbac.RoleMember()) - userAppClient := appDetails.AppClient(t) - userAppClient.SetSessionToken(userClient.SessionToken()) - - // Given: an authenticated app with passthru CORS behavior - resp, err := requestWithRetries(ctx, t, userAppClient, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.AuthenticatedCORSPassthru).String(), nil) - require.NoError(t, err) - defer resp.Body.Close() - require.Equal(t, http.StatusOK, resp.StatusCode) - - // Check CORS headers are passed through - require.Equal(t, testHeaders.Get("Access-Control-Allow-Origin"), resp.Header.Get("Access-Control-Allow-Origin")) - require.Equal(t, testHeaders.Get("Access-Control-Allow-Methods"), resp.Header.Get("Access-Control-Allow-Methods")) + appDetails := setupProxyTest(t, &DeploymentOptions{ + headers: testHeaders, }) - t.Run("UnauthenticatedPublicPassthruOK", func(t *testing.T) { - t.Parallel() + unauthenticatedClient := func(t *testing.T, appDetails *Details) *codersdk.Client { + c := appDetails.AppClient(t) + c.SetSessionToken("") + return c + } - ctx := testutil.Context(t, testutil.WaitLong) + 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 + } - appDetails := setupProxyTest(t, &DeploymentOptions{ - headers: testHeaders, - }) + ownerClient := func(t *testing.T, appDetails *Details) *codersdk.Client { + return appDetails.SDKClient + } - // Given: an unauthenticated client - client := appDetails.AppClient(t) - client.SetSessionToken("") + tests := []struct { + name string + app App + client func(t *testing.T, appDetails *Details) *codersdk.Client + expectedStatusCode int + expectedCORSHeaders bool + }{ + // Public + { + name: "Default/Public", + app: appDetails.Apps.PublicCORSDefault, + client: unauthenticatedClient, + expectedStatusCode: http.StatusOK, + expectedCORSHeaders: false, + }, + { + name: "Passthru/Public", + app: appDetails.Apps.PublicCORSPassthru, + client: unauthenticatedClient, + expectedStatusCode: http.StatusOK, + expectedCORSHeaders: true, + }, + // Authenticated + { + name: "Default/Authenticated", + app: appDetails.Apps.AuthenticatedCORSDefault, + expectedCORSHeaders: false, + client: authenticatedClient, + expectedStatusCode: http.StatusOK, + }, + { + name: "Passthru/Authenticated", + app: appDetails.Apps.AuthenticatedCORSPassthru, + 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", + app: appDetails.Apps.AuthenticatedCORSPassthru, + expectedCORSHeaders: false, + client: unauthenticatedClient, + expectedStatusCode: http.StatusSeeOther, + }, + // Owner + { + name: "Default/Owner", + app: appDetails.Apps.AuthenticatedCORSDefault, + expectedCORSHeaders: false, + client: ownerClient, + expectedStatusCode: http.StatusOK, + }, + { + name: "Passthru/Owner", + app: appDetails.Apps.AuthenticatedCORSPassthru, + expectedCORSHeaders: true, + client: ownerClient, + expectedStatusCode: http.StatusOK, + }, + } - // When: a request is made to a public app with passthru CORS behavior - resp, err := requestWithRetries(ctx, t, client, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.PublicCORSPassthru).String(), nil) - require.NoError(t, err) - defer resp.Body.Close() + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - // Then: the request succeeds because the app is public - require.Equal(t, http.StatusOK, resp.StatusCode) + ctx := testutil.Context(t, testutil.WaitLong) - // Check CORS headers are passed through - require.Equal(t, testHeaders.Get("Access-Control-Allow-Origin"), resp.Header.Get("Access-Control-Allow-Origin")) - require.Equal(t, testHeaders.Get("Access-Control-Allow-Methods"), resp.Header.Get("Access-Control-Allow-Methods")) - }) + // Given: a client + client := tc.client(t, appDetails) - t.Run("AuthenticatedPublicPassthruOK", func(t *testing.T) { - t.Parallel() + // When: a request is made to an authenticated app with a specified CORS behavior + resp, err := requestWithRetries(ctx, t, client, http.MethodGet, appDetails.SubdomainAppURL(tc.app).String(), nil) + require.NoError(t, err) + defer resp.Body.Close() - ctx := testutil.Context(t, testutil.WaitLong) + // Then: the request must match expectations + require.Equal(t, tc.expectedStatusCode, resp.StatusCode) + require.NoError(t, err) - appDetails := setupProxyTest(t, &DeploymentOptions{ - headers: testHeaders, + // Then: the CORS headers must match expectations + if tc.expectedCORSHeaders { + require.Equal(t, testHeaders.Get("Access-Control-Allow-Origin"), resp.Header.Get("Access-Control-Allow-Origin")) + require.Equal(t, testHeaders.Get("Access-Control-Allow-Methods"), resp.Header.Get("Access-Control-Allow-Methods")) + } else { + require.Empty(t, resp.Header.Get("Access-Control-Allow-Origin")) + require.Empty(t, resp.Header.Get("Access-Control-Allow-Methods")) + } }) - - userClient, _ := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, appDetails.FirstUser.OrganizationID, rbac.RoleMember()) - userAppClient := appDetails.AppClient(t) - userAppClient.SetSessionToken(userClient.SessionToken()) - - // Given: an authenticated client accessing a public app with passthru CORS behavior - resp, err := requestWithRetries(ctx, t, userAppClient, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.PublicCORSPassthru).String(), nil) - require.NoError(t, err) - defer resp.Body.Close() - - // Then: the request succeeds because the app is public - require.Equal(t, http.StatusOK, resp.StatusCode) - - // Check CORS headers are passed through - require.Equal(t, testHeaders.Get("Access-Control-Allow-Origin"), resp.Header.Get("Access-Control-Allow-Origin")) - require.Equal(t, testHeaders.Get("Access-Control-Allow-Methods"), resp.Header.Get("Access-Control-Allow-Methods")) - }) + } }) t.Run("WorkspaceApplicationAuth", func(t *testing.T) { From 3dc00e1dc58d50f0c033b9c63d79e8fda9afcbb4 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Fri, 29 Nov 2024 13:13:26 +0000 Subject: [PATCH 11/11] Comprehensive CORS test suite for both request & response Signed-off-by: Danny Kopping --- coderd/workspaceapps/apptest/apptest.go | 411 ++++++++++++++++++++---- coderd/workspaceapps/apptest/setup.go | 39 ++- 2 files changed, 363 insertions(+), 87 deletions(-) diff --git a/coderd/workspaceapps/apptest/apptest.go b/coderd/workspaceapps/apptest/apptest.go index a677778114ceb..b66e4cbbbc6be 100644 --- a/coderd/workspaceapps/apptest/apptest.go +++ b/coderd/workspaceapps/apptest/apptest.go @@ -475,15 +475,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { t.Run("WorkspaceApplicationCORS", 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"}, - } - - appDetails := setupProxyTest(t, &DeploymentOptions{ - headers: testHeaders, - }) + const external = "https://example.com" unauthenticatedClient := func(t *testing.T, appDetails *Details) *codersdk.Client { c := appDetails.AppClient(t) @@ -498,70 +490,310 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { return c } - ownerClient := func(t *testing.T, appDetails *Details) *codersdk.Client { - return appDetails.SDKClient + 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 App - client func(t *testing.T, appDetails *Details) *codersdk.Client - expectedStatusCode int - expectedCORSHeaders bool + name string + app func(details *Details) App + client func(t *testing.T, appDetails *Details) *codersdk.Client + behavior codersdk.AppCORSBehavior + 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 { - name: "Default/Public", - app: appDetails.Apps.PublicCORSDefault, - client: unauthenticatedClient, - expectedStatusCode: http.StatusOK, - expectedCORSHeaders: false, + // 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.AppCORSBehaviorSimple, + 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")) + }, }, { - name: "Passthru/Public", - app: appDetails.Apps.PublicCORSPassthru, - client: unauthenticatedClient, - expectedStatusCode: http.StatusOK, - expectedCORSHeaders: true, + // 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.AppCORSBehaviorSimple, + 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")) + }, + }, + { + // 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.AppCORSBehaviorSimple, + 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")) + }, + }, + { + // 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.AppCORSBehaviorPassthru, + 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")) + }, + }, + { + // 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.AppCORSBehaviorPassthru, + 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.AppCORSBehaviorPassthru, + 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 { - name: "Default/Authenticated", - app: appDetails.Apps.AuthenticatedCORSDefault, - expectedCORSHeaders: false, - client: authenticatedClient, - expectedStatusCode: http.StatusOK, + // Same behavior as Default/Public/Preflight/Subdomain. + name: "Default/Authenticated/Preflight/Subdomain", + app: func(details *Details) App { return details.Apps.AuthenticatedCORSDefault }, + behavior: codersdk.AppCORSBehaviorSimple, + 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.AppCORSBehaviorSimple, + 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.AppCORSBehaviorSimple, + 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.AppCORSBehaviorSimple, + 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")) + }, }, { - name: "Passthru/Authenticated", - app: appDetails.Apps.AuthenticatedCORSPassthru, - expectedCORSHeaders: true, - client: authenticatedClient, - expectedStatusCode: http.StatusOK, + // 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.AppCORSBehaviorPassthru, + 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")) + }, }, { - // The CORS behavior will not affect unauthenticated requests. - // The request will be redirected to the login page. - name: "Passthru/Unauthenticated", - app: appDetails.Apps.AuthenticatedCORSPassthru, - expectedCORSHeaders: false, - client: unauthenticatedClient, - expectedStatusCode: http.StatusSeeOther, + // 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.AppCORSBehaviorPassthru, + 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")) + }, }, - // Owner { - name: "Default/Owner", - app: appDetails.Apps.AuthenticatedCORSDefault, - expectedCORSHeaders: false, - client: ownerClient, - expectedStatusCode: http.StatusOK, + // 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.AppCORSBehaviorPassthru, + 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")) + }, }, { - name: "Passthru/Owner", - app: appDetails.Apps.AuthenticatedCORSPassthru, - expectedCORSHeaders: true, - client: ownerClient, - expectedStatusCode: http.StatusOK, + // 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.AppCORSBehaviorPassthru, + 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.AppCORSBehaviorPassthru, + 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.AppCORSBehaviorPassthru, + 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.AppCORSBehaviorPassthru, + 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.AppCORSBehaviorPassthru, + 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")) + }, }, } @@ -571,26 +803,65 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { ctx := testutil.Context(t, testutil.WaitLong) - // Given: a client - client := tc.client(t, appDetails) + 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.AppCORSBehaviorPassthru: + 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.AppCORSBehaviorSimple: + w.Header().Set("X-CORS-Handler", "simple") + } + }), + }) - // When: a request is made to an authenticated app with a specified CORS behavior - resp, err := requestWithRetries(ctx, t, client, http.MethodGet, appDetails.SubdomainAppURL(tc.app).String(), nil) + // 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)) + + // 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 must match expectations - require.Equal(t, tc.expectedStatusCode, resp.StatusCode) - require.NoError(t, err) - - // Then: the CORS headers must match expectations - if tc.expectedCORSHeaders { - require.Equal(t, testHeaders.Get("Access-Control-Allow-Origin"), resp.Header.Get("Access-Control-Allow-Origin")) - require.Equal(t, testHeaders.Get("Access-Control-Allow-Methods"), resp.Header.Get("Access-Control-Allow-Methods")) - } else { - require.Empty(t, resp.Header.Get("Access-Control-Allow-Origin")) - require.Empty(t, resp.Header.Get("Access-Control-Allow-Methods")) + // 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) }) } }) @@ -1511,7 +1782,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { forceURLTransport(t, client) // Create workspace. - port := appServer(t, nil, false) + port := appServer(t, nil, false, nil) workspace, _ = createWorkspaceWithApps(t, client, user.OrganizationIDs[0], user, port, false) // Verify that the apps have the correct sharing levels set. diff --git a/coderd/workspaceapps/apptest/setup.go b/coderd/workspaceapps/apptest/setup.go index cb3f8139f20a0..c8c094479292c 100644 --- a/coderd/workspaceapps/apptest/setup.go +++ b/coderd/workspaceapps/apptest/setup.go @@ -65,6 +65,7 @@ type DeploymentOptions struct { noWorkspace bool port uint16 headers http.Header + handler http.Handler } // Deployment is a license-agnostic deployment with all the fields that apps @@ -214,7 +215,7 @@ func setupProxyTestWithFactory(t *testing.T, factory DeploymentFactory, opts *De } 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) @@ -300,25 +301,29 @@ func setupProxyTestWithFactory(t *testing.T, factory DeploymentFactory, opts *De } //nolint:revive -func appServer(t *testing.T, headers http.Header, isHTTPS bool) uint16 { - server := httptest.NewUnstartedServer( - http.HandlerFunc( - func(w http.ResponseWriter, r *http.Request) { - _, err := r.Cookie(codersdk.SessionTokenCookie) - assert.ErrorIs(t, err, http.ErrNoCookie) - w.Header().Set("X-Forwarded-For", r.Header.Get("X-Forwarded-For")) - w.Header().Set("X-Got-Host", r.Host) - for name, values := range headers { - for _, value := range values { - w.Header().Add(name, value) - } +func appServer(t *testing.T, headers http.Header, isHTTPS bool, handler http.Handler) uint16 { + defaultHandler := http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + _, err := r.Cookie(codersdk.SessionTokenCookie) + assert.ErrorIs(t, err, http.ErrNoCookie) + w.Header().Set("X-Forwarded-For", r.Header.Get("X-Forwarded-For")) + w.Header().Set("X-Got-Host", r.Host) + for name, values := range headers { + for _, value := range values { + 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 if isHTTPS { server.StartTLS()