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() pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy