diff --git a/cli/tokens.go b/cli/tokens.go index 8719ab34348cb..4c3cb830cd24a 100644 --- a/cli/tokens.go +++ b/cli/tokens.go @@ -55,7 +55,7 @@ func createToken() *cobra.Command { return xerrors.Errorf("create codersdk client: %w", err) } - res, err := client.CreateToken(cmd.Context(), codersdk.Me) + res, err := client.CreateToken(cmd.Context(), codersdk.Me, codersdk.CreateTokenRequest{}) if err != nil { return xerrors.Errorf("create tokens: %w", err) } diff --git a/coderd/apikey.go b/coderd/apikey.go index 645d660adab90..84e936cb22e16 100644 --- a/coderd/apikey.go +++ b/coderd/apikey.go @@ -34,12 +34,23 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) { return } + var createToken codersdk.CreateTokenRequest + if !httpapi.Read(ctx, rw, r, &createToken) { + return + } + + scope := database.APIKeyScopeAll + if scope != "" { + scope = database.APIKeyScope(createToken.Scope) + } + // tokens last 100 years lifeTime := time.Hour * 876000 cookie, err := api.createAPIKey(ctx, createAPIKeyParams{ UserID: user.ID, LoginType: database.LoginTypeToken, ExpiresAt: database.Now().Add(lifeTime), + Scope: scope, LifetimeSeconds: int64(lifeTime.Seconds()), }) if err != nil { @@ -54,6 +65,7 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) { } // Creates a new session key, used for logging in via the CLI. +// DEPRECATED: use postToken instead. func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() user := httpmw.UserParam(r) @@ -229,6 +241,11 @@ func (api *API) createAPIKey(ctx context.Context, params createAPIKeyParams) (*h if params.Scope != "" { scope = params.Scope } + switch scope { + case database.APIKeyScopeAll, database.APIKeyScopeApplicationConnect: + default: + return nil, xerrors.Errorf("invalid API key scope: %q", scope) + } key, err := api.Database.InsertAPIKey(ctx, database.InsertAPIKeyParams{ ID: keyID, diff --git a/coderd/apikey_test.go b/coderd/apikey_test.go index e9163e5c5917e..f40966b0a239e 100644 --- a/coderd/apikey_test.go +++ b/coderd/apikey_test.go @@ -14,30 +14,61 @@ import ( func TestTokens(t *testing.T) { t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - _ = coderdtest.CreateFirstUser(t, client) - keys, err := client.GetTokens(ctx, codersdk.Me) - require.NoError(t, err) - require.Empty(t, keys) - res, err := client.CreateToken(ctx, codersdk.Me) - require.NoError(t, err) - require.Greater(t, len(res.Key), 2) + t.Run("CRUD", func(t *testing.T) { + t.Parallel() - keys, err = client.GetTokens(ctx, codersdk.Me) - require.NoError(t, err) - require.EqualValues(t, len(keys), 1) - require.Contains(t, res.Key, keys[0].ID) - // expires_at must be greater than 50 years - require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*438300)) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + keys, err := client.GetTokens(ctx, codersdk.Me) + require.NoError(t, err) + require.Empty(t, keys) - err = client.DeleteAPIKey(ctx, codersdk.Me, keys[0].ID) - require.NoError(t, err) - keys, err = client.GetTokens(ctx, codersdk.Me) - require.NoError(t, err) - require.Empty(t, keys) + res, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{}) + require.NoError(t, err) + require.Greater(t, len(res.Key), 2) + + keys, err = client.GetTokens(ctx, codersdk.Me) + require.NoError(t, err) + require.EqualValues(t, len(keys), 1) + require.Contains(t, res.Key, keys[0].ID) + // expires_at must be greater than 50 years + require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*438300)) + require.Equal(t, codersdk.APIKeyScopeAll, keys[0].Scope) + + // no update + + err = client.DeleteAPIKey(ctx, codersdk.Me, keys[0].ID) + require.NoError(t, err) + keys, err = client.GetTokens(ctx, codersdk.Me) + require.NoError(t, err) + require.Empty(t, keys) + }) + + t.Run("Scoped", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + + res, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{ + Scope: codersdk.APIKeyScopeApplicationConnect, + }) + require.NoError(t, err) + require.Greater(t, len(res.Key), 2) + + keys, err := client.GetTokens(ctx, codersdk.Me) + require.NoError(t, err) + require.EqualValues(t, len(keys), 1) + require.Contains(t, res.Key, keys[0].ID) + // expires_at must be greater than 50 years + require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*438300)) + require.Equal(t, keys[0].Scope, codersdk.APIKeyScopeApplicationConnect) + }) } func TestAPIKey(t *testing.T) { diff --git a/coderd/coderd.go b/coderd/coderd.go index b067035b3adb4..8b5080346c35b 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -197,7 +197,7 @@ func New(options *Options) *API { RedirectToLogin: false, Optional: true, }), - httpmw.ExtractUserParam(api.Database), + httpmw.ExtractUserParam(api.Database, false), httpmw.ExtractWorkspaceAndAgentParam(api.Database), ), // Build-Version is helpful for debugging. @@ -214,8 +214,18 @@ func New(options *Options) *API { r.Use( tracing.Middleware(api.TracerProvider), httpmw.RateLimitPerMinute(options.APIRateLimit), - apiKeyMiddlewareRedirect, - httpmw.ExtractUserParam(api.Database), + httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{ + DB: options.Database, + OAuth2Configs: oauthConfigs, + // Optional is true to allow for public apps. If an + // authorization check fails and the user is not authenticated, + // they will be redirected to the login page by the app handler. + RedirectToLogin: false, + Optional: true, + }), + // Redirect to the login page if the user tries to open an app with + // "me" as the username and they are not logged in. + httpmw.ExtractUserParam(api.Database, true), // Extracts the from the url httpmw.ExtractWorkspaceAndAgentParam(api.Database), ) @@ -310,7 +320,7 @@ func New(options *Options) *API { r.Get("/roles", api.assignableOrgRoles) r.Route("/{user}", func(r chi.Router) { r.Use( - httpmw.ExtractUserParam(options.Database), + httpmw.ExtractUserParam(options.Database, false), httpmw.ExtractOrganizationMemberParam(options.Database), ) r.Put("/roles", api.putMemberRoles) @@ -389,7 +399,7 @@ func New(options *Options) *API { r.Get("/", api.assignableSiteRoles) }) r.Route("/{user}", func(r chi.Router) { - r.Use(httpmw.ExtractUserParam(options.Database)) + r.Use(httpmw.ExtractUserParam(options.Database, false)) r.Delete("/", api.deleteUser) r.Get("/", api.userByName) r.Put("/profile", api.putUserProfile) diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 2f5d57e5d6a9b..654139e604ae2 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -2324,6 +2324,10 @@ func (q *fakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertW q.mutex.Lock() defer q.mutex.Unlock() + if arg.SharingLevel == "" { + arg.SharingLevel = database.AppSharingLevelOwner + } + // nolint:gosimple workspaceApp := database.WorkspaceApp{ ID: arg.ID, @@ -2334,6 +2338,7 @@ func (q *fakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertW Command: arg.Command, Url: arg.Url, Subdomain: arg.Subdomain, + SharingLevel: arg.SharingLevel, HealthcheckUrl: arg.HealthcheckUrl, HealthcheckInterval: arg.HealthcheckInterval, HealthcheckThreshold: arg.HealthcheckThreshold, diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 9345907d044c4..9e2c68dbf6ef3 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -5,6 +5,12 @@ CREATE TYPE api_key_scope AS ENUM ( 'application_connect' ); +CREATE TYPE app_sharing_level AS ENUM ( + 'owner', + 'authenticated', + 'public' +); + CREATE TYPE audit_action AS ENUM ( 'create', 'write', @@ -371,7 +377,8 @@ CREATE TABLE workspace_apps ( healthcheck_interval integer DEFAULT 0 NOT NULL, healthcheck_threshold integer DEFAULT 0 NOT NULL, health workspace_app_health DEFAULT 'disabled'::public.workspace_app_health NOT NULL, - subdomain boolean DEFAULT false NOT NULL + subdomain boolean DEFAULT false NOT NULL, + sharing_level app_sharing_level DEFAULT 'owner'::public.app_sharing_level NOT NULL ); CREATE TABLE workspace_builds ( diff --git a/coderd/database/migrations/000060_app_sharing_level.down.sql b/coderd/database/migrations/000060_app_sharing_level.down.sql new file mode 100644 index 0000000000000..757a7f8792740 --- /dev/null +++ b/coderd/database/migrations/000060_app_sharing_level.down.sql @@ -0,0 +1,5 @@ +-- Drop column sharing_level from workspace_apps +ALTER TABLE workspace_apps DROP COLUMN sharing_level; + +-- Drop type app_sharing_level +DROP TYPE app_sharing_level; diff --git a/coderd/database/migrations/000060_app_sharing_level.up.sql b/coderd/database/migrations/000060_app_sharing_level.up.sql new file mode 100644 index 0000000000000..b339ab9726af8 --- /dev/null +++ b/coderd/database/migrations/000060_app_sharing_level.up.sql @@ -0,0 +1,12 @@ +-- Add enum app_sharing_level +CREATE TYPE app_sharing_level AS ENUM ( + -- only the workspace owner can access the app + 'owner', + -- any authenticated user on the site can access the app + 'authenticated', + -- any user can access the app even if they are not authenticated + 'public' +); + +-- Add sharing_level column to workspace_apps table +ALTER TABLE workspace_apps ADD COLUMN sharing_level app_sharing_level NOT NULL DEFAULT 'owner'::app_sharing_level; diff --git a/coderd/database/models.go b/coderd/database/models.go index 2e50d2b647bd7..e30615244e299 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -34,6 +34,26 @@ func (e *APIKeyScope) Scan(src interface{}) error { return nil } +type AppSharingLevel string + +const ( + AppSharingLevelOwner AppSharingLevel = "owner" + AppSharingLevelAuthenticated AppSharingLevel = "authenticated" + AppSharingLevelPublic AppSharingLevel = "public" +) + +func (e *AppSharingLevel) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = AppSharingLevel(s) + case string: + *e = AppSharingLevel(s) + default: + return fmt.Errorf("unsupported scan type for AppSharingLevel: %T", src) + } + return nil +} + type AuditAction string const ( @@ -626,6 +646,7 @@ type WorkspaceApp struct { HealthcheckThreshold int32 `db:"healthcheck_threshold" json:"healthcheck_threshold"` Health WorkspaceAppHealth `db:"health" json:"health"` Subdomain bool `db:"subdomain" json:"subdomain"` + SharingLevel AppSharingLevel `db:"sharing_level" json:"sharing_level"` } type WorkspaceBuild struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 8673822ca4c55..4b28c6004098a 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -4324,7 +4324,7 @@ func (q *sqlQuerier) UpdateWorkspaceAgentVersionByID(ctx context.Context, arg Up } const getWorkspaceAppByAgentIDAndName = `-- name: GetWorkspaceAppByAgentIDAndName :one -SELECT id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain FROM workspace_apps WHERE agent_id = $1 AND name = $2 +SELECT id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level FROM workspace_apps WHERE agent_id = $1 AND name = $2 ` type GetWorkspaceAppByAgentIDAndNameParams struct { @@ -4348,12 +4348,13 @@ func (q *sqlQuerier) GetWorkspaceAppByAgentIDAndName(ctx context.Context, arg Ge &i.HealthcheckThreshold, &i.Health, &i.Subdomain, + &i.SharingLevel, ) return i, err } const getWorkspaceAppsByAgentID = `-- name: GetWorkspaceAppsByAgentID :many -SELECT id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain FROM workspace_apps WHERE agent_id = $1 ORDER BY name ASC +SELECT id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level FROM workspace_apps WHERE agent_id = $1 ORDER BY name ASC ` func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceApp, error) { @@ -4378,6 +4379,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid &i.HealthcheckThreshold, &i.Health, &i.Subdomain, + &i.SharingLevel, ); err != nil { return nil, err } @@ -4393,7 +4395,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid } const getWorkspaceAppsByAgentIDs = `-- name: GetWorkspaceAppsByAgentIDs :many -SELECT id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain FROM workspace_apps WHERE agent_id = ANY($1 :: uuid [ ]) ORDER BY name ASC +SELECT id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level FROM workspace_apps WHERE agent_id = ANY($1 :: uuid [ ]) ORDER BY name ASC ` func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceApp, error) { @@ -4418,6 +4420,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid. &i.HealthcheckThreshold, &i.Health, &i.Subdomain, + &i.SharingLevel, ); err != nil { return nil, err } @@ -4433,7 +4436,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid. } const getWorkspaceAppsCreatedAfter = `-- name: GetWorkspaceAppsCreatedAfter :many -SELECT id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain FROM workspace_apps WHERE created_at > $1 ORDER BY name ASC +SELECT id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level FROM workspace_apps WHERE created_at > $1 ORDER BY name ASC ` func (q *sqlQuerier) GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceApp, error) { @@ -4458,6 +4461,7 @@ func (q *sqlQuerier) GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt &i.HealthcheckThreshold, &i.Health, &i.Subdomain, + &i.SharingLevel, ); err != nil { return nil, err } @@ -4483,13 +4487,14 @@ INSERT INTO command, url, subdomain, + sharing_level, healthcheck_url, healthcheck_interval, healthcheck_threshold, health ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level ` type InsertWorkspaceAppParams struct { @@ -4501,6 +4506,7 @@ type InsertWorkspaceAppParams struct { Command sql.NullString `db:"command" json:"command"` Url sql.NullString `db:"url" json:"url"` Subdomain bool `db:"subdomain" json:"subdomain"` + SharingLevel AppSharingLevel `db:"sharing_level" json:"sharing_level"` HealthcheckUrl string `db:"healthcheck_url" json:"healthcheck_url"` HealthcheckInterval int32 `db:"healthcheck_interval" json:"healthcheck_interval"` HealthcheckThreshold int32 `db:"healthcheck_threshold" json:"healthcheck_threshold"` @@ -4517,6 +4523,7 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace arg.Command, arg.Url, arg.Subdomain, + arg.SharingLevel, arg.HealthcheckUrl, arg.HealthcheckInterval, arg.HealthcheckThreshold, @@ -4536,6 +4543,7 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace &i.HealthcheckThreshold, &i.Health, &i.Subdomain, + &i.SharingLevel, ) return i, err } diff --git a/coderd/database/queries/workspaceapps.sql b/coderd/database/queries/workspaceapps.sql index 3336bfda4ad74..36494a8e9aeb2 100644 --- a/coderd/database/queries/workspaceapps.sql +++ b/coderd/database/queries/workspaceapps.sql @@ -21,13 +21,14 @@ INSERT INTO command, url, subdomain, + sharing_level, healthcheck_url, healthcheck_interval, healthcheck_threshold, health ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *; + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING *; -- name: UpdateWorkspaceAppHealthByID :exec UPDATE diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index 5e409a58d79b7..cc331983ce1ee 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -83,8 +83,8 @@ type OAuth2Configs struct { } const ( - signedOutErrorMessage string = "You are signed out or your session has expired. Please sign in again to continue." - internalErrorMessage string = "An internal error occurred. Please try again or contact the system administrator." + SignedOutErrorMessage = "You are signed out or your session has expired. Please sign in again to continue." + internalErrorMessage = "An internal error occurred. Please try again or contact the system administrator." ) type ExtractAPIKeyConfig struct { @@ -119,21 +119,7 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler { // like workspace applications. write := func(code int, response codersdk.Response) { if cfg.RedirectToLogin { - path := r.URL.Path - if r.URL.RawQuery != "" { - path += "?" + r.URL.RawQuery - } - - q := url.Values{} - q.Add("message", response.Message) - q.Add("redirect", path) - - u := &url.URL{ - Path: "/login", - RawQuery: q.Encode(), - } - - http.Redirect(rw, r, u.String(), http.StatusTemporaryRedirect) + RedirectToLogin(rw, r, response.Message) return } @@ -157,7 +143,7 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler { token := apiTokenFromRequest(r) if token == "" { optionalWrite(http.StatusUnauthorized, codersdk.Response{ - Message: signedOutErrorMessage, + Message: SignedOutErrorMessage, Detail: fmt.Sprintf("Cookie %q or query parameter must be provided.", codersdk.SessionTokenKey), }) return @@ -166,7 +152,7 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler { keyID, keySecret, err := SplitAPIToken(token) if err != nil { optionalWrite(http.StatusUnauthorized, codersdk.Response{ - Message: signedOutErrorMessage, + Message: SignedOutErrorMessage, Detail: "Invalid API key format: " + err.Error(), }) return @@ -176,7 +162,7 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler { if err != nil { if errors.Is(err, sql.ErrNoRows) { optionalWrite(http.StatusUnauthorized, codersdk.Response{ - Message: signedOutErrorMessage, + Message: SignedOutErrorMessage, Detail: "API key is invalid.", }) return @@ -192,7 +178,7 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler { hashedSecret := sha256.Sum256([]byte(keySecret)) if subtle.ConstantTimeCompare(key.HashedSecret, hashedSecret[:]) != 1 { optionalWrite(http.StatusUnauthorized, codersdk.Response{ - Message: signedOutErrorMessage, + Message: SignedOutErrorMessage, Detail: "API key secret is invalid.", }) return @@ -255,7 +241,7 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler { // Checking if the key is expired. if key.ExpiresAt.Before(now) { optionalWrite(http.StatusUnauthorized, codersdk.Response{ - Message: signedOutErrorMessage, + Message: SignedOutErrorMessage, Detail: fmt.Sprintf("API key expired at %q.", key.ExpiresAt.String()), }) return @@ -422,3 +408,23 @@ func SplitAPIToken(token string) (id string, secret string, err error) { return keyID, keySecret, nil } + +// RedirectToLogin redirects the user to the login page with the `message` and +// `redirect` query parameters set. +func RedirectToLogin(rw http.ResponseWriter, r *http.Request, message string) { + path := r.URL.Path + if r.URL.RawQuery != "" { + path += "?" + r.URL.RawQuery + } + + q := url.Values{} + q.Add("message", message) + q.Add("redirect", path) + + u := &url.URL{ + Path: "/login", + RawQuery: q.Encode(), + } + + http.Redirect(rw, r, u.String(), http.StatusTemporaryRedirect) +} diff --git a/coderd/httpmw/organizationparam_test.go b/coderd/httpmw/organizationparam_test.go index faab86228f26f..9ad91e7d2c2ba 100644 --- a/coderd/httpmw/organizationparam_test.go +++ b/coderd/httpmw/organizationparam_test.go @@ -148,7 +148,7 @@ func TestOrganizationParam(t *testing.T) { DB: db, RedirectToLogin: false, }), - httpmw.ExtractUserParam(db), + httpmw.ExtractUserParam(db, false), httpmw.ExtractOrganizationParam(db), httpmw.ExtractOrganizationMemberParam(db), ) @@ -189,7 +189,7 @@ func TestOrganizationParam(t *testing.T) { RedirectToLogin: false, }), httpmw.ExtractOrganizationParam(db), - httpmw.ExtractUserParam(db), + httpmw.ExtractUserParam(db, false), httpmw.ExtractOrganizationMemberParam(db), ) rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) { diff --git a/coderd/httpmw/userparam.go b/coderd/httpmw/userparam.go index 6b852408c2117..74119d503a97b 100644 --- a/coderd/httpmw/userparam.go +++ b/coderd/httpmw/userparam.go @@ -33,8 +33,11 @@ func UserParam(r *http.Request) database.User { return user } -// ExtractUserParam extracts a user from an ID/username in the {user} URL parameter. -func ExtractUserParam(db database.Store) func(http.Handler) http.Handler { +// ExtractUserParam extracts a user from an ID/username in the {user} URL +// parameter. +// +//nolint:revive +func ExtractUserParam(db database.Store, redirectToLoginOnMe bool) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { var ( @@ -53,7 +56,19 @@ func ExtractUserParam(db database.Store) func(http.Handler) http.Handler { } if userQuery == "me" { - user, err = db.GetUserByID(ctx, APIKey(r).UserID) + apiKey, ok := APIKeyOptional(r) + if !ok { + if redirectToLoginOnMe { + RedirectToLogin(rw, r, SignedOutErrorMessage) + return + } + + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Cannot use \"me\" without a valid session.", + }) + return + } + user, err = db.GetUserByID(ctx, apiKey.UserID) if xerrors.Is(err, sql.ErrNoRows) { httpapi.ResourceNotFound(rw) return diff --git a/coderd/httpmw/userparam_test.go b/coderd/httpmw/userparam_test.go index edd7faf128124..9d283f1ea4abd 100644 --- a/coderd/httpmw/userparam_test.go +++ b/coderd/httpmw/userparam_test.go @@ -63,7 +63,7 @@ func TestUserParam(t *testing.T) { r = returnedRequest })).ServeHTTP(rw, r) - httpmw.ExtractUserParam(db)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + httpmw.ExtractUserParam(db, false)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { rw.WriteHeader(http.StatusOK) })).ServeHTTP(rw, r) res := rw.Result() @@ -85,7 +85,7 @@ func TestUserParam(t *testing.T) { routeContext := chi.NewRouteContext() routeContext.URLParams.Add("user", "ben") r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, routeContext)) - httpmw.ExtractUserParam(db)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + httpmw.ExtractUserParam(db, false)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { rw.WriteHeader(http.StatusOK) })).ServeHTTP(rw, r) res := rw.Result() @@ -107,7 +107,7 @@ func TestUserParam(t *testing.T) { routeContext := chi.NewRouteContext() routeContext.URLParams.Add("user", "me") r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, routeContext)) - httpmw.ExtractUserParam(db)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + httpmw.ExtractUserParam(db, false)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { _ = httpmw.UserParam(r) rw.WriteHeader(http.StatusOK) })).ServeHTTP(rw, r) diff --git a/coderd/httpmw/workspaceparam_test.go b/coderd/httpmw/workspaceparam_test.go index bc040b98bcf0f..b44a80c391c8e 100644 --- a/coderd/httpmw/workspaceparam_test.go +++ b/coderd/httpmw/workspaceparam_test.go @@ -305,7 +305,7 @@ func TestWorkspaceAgentByNameParam(t *testing.T) { DB: db, RedirectToLogin: true, }), - httpmw.ExtractUserParam(db), + httpmw.ExtractUserParam(db, false), httpmw.ExtractWorkspaceAndAgentParam(db), ) rtr.Get("/", func(w http.ResponseWriter, r *http.Request) { diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index 1863de54c6b98..adb5cb2edfc35 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -814,6 +814,14 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. health = database.WorkspaceAppHealthInitializing } + sharingLevel := database.AppSharingLevelOwner + switch app.SharingLevel { + case sdkproto.AppSharingLevel_AUTHENTICATED: + sharingLevel = database.AppSharingLevelAuthenticated + case sdkproto.AppSharingLevel_PUBLIC: + sharingLevel = database.AppSharingLevelPublic + } + dbApp, err := db.InsertWorkspaceApp(ctx, database.InsertWorkspaceAppParams{ ID: uuid.New(), CreatedAt: database.Now(), @@ -829,6 +837,7 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. Valid: app.Url != "", }, Subdomain: app.Subdomain, + SharingLevel: sharingLevel, HealthcheckUrl: app.Healthcheck.Url, HealthcheckInterval: app.Healthcheck.Interval, HealthcheckThreshold: app.Healthcheck.Threshold, diff --git a/coderd/users.go b/coderd/users.go index f48708e9b5ed2..5b56509786d6c 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -1207,6 +1207,7 @@ func convertAPIKey(k database.APIKey) codersdk.APIKey { CreatedAt: k.CreatedAt, UpdatedAt: k.UpdatedAt, LoginType: codersdk.LoginType(k.LoginType), + Scope: codersdk.APIKeyScope(k.Scope), LifetimeSeconds: k.LifetimeSeconds, } } diff --git a/coderd/users_test.go b/coderd/users_test.go index 3a7a11b67074d..e4e8b7d661868 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -286,7 +286,7 @@ func TestPostLogin(t *testing.T) { require.Equal(t, int64(86400), key.LifetimeSeconds, "default should be 86400") // tokens have a longer life - token, err := client.CreateToken(ctx, codersdk.Me) + token, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{}) require.NoError(t, err, "make new token api key") split = strings.Split(token.Key, "-") apiKey, err := client.GetAPIKey(ctx, admin.UserID.String(), split[0]) @@ -1202,7 +1202,7 @@ func TestPostTokens(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - apiKey, err := client.CreateToken(ctx, codersdk.Me) + apiKey, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{}) require.NotNil(t, apiKey) require.GreaterOrEqual(t, len(apiKey.Key), 2) require.NoError(t, err) diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index f854fc1c29aca..295beff0d2b7e 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -594,11 +594,12 @@ func convertApps(dbApps []database.WorkspaceApp) []codersdk.WorkspaceApp { apps := make([]codersdk.WorkspaceApp, 0) for _, dbApp := range dbApps { apps = append(apps, codersdk.WorkspaceApp{ - ID: dbApp.ID, - Name: dbApp.Name, - Command: dbApp.Command.String, - Icon: dbApp.Icon, - Subdomain: dbApp.Subdomain, + ID: dbApp.ID, + Name: dbApp.Name, + Command: dbApp.Command.String, + Icon: dbApp.Icon, + Subdomain: dbApp.Subdomain, + SharingLevel: codersdk.WorkspaceAppSharingLevel(dbApp.SharingLevel), Healthcheck: codersdk.Healthcheck{ URL: dbApp.HealthcheckUrl, Interval: dbApp.HealthcheckInterval, diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 1e3e416861ec3..55f73d2c52ae1 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -16,10 +16,12 @@ import ( "time" "github.com/go-chi/chi/v5" + "github.com/google/uuid" "go.opentelemetry.io/otel/trace" "golang.org/x/xerrors" jose "gopkg.in/square/go-jose.v2" + "cdr.dev/slog" "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/httpmw" @@ -32,8 +34,6 @@ import ( const ( // This needs to be a super unique query parameter because we don't want to // conflict with query parameters that users may use. - // TODO: this will make dogfooding harder so come up with a more unique - // solution //nolint:gosec subdomainProxyAPIKeyParam = "coder_application_connect_api_key_35e783" redirectURIQueryParam = "redirect_uri" @@ -51,8 +51,32 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) workspace := httpmw.WorkspaceParam(r) agent := httpmw.WorkspaceAgentParam(r) - if !api.Authorize(r, rbac.ActionCreate, workspace.ApplicationConnectRBAC()) { - httpapi.ResourceNotFound(rw) + // We do not support port proxying on paths, so lookup the app by name. + appName := chi.URLParam(r, "workspaceapp") + app, ok := api.lookupWorkspaceApp(rw, r, agent.ID, appName) + if !ok { + return + } + + appSharingLevel := database.AppSharingLevelOwner + if app.SharingLevel != "" { + appSharingLevel = app.SharingLevel + } + authed, ok := api.fetchWorkspaceApplicationAuth(rw, r, workspace, appSharingLevel) + if !ok { + return + } + if !authed { + _, hasAPIKey := httpmw.APIKeyOptional(r) + if hasAPIKey { + // The request has a valid API key but insufficient permissions. + renderApplicationNotFound(rw, r, api.AccessURL) + return + } + + // Redirect to login as they don't have permission to access the app and + // they aren't signed in. + httpmw.RedirectToLogin(rw, r, httpmw.SignedOutErrorMessage) return } @@ -67,10 +91,9 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) api.proxyWorkspaceApplication(proxyApplication{ Workspace: workspace, Agent: agent, - // We do not support port proxying for paths. - AppName: chi.URLParam(r, "workspaceapp"), - Port: 0, - Path: chiPath, + App: &app, + Port: 0, + Path: chiPath, }, rw, r) } @@ -156,16 +179,30 @@ func (api *API) handleSubdomainApplications(middlewares ...func(http.Handler) ht workspace := httpmw.WorkspaceParam(r) agent := httpmw.WorkspaceAgentParam(r) + var workspaceAppPtr *database.WorkspaceApp + if app.AppName != "" { + workspaceApp, ok := api.lookupWorkspaceApp(rw, r, agent.ID, app.AppName) + if !ok { + return + } + + workspaceAppPtr = &workspaceApp + } + // Verify application auth. This function will redirect or // return an error page if the user doesn't have permission. - if !api.verifyWorkspaceApplicationAuth(rw, r, workspace, host) { + sharingLevel := database.AppSharingLevelOwner + if workspaceAppPtr != nil && workspaceAppPtr.SharingLevel != "" { + sharingLevel = workspaceAppPtr.SharingLevel + } + if !api.verifyWorkspaceApplicationSubdomainAuth(rw, r, host, workspace, sharingLevel) { return } api.proxyWorkspaceApplication(proxyApplication{ Workspace: workspace, Agent: agent, - AppName: app.AppName, + App: workspaceAppPtr, Port: app.Port, Path: r.URL.Path, }, rw, r) @@ -231,22 +268,139 @@ func (api *API) parseWorkspaceApplicationHostname(rw http.ResponseWriter, r *htt return app, true } -// verifyWorkspaceApplicationAuth checks that the request is authorized to -// access the given application. If the user does not have a app session key, -// they will be redirected to the route below. If the user does have a session -// key but insufficient permissions a static error page will be rendered. -func (api *API) verifyWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Request, workspace database.Workspace, host string) bool { - _, ok := httpmw.APIKeyOptional(r) - if ok { - if !api.Authorize(r, rbac.ActionCreate, workspace.ApplicationConnectRBAC()) { - renderApplicationNotFound(rw, r, api.AccessURL) - return false +// lookupWorkspaceApp looks up the workspace application by name in the given +// agent and returns it. If the application is not found or there was a server +// error while looking it up, an HTML error page is returned and false is +// returned so the caller can return early. +func (api *API) lookupWorkspaceApp(rw http.ResponseWriter, r *http.Request, agentID uuid.UUID, appName string) (database.WorkspaceApp, bool) { + app, err := api.Database.GetWorkspaceAppByAgentIDAndName(r.Context(), database.GetWorkspaceAppByAgentIDAndNameParams{ + AgentID: agentID, + Name: appName, + }) + if xerrors.Is(err, sql.ErrNoRows) { + renderApplicationNotFound(rw, r, api.AccessURL) + return database.WorkspaceApp{}, false + } + if err != nil { + site.RenderStaticErrorPage(rw, r, site.ErrorPageData{ + Status: http.StatusInternalServerError, + Title: "Internal Server Error", + Description: "Could not fetch workspace application: " + err.Error(), + RetryEnabled: true, + DashboardURL: api.AccessURL.String(), + }) + return database.WorkspaceApp{}, false + } + + return app, true +} + +func (api *API) authorizeWorkspaceApp(r *http.Request, sharingLevel database.AppSharingLevel, workspace database.Workspace) (bool, error) { + ctx := r.Context() + + // Short circuit if not authenticated. + roles, ok := httpmw.UserAuthorizationOptional(r) + if !ok { + // The user is not authenticated, so they can only access the app if it + // is public. + return sharingLevel == database.AppSharingLevelPublic, nil + } + + // Do a standard RBAC check. This accounts for share level "owner" and any + // other RBAC rules that may be in place. + // + // Regardless of share level or whether it's enabled or not, the owner of + // the workspace can always access applications (as long as their API key's + // scope allows it). + err := api.Authorizer.ByRoleName(ctx, roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), []string{}, rbac.ActionCreate, workspace.ApplicationConnectRBAC()) + if err == nil { + return true, nil + } + + switch sharingLevel { + case database.AppSharingLevelOwner: + // We essentially already did this above with the regular RBAC check. + // Owners can always access their own apps according to RBAC rules, so + // they have already been returned from this function. + case database.AppSharingLevelAuthenticated: + // The user is authenticated at this point, but we need to make sure + // that they have ApplicationConnect permissions to their own + // workspaces. This ensures that the key's scope has permission to + // connect to workspace apps. + object := rbac.ResourceWorkspaceApplicationConnect.WithOwner(roles.ID.String()) + err := api.Authorizer.ByRoleName(ctx, roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), []string{}, rbac.ActionCreate, object) + if err == nil { + return true, nil } + case database.AppSharingLevelPublic: + // We don't really care about scopes and stuff if it's public anyways. + // Someone with a restricted-scope API key could just not submit the + // API key cookie in the request and access the page. + return true, nil + } - // Request should be all good to go! + // No checks were successful. + return false, nil +} + +// fetchWorkspaceApplicationAuth authorizes the user using api.AppAuthorizer +// for a given app share level in the given workspace. The user's authorization +// status is returned. If a server error occurs, a HTML error page is rendered +// and false is returned so the caller can return early. +func (api *API) fetchWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Request, workspace database.Workspace, appSharingLevel database.AppSharingLevel) (authed bool, ok bool) { + ok, err := api.authorizeWorkspaceApp(r, appSharingLevel, workspace) + if err != nil { + api.Logger.Error(r.Context(), "authorize workspace app", slog.Error(err)) + site.RenderStaticErrorPage(rw, r, site.ErrorPageData{ + Status: http.StatusInternalServerError, + Title: "Internal Server Error", + Description: "Could not verify authorization. Please try again or contact an administrator.", + RetryEnabled: true, + DashboardURL: api.AccessURL.String(), + }) + return false, false + } + + return ok, true +} + +// checkWorkspaceApplicationAuth authorizes the user using api.AppAuthorizer +// for a given app share level in the given workspace. If the user is not +// authorized or a server error occurs, a discrete HTML error page is rendered +// and false is returned so the caller can return early. +func (api *API) checkWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Request, workspace database.Workspace, appSharingLevel database.AppSharingLevel) bool { + authed, ok := api.fetchWorkspaceApplicationAuth(rw, r, workspace, appSharingLevel) + if !ok { + return false + } + if !authed { + renderApplicationNotFound(rw, r, api.AccessURL) + return false + } + + return true +} + +// verifyWorkspaceApplicationSubdomainAuth checks that the request is authorized +// to access the given application. If the user does not have a app session key, +// they will be redirected to the route below. If the user does have a session +// key but insufficient permissions a static error page will be rendered. +func (api *API) verifyWorkspaceApplicationSubdomainAuth(rw http.ResponseWriter, r *http.Request, host string, workspace database.Workspace, appSharingLevel database.AppSharingLevel) bool { + authed, ok := api.fetchWorkspaceApplicationAuth(rw, r, workspace, appSharingLevel) + if !ok { + return false + } + if authed { return true } + _, hasAPIKey := httpmw.APIKeyOptional(r) + if hasAPIKey { + // The request has a valid API key but insufficient permissions. + renderApplicationNotFound(rw, r, api.AccessURL) + return false + } + // If the request has the special query param then we need to set a cookie // and strip that query parameter. if encryptedAPIKey := r.URL.Query().Get(subdomainProxyAPIKeyParam); encryptedAPIKey != "" { @@ -421,58 +575,49 @@ type proxyApplication struct { Workspace database.Workspace Agent database.WorkspaceAgent - // Either AppName or Port must be set, but not both. - AppName string - Port uint16 + // Either App or Port must be set, but not both. + App *database.WorkspaceApp + Port uint16 + + // SharingLevel MUST be set to database.AppSharingLevelOwner by default for + // ports. + SharingLevel database.AppSharingLevel // Path must either be empty or have a leading slash. Path string } func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - if !api.Authorize(r, rbac.ActionCreate, proxyApp.Workspace.ApplicationConnectRBAC()) { - httpapi.ResourceNotFound(rw) + + sharingLevel := database.AppSharingLevelOwner + if proxyApp.App != nil && proxyApp.App.SharingLevel != "" { + sharingLevel = proxyApp.App.SharingLevel + } + if !api.checkWorkspaceApplicationAuth(rw, r, proxyApp.Workspace, sharingLevel) { return } // If the app does not exist, but the app name is a port number, then // route to the port as an "anonymous app". We only support HTTP for // port-based URLs. + // + // This is only supported for subdomain-based applications. internalURL := fmt.Sprintf("http://127.0.0.1:%d", proxyApp.Port) // If the app name was used instead, fetch the app from the database so we // can get the internal URL. - if proxyApp.AppName != "" { - app, err := api.Database.GetWorkspaceAppByAgentIDAndName(ctx, database.GetWorkspaceAppByAgentIDAndNameParams{ - AgentID: proxyApp.Agent.ID, - Name: proxyApp.AppName, - }) - if xerrors.Is(err, sql.ErrNoRows) { - renderApplicationNotFound(rw, r, api.AccessURL) - return - } - if err != nil { - site.RenderStaticErrorPage(rw, r, site.ErrorPageData{ - Status: http.StatusInternalServerError, - Title: "Internal Server Error", - Description: "Could not fetch workspace application: " + err.Error(), - RetryEnabled: true, - DashboardURL: api.AccessURL.String(), - }) - return - } - - if !app.Url.Valid { + if proxyApp.App != nil { + if !proxyApp.App.Url.Valid { site.RenderStaticErrorPage(rw, r, site.ErrorPageData{ Status: http.StatusBadRequest, Title: "Bad Request", - Description: fmt.Sprintf("Application %q does not have a URL set.", app.Name), + Description: fmt.Sprintf("Application %q does not have a URL set.", proxyApp.App.Name), RetryEnabled: true, DashboardURL: api.AccessURL.String(), }) return } - internalURL = app.Url.String + internalURL = proxyApp.App.Url.String } appURL, err := url.Parse(internalURL) @@ -692,8 +837,8 @@ func decryptAPIKey(ctx context.Context, db database.Store, encryptedAPIKey strin func renderApplicationNotFound(rw http.ResponseWriter, r *http.Request, accessURL *url.URL) { site.RenderStaticErrorPage(rw, r, site.ErrorPageData{ Status: http.StatusNotFound, - Title: "Application not found", - Description: "The application or workspace you are trying to access does not exist.", + Title: "Application Not Found", + Description: "The application or workspace you are trying to access does not exist or you do not have permission to access it.", RetryEnabled: false, DashboardURL: accessURL.String(), }) diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go index c5bc98666fb67..ed2f536ef2c68 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -7,6 +7,7 @@ import ( "io" "net" "net/http" + "net/http/httputil" "net/url" "strings" "testing" @@ -28,11 +29,13 @@ import ( ) const ( - proxyTestAgentName = "agent-name" - proxyTestAppName = "example" - proxyTestAppQuery = "query=true" - proxyTestAppBody = "hello world" - proxyTestFakeAppName = "fake" + 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" proxyTestSubdomain = "test.coder.com" ) @@ -101,6 +104,8 @@ func setupProxyTest(t *testing.T, workspaceMutators ...func(*codersdk.CreateWork }) user := coderdtest.CreateFirstUser(t, client) authToken := uuid.NewString() + + appURL := fmt.Sprintf("http://127.0.0.1:%d?%s", tcpAddr.Port, proxyTestAppQuery) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, ProvisionDryRun: echo.ProvisionComplete, @@ -118,13 +123,26 @@ func setupProxyTest(t *testing.T, workspaceMutators ...func(*codersdk.CreateWork }, Apps: []*proto.App{ { - Name: proxyTestAppName, - Url: fmt.Sprintf("http://127.0.0.1:%d?%s", tcpAddr.Port, proxyTestAppQuery), - }, { - Name: proxyTestFakeAppName, + Name: proxyTestAppNameFake, + SharingLevel: proto.AppSharingLevel_OWNER, // Hopefully this IP and port doesn't exist. Url: "http://127.1.0.1:65535", }, + { + Name: proxyTestAppNameOwner, + SharingLevel: proto.AppSharingLevel_OWNER, + Url: appURL, + }, + { + Name: proxyTestAppNameAuthenticated, + SharingLevel: proto.AppSharingLevel_AUTHENTICATED, + Url: appURL, + }, + { + Name: proxyTestAppNamePublic, + SharingLevel: proto.AppSharingLevel_PUBLIC, + Url: appURL, + }, }, }}, }}, @@ -180,7 +198,7 @@ func TestWorkspaceAppsProxyPath(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - resp, err := client.Request(ctx, http.MethodGet, "/@me/"+workspace.Name+"/apps/example", nil) + resp, err := client.Request(ctx, http.MethodGet, fmt.Sprintf("/@me/%s/apps/%s", workspace.Name, proxyTestAppNameOwner), nil) require.NoError(t, err) defer resp.Body.Close() @@ -201,7 +219,7 @@ func TestWorkspaceAppsProxyPath(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - resp, err := userClient.Request(ctx, http.MethodGet, "/@me/"+workspace.Name+"/apps/example", nil) + resp, err := userClient.Request(ctx, http.MethodGet, fmt.Sprintf("/@me/%s/apps/%s", workspace.Name, proxyTestAppNameOwner), nil) require.NoError(t, err) defer resp.Body.Close() require.Equal(t, http.StatusNotFound, resp.StatusCode) @@ -213,7 +231,7 @@ func TestWorkspaceAppsProxyPath(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - resp, err := client.Request(ctx, http.MethodGet, "/@me/"+workspace.Name+"/apps/example", nil) + resp, err := client.Request(ctx, http.MethodGet, fmt.Sprintf("/@me/%s/apps/%s", workspace.Name, proxyTestAppNameOwner), nil) require.NoError(t, err) defer resp.Body.Close() require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) @@ -225,7 +243,7 @@ func TestWorkspaceAppsProxyPath(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - resp, err := client.Request(ctx, http.MethodGet, "/@me/"+workspace.Name+"/apps/example/", nil) + resp, err := client.Request(ctx, http.MethodGet, fmt.Sprintf("/@me/%s/apps/%s/", workspace.Name, proxyTestAppNameOwner), nil) require.NoError(t, err) defer resp.Body.Close() require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) @@ -240,7 +258,7 @@ func TestWorkspaceAppsProxyPath(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - resp, err := client.Request(ctx, http.MethodGet, "/@me/"+workspace.Name+"/apps/example/?"+proxyTestAppQuery, nil) + resp, err := client.Request(ctx, http.MethodGet, fmt.Sprintf("/@me/%s/apps/%s/?%s", workspace.Name, proxyTestAppNameOwner, proxyTestAppQuery), nil) require.NoError(t, err) defer resp.Body.Close() body, err := io.ReadAll(resp.Body) @@ -255,7 +273,7 @@ func TestWorkspaceAppsProxyPath(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - resp, err := client.Request(ctx, http.MethodGet, "/@me/"+workspace.Name+"/apps/fake/", nil) + resp, err := client.Request(ctx, http.MethodGet, fmt.Sprintf("/@me/%s/apps/%s/", workspace.Name, proxyTestAppNameFake), nil) require.NoError(t, err) defer resp.Body.Close() require.Equal(t, http.StatusBadGateway, resp.StatusCode) @@ -281,7 +299,7 @@ func TestWorkspaceApplicationAuth(t *testing.T) { require.NoError(t, err) // Try to load the application without authentication. - subdomain := fmt.Sprintf("%s--%s--%s--%s", proxyTestAppName, proxyTestAgentName, workspace.Name, user.Username) + subdomain := fmt.Sprintf("%s--%s--%s--%s", proxyTestAppNameOwner, proxyTestAgentName, workspace.Name, user.Username) u, err := url.Parse(fmt.Sprintf("http://%s.%s/test", subdomain, proxyTestSubdomain)) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) @@ -607,7 +625,7 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - resp, err := userClient.Request(ctx, http.MethodGet, proxyURL(t, proxyTestAppName), nil) + resp, err := userClient.Request(ctx, http.MethodGet, proxyURL(t, proxyTestAppNameOwner), nil) require.NoError(t, err) defer resp.Body.Close() require.Equal(t, http.StatusNotFound, resp.StatusCode) @@ -619,7 +637,7 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - slashlessURL := proxyURL(t, proxyTestAppName, "") + slashlessURL := proxyURL(t, proxyTestAppNameOwner, "") resp, err := client.Request(ctx, http.MethodGet, slashlessURL, nil) require.NoError(t, err) defer resp.Body.Close() @@ -636,7 +654,7 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - querylessURL := proxyURL(t, proxyTestAppName, "/", "") + querylessURL := proxyURL(t, proxyTestAppNameOwner, "/", "") resp, err := client.Request(ctx, http.MethodGet, querylessURL, nil) require.NoError(t, err) defer resp.Body.Close() @@ -653,7 +671,7 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - resp, err := client.Request(ctx, http.MethodGet, proxyURL(t, proxyTestAppName, "/", proxyTestAppQuery), nil) + resp, err := client.Request(ctx, http.MethodGet, proxyURL(t, proxyTestAppNameOwner, "/", proxyTestAppQuery), nil) require.NoError(t, err) defer resp.Body.Close() body, err := io.ReadAll(resp.Body) @@ -683,7 +701,7 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - resp, err := client.Request(ctx, http.MethodGet, proxyURL(t, proxyTestFakeAppName, "/", ""), nil) + resp, err := client.Request(ctx, http.MethodGet, proxyURL(t, proxyTestAppNameFake, "/", ""), nil) require.NoError(t, err) defer resp.Body.Close() require.Equal(t, http.StatusBadGateway, resp.StatusCode) @@ -708,3 +726,168 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) { require.Contains(t, resBody.Message, "Coder reserves ports less than") }) } + +func TestAppSharing(t *testing.T) { + t.Parallel() + + setup := func(t *testing.T) (workspace codersdk.Workspace, agnt codersdk.WorkspaceAgent, user codersdk.User, client *codersdk.Client, clientInOtherOrg *codersdk.Client, clientWithNoAuth *codersdk.Client) { + //nolint:gosec + const password = "password" + + client, _, workspace, _ = setupProxyTest(t) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + t.Cleanup(cancel) + + user, err := client.User(ctx, codersdk.Me) + require.NoError(t, err) + + // Verify that the apps have the correct sharing levels set. + workspaceBuild, err := client.WorkspaceBuild(ctx, workspace.LatestBuild.ID) + require.NoError(t, err) + 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, + } + for _, app := range agnt.Apps { + found[app.Name] = app.SharingLevel + } + require.Equal(t, expected, found, "apps have incorrect sharing levels") + + // Create a user in a different org. + otherOrg, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: "a-different-org", + }) + require.NoError(t, err) + userInOtherOrg, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + Email: "no-template-access@coder.com", + Username: "no-template-access", + Password: password, + OrganizationID: otherOrg.ID, + }) + require.NoError(t, err) + + clientInOtherOrg = codersdk.New(client.URL) + loginRes, err := clientInOtherOrg.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{ + Email: userInOtherOrg.Email, + Password: password, + }) + require.NoError(t, err) + clientInOtherOrg.SessionToken = loginRes.SessionToken + clientInOtherOrg.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + + // Create an unauthenticated codersdk client. + clientWithNoAuth = codersdk.New(client.URL) + clientWithNoAuth.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + + return workspace, agnt, user, client, clientInOtherOrg, clientWithNoAuth + } + + verifyAccess := func(t *testing.T, username, workspaceName, agentName, appName string, client *codersdk.Client, shouldHaveAccess, shouldRedirectToLogin bool) { + t.Helper() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // If the client has a session token, we also want to check that a + // scoped key works. + clients := []*codersdk.Client{client} + if client.SessionToken != "" { + token, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{ + Scope: codersdk.APIKeyScopeApplicationConnect, + }) + require.NoError(t, err) + + scopedClient := codersdk.New(client.URL) + scopedClient.SessionToken = token.Key + scopedClient.HTTPClient.CheckRedirect = client.HTTPClient.CheckRedirect + + clients = append(clients, scopedClient) + } + + for i, client := range clients { + msg := fmt.Sprintf("client %d", i) + + appPath := fmt.Sprintf("/@%s/%s.%s/apps/%s/?%s", username, workspaceName, agentName, appName, proxyTestAppQuery) + res, err := client.Request(ctx, http.MethodGet, appPath, nil) + require.NoError(t, err, msg) + + dump, err := httputil.DumpResponse(res, true) + res.Body.Close() + require.NoError(t, err, msg) + t.Logf("response dump: %s", dump) + + if !shouldHaveAccess { + if shouldRedirectToLogin { + assert.Equal(t, http.StatusTemporaryRedirect, res.StatusCode, "should not have access, expected temporary redirect. "+msg) + location, err := res.Location() + require.NoError(t, err, msg) + assert.Equal(t, "/login", location.Path, "should not have access, expected redirect to /login. "+msg) + } else { + // If the user doesn't have access we return 404 to avoid + // leaking information about the existence of the app. + assert.Equal(t, http.StatusNotFound, res.StatusCode, "should not have access, expected not found. "+msg) + } + } + + if shouldHaveAccess { + assert.Equal(t, http.StatusOK, res.StatusCode, "should have access, expected ok. "+msg) + assert.Contains(t, string(dump), "hello world", "should have access, expected hello world. "+msg) + } + } + } + + t.Run("Level", func(t *testing.T) { + t.Parallel() + + workspace, agent, user, client, clientInOtherOrg, clientWithNoAuth := setup(t) + + t.Run("Owner", func(t *testing.T) { + t.Parallel() + + // Owner should be able to access their own workspace. + verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameOwner, client, true, false) + + // Authenticated users should not have access to a workspace that + // they do not own. + verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameOwner, clientInOtherOrg, false, false) + + // Unauthenticated user should not have any access. + verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameOwner, clientWithNoAuth, false, true) + }) + + t.Run("Authenticated", func(t *testing.T) { + t.Parallel() + + // Owner should be able to access their own workspace. + verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameAuthenticated, client, true, false) + + // Authenticated users should be able to access the workspace. + verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameAuthenticated, clientInOtherOrg, true, false) + + // Unauthenticated user should not have any access. + verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameAuthenticated, clientWithNoAuth, false, true) + }) + + t.Run("Public", func(t *testing.T) { + t.Parallel() + + // Owner should be able to access their own workspace. + verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNamePublic, client, true, false) + + // Authenticated users should be able to access the workspace. + verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNamePublic, clientInOtherOrg, true, false) + + // Unauthenticated user should be able to access the workspace. + verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNamePublic, clientWithNoAuth, true, false) + }) + }) +} diff --git a/codersdk/apikey.go b/codersdk/apikey.go index 44782dde85c6a..1d22cba34526c 100644 --- a/codersdk/apikey.go +++ b/codersdk/apikey.go @@ -13,13 +13,14 @@ import ( type APIKey struct { ID string `json:"id" validate:"required"` // NOTE: do not ever return the HashedSecret - UserID uuid.UUID `json:"user_id" validate:"required"` - LastUsed time.Time `json:"last_used" validate:"required"` - ExpiresAt time.Time `json:"expires_at" validate:"required"` - CreatedAt time.Time `json:"created_at" validate:"required"` - UpdatedAt time.Time `json:"updated_at" validate:"required"` - LoginType LoginType `json:"login_type" validate:"required"` - LifetimeSeconds int64 `json:"lifetime_seconds" validate:"required"` + UserID uuid.UUID `json:"user_id" validate:"required"` + LastUsed time.Time `json:"last_used" validate:"required"` + ExpiresAt time.Time `json:"expires_at" validate:"required"` + CreatedAt time.Time `json:"created_at" validate:"required"` + UpdatedAt time.Time `json:"updated_at" validate:"required"` + LoginType LoginType `json:"login_type" validate:"required"` + Scope APIKeyScope `json:"scope" validate:"required"` + LifetimeSeconds int64 `json:"lifetime_seconds" validate:"required"` } type LoginType string @@ -31,32 +32,51 @@ const ( LoginTypeToken LoginType = "token" ) +type APIKeyScope string + +const ( + APIKeyScopeAll APIKeyScope = "all" + APIKeyScopeApplicationConnect APIKeyScope = "application_connect" +) + +type CreateTokenRequest struct { + Scope APIKeyScope `json:"scope"` +} + +// GenerateAPIKeyResponse contains an API key for a user. +type GenerateAPIKeyResponse struct { + Key string `json:"key"` +} + // CreateToken generates an API key that doesn't expire. -func (c *Client) CreateToken(ctx context.Context, userID string) (*GenerateAPIKeyResponse, error) { - res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys/tokens", userID), nil) +func (c *Client) CreateToken(ctx context.Context, userID string, req CreateTokenRequest) (GenerateAPIKeyResponse, error) { + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys/tokens", userID), req) if err != nil { - return nil, err + return GenerateAPIKeyResponse{}, err } defer res.Body.Close() if res.StatusCode > http.StatusCreated { - return nil, readBodyAsError(res) + return GenerateAPIKeyResponse{}, readBodyAsError(res) } - apiKey := &GenerateAPIKeyResponse{} - return apiKey, json.NewDecoder(res.Body).Decode(apiKey) + + var apiKey GenerateAPIKeyResponse + return apiKey, json.NewDecoder(res.Body).Decode(&apiKey) } // CreateAPIKey generates an API key for the user ID provided. -func (c *Client) CreateAPIKey(ctx context.Context, user string) (*GenerateAPIKeyResponse, error) { +// DEPRECATED: use CreateToken instead. +func (c *Client) CreateAPIKey(ctx context.Context, user string) (GenerateAPIKeyResponse, error) { res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys", user), nil) if err != nil { - return nil, err + return GenerateAPIKeyResponse{}, err } defer res.Body.Close() if res.StatusCode > http.StatusCreated { - return nil, readBodyAsError(res) + return GenerateAPIKeyResponse{}, readBodyAsError(res) } - apiKey := &GenerateAPIKeyResponse{} - return apiKey, json.NewDecoder(res.Body).Decode(apiKey) + + var apiKey GenerateAPIKeyResponse + return apiKey, json.NewDecoder(res.Body).Decode(&apiKey) } // GetTokens list machine API keys. diff --git a/codersdk/users.go b/codersdk/users.go index a37e41d84ec5a..b2452284a2412 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -96,11 +96,6 @@ type LoginWithPasswordResponse struct { SessionToken string `json:"session_token" validate:"required"` } -// GenerateAPIKeyResponse contains an API key for a user. -type GenerateAPIKeyResponse struct { - Key string `json:"key"` -} - type CreateOrganizationRequest struct { Name string `json:"name" validate:"required,username"` } diff --git a/codersdk/workspaceapps.go b/codersdk/workspaceapps.go index 7de4217bac999..6faf4bd3c3ba2 100644 --- a/codersdk/workspaceapps.go +++ b/codersdk/workspaceapps.go @@ -13,6 +13,14 @@ const ( WorkspaceAppHealthUnhealthy WorkspaceAppHealth = "unhealthy" ) +type WorkspaceAppSharingLevel string + +const ( + WorkspaceAppSharingLevelOwner WorkspaceAppSharingLevel = "owner" + WorkspaceAppSharingLevelAuthenticated WorkspaceAppSharingLevel = "authenticated" + WorkspaceAppSharingLevelPublic WorkspaceAppSharingLevel = "public" +) + type WorkspaceApp struct { ID uuid.UUID `json:"id"` // Name is a unique identifier attached to an agent. @@ -25,7 +33,8 @@ type WorkspaceApp struct { // `coder server` or via a hostname-based dev URL. If this is set to true // and there is no app wildcard configured on the server, the app will not // be accessible in the UI. - Subdomain bool `json:"subdomain"` + Subdomain bool `json:"subdomain"` + SharingLevel WorkspaceAppSharingLevel `json:"sharing_level"` // Healthcheck specifies the configuration for checking app health. Healthcheck Healthcheck `json:"healthcheck"` Health WorkspaceAppHealth `json:"health"` diff --git a/dogfood/main.tf b/dogfood/main.tf index 5635223c52227..cc65f6b1c6f42 100644 --- a/dogfood/main.tf +++ b/dogfood/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.0" + version = "0.5.3" } docker = { source = "kreuzwerker/docker" @@ -38,10 +38,12 @@ resource "coder_agent" "dev" { } resource "coder_app" "code-server" { - agent_id = coder_agent.dev.id - name = "code-server" - url = "http://localhost:13337/" - icon = "/icon/code.svg" + agent_id = coder_agent.dev.id + name = "code-server" + url = "http://localhost:13337/" + icon = "/icon/code.svg" + subdomain = false + share = "owner" healthcheck { url = "http://localhost:13337/healthz" @@ -50,7 +52,6 @@ resource "coder_app" "code-server" { } } - resource "docker_volume" "home_volume" { name = "coder-${data.coder_workspace.me.owner}-${data.coder_workspace.me.name}-home" } diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 488d5dc033956..2c341dd13a224 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -35,6 +35,12 @@ func New(ctx context.Context, options *Options) (*API, error) { if options.Keys == nil { options.Keys = Keys } + if options.Options == nil { + options.Options = &coderd.Options{} + } + if options.Options.Authorizer == nil { + options.Options.Authorizer = rbac.NewAuthorizer() + } ctx, cancelFunc := context.WithCancel(ctx) api := &API{ AGPL: coderd.New(options.Options), @@ -92,7 +98,7 @@ func New(ctx context.Context, options *Options) (*API, error) { r.Route("/workspace-quota", func(r chi.Router) { r.Use(apiKeyMiddleware) r.Route("/{user}", func(r chi.Router) { - r.Use(httpmw.ExtractUserParam(options.Database)) + r.Use(httpmw.ExtractUserParam(options.Database, false)) r.Get("/", api.workspaceQuota) }) }) diff --git a/enterprise/coderd/coderdenttest/coderdenttest.go b/enterprise/coderd/coderdenttest/coderdenttest.go index 84b5422982bda..75760b3d4f2eb 100644 --- a/enterprise/coderd/coderdenttest/coderdenttest.go +++ b/enterprise/coderd/coderdenttest/coderdenttest.go @@ -118,19 +118,19 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string { if options.GraceAt.IsZero() { options.GraceAt = time.Now().Add(time.Hour) } - auditLog := int64(0) + var auditLog int64 if options.AuditLog { auditLog = 1 } - browserOnly := int64(0) + var browserOnly int64 if options.BrowserOnly { browserOnly = 1 } - scim := int64(0) + var scim int64 if options.SCIM { scim = 1 } - workspaceQuota := int64(0) + var workspaceQuota int64 if options.WorkspaceQuota { workspaceQuota = 1 } diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go index 3bb40b75b00f8..9fe3cfeaa3064 100644 --- a/enterprise/coderd/workspaceagents_test.go +++ b/enterprise/coderd/workspaceagents_test.go @@ -2,6 +2,7 @@ package coderd_test import ( "context" + "fmt" "net/http" "testing" @@ -16,6 +17,14 @@ import ( "github.com/coder/coder/enterprise/coderd/coderdenttest" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" + "github.com/coder/coder/testutil" +) + +// App names for each app sharing level. +const ( + testAppNameOwner = "test-app-owner" + testAppNameAuthenticated = "test-app-authenticated" + testAppNamePublic = "test-app-public" ) func TestBlockNonBrowser(t *testing.T) { @@ -32,8 +41,8 @@ func TestBlockNonBrowser(t *testing.T) { coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ BrowserOnly: true, }) - id := setupWorkspaceAgent(t, client, user) - _, err := client.DialWorkspaceAgentTailnet(context.Background(), slog.Logger{}, id) + _, agent := setupWorkspaceAgent(t, client, user, 0) + _, err := client.DialWorkspaceAgentTailnet(context.Background(), slog.Logger{}, agent.ID) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusConflict, apiErr.StatusCode()) @@ -49,14 +58,14 @@ func TestBlockNonBrowser(t *testing.T) { coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ BrowserOnly: false, }) - id := setupWorkspaceAgent(t, client, user) - conn, err := client.DialWorkspaceAgentTailnet(context.Background(), slog.Logger{}, id) + _, agent := setupWorkspaceAgent(t, client, user, 0) + conn, err := client.DialWorkspaceAgentTailnet(context.Background(), slog.Logger{}, agent.ID) require.NoError(t, err) _ = conn.Close() }) } -func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.CreateFirstUserResponse) uuid.UUID { +func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.CreateFirstUserResponse, appPort uint16) (codersdk.Workspace, codersdk.WorkspaceAgent) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, @@ -72,6 +81,23 @@ func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.Cr Auth: &proto.Agent_Token{ Token: authToken, }, + Apps: []*proto.App{ + { + Name: testAppNameOwner, + SharingLevel: proto.AppSharingLevel_OWNER, + Url: fmt.Sprintf("http://localhost:%d", appPort), + }, + { + Name: testAppNameAuthenticated, + SharingLevel: proto.AppSharingLevel_AUTHENTICATED, + Url: fmt.Sprintf("http://localhost:%d", appPort), + }, + { + Name: testAppNamePublic, + SharingLevel: proto.AppSharingLevel_PUBLIC, + Url: fmt.Sprintf("http://localhost:%d", appPort), + }, + }, }}, }}, }, @@ -89,9 +115,16 @@ func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.Cr CoordinatorDialer: agentClient.ListenWorkspaceAgentTailnet, Logger: slogtest.Make(t, nil).Named("agent"), }) - defer func() { + t.Cleanup(func() { _ = agentCloser.Close() - }() + }) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) - return resources[0].Agents[0].ID + agnt, err := client.WorkspaceAgent(ctx, resources[0].Agents[0].ID) + require.NoError(t, err) + + return workspace, agnt } diff --git a/examples/templates/aws-ecs-container/main.tf b/examples/templates/aws-ecs-container/main.tf index e21381ad543e1..394bbed6dcec6 100644 --- a/examples/templates/aws-ecs-container/main.tf +++ b/examples/templates/aws-ecs-container/main.tf @@ -6,7 +6,7 @@ terraform { } coder = { source = "coder/coder" - version = "0.5.0" + version = "0.5.3" } } } @@ -110,6 +110,7 @@ resource "coder_app" "code-server" { icon = "/icon/code.svg" url = "http://localhost:13337?folder=/home/coder" subdomain = false + share = "owner" healthcheck { url = "http://localhost:13337/healthz" diff --git a/examples/templates/aws-linux/main.tf b/examples/templates/aws-linux/main.tf index 549afaeccb423..89b69be2472c1 100644 --- a/examples/templates/aws-linux/main.tf +++ b/examples/templates/aws-linux/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.0" + version = "0.5.3" } } } @@ -86,10 +86,12 @@ resource "coder_agent" "main" { } resource "coder_app" "code-server" { - agent_id = coder_agent.main.id - name = "code-server" - url = "http://localhost:13337/?folder=/home/coder" - icon = "/icon/code.svg" + agent_id = coder_agent.main.id + name = "code-server" + url = "http://localhost:13337/?folder=/home/coder" + icon = "/icon/code.svg" + subdomain = false + share = "owner" healthcheck { url = "http://localhost:13337/healthz" diff --git a/examples/templates/aws-windows/main.tf b/examples/templates/aws-windows/main.tf index 960f1d88aa16c..a01ee9a7ebad4 100644 --- a/examples/templates/aws-windows/main.tf +++ b/examples/templates/aws-windows/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.0" + version = "0.5.3" } } } diff --git a/examples/templates/azure-linux/main.tf b/examples/templates/azure-linux/main.tf index e40fb4f5104a4..aa6698e6bcfc0 100644 --- a/examples/templates/azure-linux/main.tf +++ b/examples/templates/azure-linux/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.0" + version = "0.5.3" } azurerm = { source = "hashicorp/azurerm" diff --git a/examples/templates/bare/main.tf b/examples/templates/bare/main.tf index 24bf79b337a14..b51b3e777c3e9 100644 --- a/examples/templates/bare/main.tf +++ b/examples/templates/bare/main.tf @@ -43,10 +43,12 @@ resource "null_resource" "fake-disk" { resource "coder_app" "fake-app" { # Access :8080 in the workspace from the Coder dashboard. - name = "VS Code" - icon = "/icon/code.svg" - agent_id = "fake-compute" - url = "http://localhost:8080" + name = "VS Code" + icon = "/icon/code.svg" + agent_id = "fake-compute" + url = "http://localhost:8080" + subdomain = false + share = "owner" healthcheck { url = "http://localhost:8080/healthz" diff --git a/examples/templates/do-linux/main.tf b/examples/templates/do-linux/main.tf index 375d61c3a1c5c..9f54de8957981 100644 --- a/examples/templates/do-linux/main.tf +++ b/examples/templates/do-linux/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.0" + version = "0.5.3" } digitalocean = { source = "digitalocean/digitalocean" diff --git a/examples/templates/docker-code-server/main.tf b/examples/templates/docker-code-server/main.tf index 3ac932a1d0d63..2e4f4f5b488bc 100644 --- a/examples/templates/docker-code-server/main.tf +++ b/examples/templates/docker-code-server/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.0" + version = "0.5.3" } docker = { source = "kreuzwerker/docker" @@ -38,9 +38,12 @@ resource "coder_agent" "main" { } resource "coder_app" "code-server" { - agent_id = coder_agent.main.id - url = "http://localhost:8080/?folder=/home/coder" - icon = "/icon/code.svg" + agent_id = coder_agent.main.id + name = "code-server" + url = "http://localhost:8080/?folder=/home/coder" + icon = "/icon/code.svg" + subdomain = false + share = "owner" healthcheck { url = "http://localhost:8080/healthz" diff --git a/examples/templates/docker-image-builds/main.tf b/examples/templates/docker-image-builds/main.tf index 4849253a7b442..f5290efdfe440 100644 --- a/examples/templates/docker-image-builds/main.tf +++ b/examples/templates/docker-image-builds/main.tf @@ -3,7 +3,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.0" + version = "0.5.3" } docker = { source = "kreuzwerker/docker" @@ -34,10 +34,12 @@ resource "coder_agent" "main" { } resource "coder_app" "code-server" { - agent_id = coder_agent.main.id - name = "code-server" - url = "http://localhost:13337/?folder=/home/coder" - icon = "/icon/code.svg" + agent_id = coder_agent.main.id + name = "code-server" + url = "http://localhost:13337/?folder=/home/coder" + icon = "/icon/code.svg" + subdomain = false + share = "owner" healthcheck { url = "http://localhost:13337/healthz" diff --git a/examples/templates/docker-with-dotfiles/main.tf b/examples/templates/docker-with-dotfiles/main.tf index 73ab639f69b5e..750dbed2e0e46 100644 --- a/examples/templates/docker-with-dotfiles/main.tf +++ b/examples/templates/docker-with-dotfiles/main.tf @@ -9,7 +9,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.0" + version = "0.5.3" } docker = { source = "kreuzwerker/docker" diff --git a/examples/templates/docker/main.tf b/examples/templates/docker/main.tf index 2463eeef9f766..677dace7f43f6 100644 --- a/examples/templates/docker/main.tf +++ b/examples/templates/docker/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.0" + version = "0.5.3" } docker = { source = "kreuzwerker/docker" @@ -43,10 +43,13 @@ resource "coder_agent" "main" { } resource "coder_app" "code-server" { - agent_id = coder_agent.main.id - name = "code-server" - url = "http://localhost:13337/?folder=/home/coder" - icon = "/icon/code.svg" + agent_id = coder_agent.main.id + name = "code-server" + url = "http://localhost:13337/?folder=/home/coder" + icon = "/icon/code.svg" + subdomain = false + share = "owner" + healthcheck { url = "http://localhost:13337/healthz" interval = 5 diff --git a/examples/templates/gcp-linux/main.tf b/examples/templates/gcp-linux/main.tf index 29e516790c91d..8e184b17c3186 100644 --- a/examples/templates/gcp-linux/main.tf +++ b/examples/templates/gcp-linux/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.0" + version = "0.5.3" } google = { source = "hashicorp/google" @@ -65,6 +65,7 @@ resource "coder_app" "code-server" { icon = "/icon/code.svg" url = "http://localhost:13337?folder=/home/coder" subdomain = false + share = "owner" healthcheck { url = "http://localhost:13337/healthz" diff --git a/examples/templates/gcp-vm-container/main.tf b/examples/templates/gcp-vm-container/main.tf index 508bf4a344bcb..753a2535fe0a9 100644 --- a/examples/templates/gcp-vm-container/main.tf +++ b/examples/templates/gcp-vm-container/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.0" + version = "0.5.3" } google = { source = "hashicorp/google" @@ -55,6 +55,7 @@ resource "coder_app" "code-server" { icon = "/icon/code.svg" url = "http://localhost:13337?folder=/home/coder" subdomain = false + share = "owner" healthcheck { url = "http://localhost:13337/healthz" diff --git a/examples/templates/gcp-windows/main.tf b/examples/templates/gcp-windows/main.tf index e9f65d332c839..5f9a65ac1aef5 100644 --- a/examples/templates/gcp-windows/main.tf +++ b/examples/templates/gcp-windows/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.0" + version = "0.5.3" } google = { source = "hashicorp/google" diff --git a/examples/templates/kubernetes/main.tf b/examples/templates/kubernetes/main.tf index a3656df07e41e..b9d6ebd0baf1f 100644 --- a/examples/templates/kubernetes/main.tf +++ b/examples/templates/kubernetes/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.0" + version = "0.5.3" } kubernetes = { source = "hashicorp/kubernetes" @@ -76,6 +76,7 @@ resource "coder_app" "code-server" { icon = "/icon/code.svg" url = "http://localhost:13337?folder=/home/coder" subdomain = false + share = "owner" healthcheck { url = "http://localhost:13337/healthz" diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index e62962532349f..604c99c7fbbb6 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -30,6 +30,7 @@ type agentAppAttributes struct { Icon string `mapstructure:"icon"` URL string `mapstructure:"url"` Command string `mapstructure:"command"` + Share string `mapstructure:"share"` Subdomain bool `mapstructure:"subdomain"` Healthcheck []appHealthcheckAttributes `mapstructure:"healthcheck"` } @@ -235,6 +236,16 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res } } + sharingLevel := proto.AppSharingLevel_OWNER + switch strings.ToLower(attrs.Share) { + case "owner": + sharingLevel = proto.AppSharingLevel_OWNER + case "authenticated": + sharingLevel = proto.AppSharingLevel_AUTHENTICATED + case "public": + sharingLevel = proto.AppSharingLevel_PUBLIC + } + for _, agents := range resourceAgents { for _, agent := range agents { // Find agents with the matching ID and associate them! @@ -242,12 +253,13 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res continue } agent.Apps = append(agent.Apps, &proto.App{ - Name: attrs.Name, - Command: attrs.Command, - Url: attrs.URL, - Icon: attrs.Icon, - Subdomain: attrs.Subdomain, - Healthcheck: healthcheck, + Name: attrs.Name, + Command: attrs.Command, + Url: attrs.URL, + Icon: attrs.Icon, + Subdomain: attrs.Subdomain, + SharingLevel: sharingLevel, + Healthcheck: healthcheck, }) } } diff --git a/provisioner/terraform/testdata/calling-module/calling-module.tf b/provisioner/terraform/testdata/calling-module/calling-module.tf index 6c6289c30db4f..6bde4e1fd0596 100644 --- a/provisioner/terraform/testdata/calling-module/calling-module.tf +++ b/provisioner/terraform/testdata/calling-module/calling-module.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.0" + version = "0.5.3" } } } diff --git a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf index 3f7a212667e84..ce8eea33b1795 100644 --- a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf +++ b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.0" + version = "0.5.3" } } } diff --git a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tf b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tf index 7ae15e86731f9..2ec5614cd13e4 100644 --- a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tf +++ b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.0" + version = "0.5.3" } } } diff --git a/provisioner/terraform/testdata/instance-id/instance-id.tf b/provisioner/terraform/testdata/instance-id/instance-id.tf index f474e4993aad2..767ed45a63390 100644 --- a/provisioner/terraform/testdata/instance-id/instance-id.tf +++ b/provisioner/terraform/testdata/instance-id/instance-id.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.0" + version = "0.5.3" } } } diff --git a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tf b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tf index 379612d2f3aa2..cae9aac261019 100644 --- a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tf +++ b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.0" + version = "0.5.3" } } } diff --git a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf index 678c600616b3a..446183a9dbb06 100644 --- a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf +++ b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.0" + version = "0.5.3" } } } diff --git a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf index 26734569b66be..ab94dcfbf7550 100644 --- a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf +++ b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.0" + version = "0.5.3" } } } diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index b88fc0ac1db44..0e70c8f919185 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -76,6 +76,55 @@ func (LogLevel) EnumDescriptor() ([]byte, []int) { return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{0} } +type AppSharingLevel int32 + +const ( + AppSharingLevel_OWNER AppSharingLevel = 0 + AppSharingLevel_AUTHENTICATED AppSharingLevel = 1 + AppSharingLevel_PUBLIC AppSharingLevel = 2 +) + +// Enum value maps for AppSharingLevel. +var ( + AppSharingLevel_name = map[int32]string{ + 0: "OWNER", + 1: "AUTHENTICATED", + 2: "PUBLIC", + } + AppSharingLevel_value = map[string]int32{ + "OWNER": 0, + "AUTHENTICATED": 1, + "PUBLIC": 2, + } +) + +func (x AppSharingLevel) Enum() *AppSharingLevel { + p := new(AppSharingLevel) + *p = x + return p +} + +func (x AppSharingLevel) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AppSharingLevel) Descriptor() protoreflect.EnumDescriptor { + return file_provisionersdk_proto_provisioner_proto_enumTypes[1].Descriptor() +} + +func (AppSharingLevel) Type() protoreflect.EnumType { + return &file_provisionersdk_proto_provisioner_proto_enumTypes[1] +} + +func (x AppSharingLevel) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AppSharingLevel.Descriptor instead. +func (AppSharingLevel) EnumDescriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{1} +} + type WorkspaceTransition int32 const ( @@ -109,11 +158,11 @@ func (x WorkspaceTransition) String() string { } func (WorkspaceTransition) Descriptor() protoreflect.EnumDescriptor { - return file_provisionersdk_proto_provisioner_proto_enumTypes[1].Descriptor() + return file_provisionersdk_proto_provisioner_proto_enumTypes[2].Descriptor() } func (WorkspaceTransition) Type() protoreflect.EnumType { - return &file_provisionersdk_proto_provisioner_proto_enumTypes[1] + return &file_provisionersdk_proto_provisioner_proto_enumTypes[2] } func (x WorkspaceTransition) Number() protoreflect.EnumNumber { @@ -122,7 +171,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{1} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{2} } type ParameterSource_Scheme int32 @@ -152,11 +201,11 @@ func (x ParameterSource_Scheme) String() string { } func (ParameterSource_Scheme) Descriptor() protoreflect.EnumDescriptor { - return file_provisionersdk_proto_provisioner_proto_enumTypes[2].Descriptor() + return file_provisionersdk_proto_provisioner_proto_enumTypes[3].Descriptor() } func (ParameterSource_Scheme) Type() protoreflect.EnumType { - return &file_provisionersdk_proto_provisioner_proto_enumTypes[2] + return &file_provisionersdk_proto_provisioner_proto_enumTypes[3] } func (x ParameterSource_Scheme) Number() protoreflect.EnumNumber { @@ -198,11 +247,11 @@ func (x ParameterDestination_Scheme) String() string { } func (ParameterDestination_Scheme) Descriptor() protoreflect.EnumDescriptor { - return file_provisionersdk_proto_provisioner_proto_enumTypes[3].Descriptor() + return file_provisionersdk_proto_provisioner_proto_enumTypes[4].Descriptor() } func (ParameterDestination_Scheme) Type() protoreflect.EnumType { - return &file_provisionersdk_proto_provisioner_proto_enumTypes[3] + return &file_provisionersdk_proto_provisioner_proto_enumTypes[4] } func (x ParameterDestination_Scheme) Number() protoreflect.EnumNumber { @@ -244,11 +293,11 @@ func (x ParameterSchema_TypeSystem) String() string { } func (ParameterSchema_TypeSystem) Descriptor() protoreflect.EnumDescriptor { - return file_provisionersdk_proto_provisioner_proto_enumTypes[4].Descriptor() + return file_provisionersdk_proto_provisioner_proto_enumTypes[5].Descriptor() } func (ParameterSchema_TypeSystem) Type() protoreflect.EnumType { - return &file_provisionersdk_proto_provisioner_proto_enumTypes[4] + return &file_provisionersdk_proto_provisioner_proto_enumTypes[5] } func (x ParameterSchema_TypeSystem) Number() protoreflect.EnumNumber { @@ -850,12 +899,13 @@ type App struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Command string `protobuf:"bytes,2,opt,name=command,proto3" json:"command,omitempty"` - Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"` - Icon string `protobuf:"bytes,4,opt,name=icon,proto3" json:"icon,omitempty"` - Subdomain bool `protobuf:"varint,5,opt,name=subdomain,proto3" json:"subdomain,omitempty"` - Healthcheck *Healthcheck `protobuf:"bytes,6,opt,name=healthcheck,proto3" json:"healthcheck,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Command string `protobuf:"bytes,2,opt,name=command,proto3" json:"command,omitempty"` + Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"` + Icon string `protobuf:"bytes,4,opt,name=icon,proto3" json:"icon,omitempty"` + Subdomain bool `protobuf:"varint,5,opt,name=subdomain,proto3" json:"subdomain,omitempty"` + Healthcheck *Healthcheck `protobuf:"bytes,6,opt,name=healthcheck,proto3" json:"healthcheck,omitempty"` + SharingLevel AppSharingLevel `protobuf:"varint,7,opt,name=sharing_level,json=sharingLevel,proto3,enum=provisioner.AppSharingLevel" json:"sharing_level,omitempty"` } func (x *App) Reset() { @@ -932,6 +982,13 @@ func (x *App) GetHealthcheck() *Healthcheck { return nil } +func (x *App) GetSharingLevel() AppSharingLevel { + if x != nil { + return x.SharingLevel + } + return AppSharingLevel_OWNER +} + // Healthcheck represents configuration for checking for app readiness. type Healthcheck struct { state protoimpl.MessageState @@ -1952,7 +2009,7 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x0a, 0x08, 0x45, 0x6e, 0x76, 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, 0x42, 0x06, 0x0a, 0x04, 0x61, 0x75, 0x74, 0x68, 0x22, 0xb3, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x06, 0x0a, 0x04, 0x61, 0x75, 0x74, 0x68, 0x22, 0xf6, 0x01, 0x0a, 0x03, 0x41, 0x70, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, @@ -1964,110 +2021,118 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x0b, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, - 0x68, 0x65, 0x63, 0x6b, 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, - 0xad, 0x02, 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, 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, - 0xfc, 0x01, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x1a, 0x27, 0x0a, 0x07, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, - 0x72, 0x79, 0x1a, 0x55, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x49, - 0x0a, 0x11, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x63, 0x68, 0x65, - 0x6d, 0x61, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, - 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, - 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x1a, 0x73, 0x0a, 0x08, 0x52, 0x65, 0x73, + 0x68, 0x65, 0x63, 0x6b, 0x12, 0x41, 0x0a, 0x0d, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x5f, + 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x07, 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, 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, 0xad, 0x02, 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, 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, 0xfc, 0x01, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x1a, 0x27, 0x0a, 0x07, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x79, 0x1a, 0x55, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, + 0x65, 0x12, 0x49, 0x0a, 0x11, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x73, + 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, + 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x10, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x1a, 0x73, 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, 0x39, + 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, + 0x61, 0x72, 0x73, 0x65, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, + 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, + 0x65, 0x22, 0xae, 0x07, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x1a, + 0xd1, 0x02, 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, 0x1a, 0xd9, 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x1c, 0x0a, + 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x46, 0x0a, 0x10, 0x70, + 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x0f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x72, 0x79, 0x5f, 0x72, 0x75, + 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x64, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x1a, + 0x08, 0x0a, 0x06, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x1a, 0x80, 0x01, 0x0a, 0x07, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, + 0x72, 0x74, 0x48, 0x00, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x37, 0x0a, 0x06, 0x63, + 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x48, 0x00, 0x52, 0x06, 0x63, 0x61, + 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x1a, 0x6b, 0x0a, 0x08, + 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, 0x1a, 0x77, 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, 0x39, 0x0a, 0x08, 0x63, - 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, - 0x65, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, - 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xae, - 0x07, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0xd1, 0x02, 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, - 0x1a, 0xd9, 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, - 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, - 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x46, 0x0a, 0x10, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, - 0x0f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, - 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, - 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x72, 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x64, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x1a, 0x08, 0x0a, 0x06, - 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x1a, 0x80, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x34, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x48, - 0x00, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x37, 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, - 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x48, 0x00, 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, - 0x6c, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x1a, 0x6b, 0x0a, 0x08, 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, 0x1a, 0x77, 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, 0x3d, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, - 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, - 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 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, + 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x3d, 0x0a, 0x08, 0x63, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, + 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 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, 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, @@ -2100,72 +2165,74 @@ 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, 5) +var file_provisionersdk_proto_provisioner_proto_enumTypes = make([]protoimpl.EnumInfo, 6) var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 24) var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (LogLevel)(0), // 0: provisioner.LogLevel - (WorkspaceTransition)(0), // 1: provisioner.WorkspaceTransition - (ParameterSource_Scheme)(0), // 2: provisioner.ParameterSource.Scheme - (ParameterDestination_Scheme)(0), // 3: provisioner.ParameterDestination.Scheme - (ParameterSchema_TypeSystem)(0), // 4: provisioner.ParameterSchema.TypeSystem - (*Empty)(nil), // 5: provisioner.Empty - (*ParameterSource)(nil), // 6: provisioner.ParameterSource - (*ParameterDestination)(nil), // 7: provisioner.ParameterDestination - (*ParameterValue)(nil), // 8: provisioner.ParameterValue - (*ParameterSchema)(nil), // 9: provisioner.ParameterSchema - (*Log)(nil), // 10: provisioner.Log - (*InstanceIdentityAuth)(nil), // 11: provisioner.InstanceIdentityAuth - (*Agent)(nil), // 12: provisioner.Agent - (*App)(nil), // 13: provisioner.App - (*Healthcheck)(nil), // 14: provisioner.Healthcheck - (*Resource)(nil), // 15: provisioner.Resource - (*Parse)(nil), // 16: provisioner.Parse - (*Provision)(nil), // 17: provisioner.Provision - nil, // 18: provisioner.Agent.EnvEntry - (*Resource_Metadata)(nil), // 19: provisioner.Resource.Metadata - (*Parse_Request)(nil), // 20: provisioner.Parse.Request - (*Parse_Complete)(nil), // 21: provisioner.Parse.Complete - (*Parse_Response)(nil), // 22: provisioner.Parse.Response - (*Provision_Metadata)(nil), // 23: provisioner.Provision.Metadata - (*Provision_Start)(nil), // 24: provisioner.Provision.Start - (*Provision_Cancel)(nil), // 25: provisioner.Provision.Cancel - (*Provision_Request)(nil), // 26: provisioner.Provision.Request - (*Provision_Complete)(nil), // 27: provisioner.Provision.Complete - (*Provision_Response)(nil), // 28: provisioner.Provision.Response + (AppSharingLevel)(0), // 1: provisioner.AppSharingLevel + (WorkspaceTransition)(0), // 2: provisioner.WorkspaceTransition + (ParameterSource_Scheme)(0), // 3: provisioner.ParameterSource.Scheme + (ParameterDestination_Scheme)(0), // 4: provisioner.ParameterDestination.Scheme + (ParameterSchema_TypeSystem)(0), // 5: provisioner.ParameterSchema.TypeSystem + (*Empty)(nil), // 6: provisioner.Empty + (*ParameterSource)(nil), // 7: provisioner.ParameterSource + (*ParameterDestination)(nil), // 8: provisioner.ParameterDestination + (*ParameterValue)(nil), // 9: provisioner.ParameterValue + (*ParameterSchema)(nil), // 10: provisioner.ParameterSchema + (*Log)(nil), // 11: provisioner.Log + (*InstanceIdentityAuth)(nil), // 12: provisioner.InstanceIdentityAuth + (*Agent)(nil), // 13: provisioner.Agent + (*App)(nil), // 14: provisioner.App + (*Healthcheck)(nil), // 15: provisioner.Healthcheck + (*Resource)(nil), // 16: provisioner.Resource + (*Parse)(nil), // 17: provisioner.Parse + (*Provision)(nil), // 18: provisioner.Provision + nil, // 19: provisioner.Agent.EnvEntry + (*Resource_Metadata)(nil), // 20: provisioner.Resource.Metadata + (*Parse_Request)(nil), // 21: provisioner.Parse.Request + (*Parse_Complete)(nil), // 22: provisioner.Parse.Complete + (*Parse_Response)(nil), // 23: provisioner.Parse.Response + (*Provision_Metadata)(nil), // 24: provisioner.Provision.Metadata + (*Provision_Start)(nil), // 25: provisioner.Provision.Start + (*Provision_Cancel)(nil), // 26: provisioner.Provision.Cancel + (*Provision_Request)(nil), // 27: provisioner.Provision.Request + (*Provision_Complete)(nil), // 28: provisioner.Provision.Complete + (*Provision_Response)(nil), // 29: provisioner.Provision.Response } var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{ - 2, // 0: provisioner.ParameterSource.scheme:type_name -> provisioner.ParameterSource.Scheme - 3, // 1: provisioner.ParameterDestination.scheme:type_name -> provisioner.ParameterDestination.Scheme - 3, // 2: provisioner.ParameterValue.destination_scheme:type_name -> provisioner.ParameterDestination.Scheme - 6, // 3: provisioner.ParameterSchema.default_source:type_name -> provisioner.ParameterSource - 7, // 4: provisioner.ParameterSchema.default_destination:type_name -> provisioner.ParameterDestination - 4, // 5: provisioner.ParameterSchema.validation_type_system:type_name -> provisioner.ParameterSchema.TypeSystem + 3, // 0: provisioner.ParameterSource.scheme:type_name -> provisioner.ParameterSource.Scheme + 4, // 1: provisioner.ParameterDestination.scheme:type_name -> provisioner.ParameterDestination.Scheme + 4, // 2: provisioner.ParameterValue.destination_scheme:type_name -> provisioner.ParameterDestination.Scheme + 7, // 3: provisioner.ParameterSchema.default_source:type_name -> provisioner.ParameterSource + 8, // 4: provisioner.ParameterSchema.default_destination:type_name -> provisioner.ParameterDestination + 5, // 5: provisioner.ParameterSchema.validation_type_system:type_name -> provisioner.ParameterSchema.TypeSystem 0, // 6: provisioner.Log.level:type_name -> provisioner.LogLevel - 18, // 7: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry - 13, // 8: provisioner.Agent.apps:type_name -> provisioner.App - 14, // 9: provisioner.App.healthcheck:type_name -> provisioner.Healthcheck - 12, // 10: provisioner.Resource.agents:type_name -> provisioner.Agent - 19, // 11: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata - 9, // 12: provisioner.Parse.Complete.parameter_schemas:type_name -> provisioner.ParameterSchema - 10, // 13: provisioner.Parse.Response.log:type_name -> provisioner.Log - 21, // 14: provisioner.Parse.Response.complete:type_name -> provisioner.Parse.Complete - 1, // 15: provisioner.Provision.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition - 8, // 16: provisioner.Provision.Start.parameter_values:type_name -> provisioner.ParameterValue - 23, // 17: provisioner.Provision.Start.metadata:type_name -> provisioner.Provision.Metadata - 24, // 18: provisioner.Provision.Request.start:type_name -> provisioner.Provision.Start - 25, // 19: provisioner.Provision.Request.cancel:type_name -> provisioner.Provision.Cancel - 15, // 20: provisioner.Provision.Complete.resources:type_name -> provisioner.Resource - 10, // 21: provisioner.Provision.Response.log:type_name -> provisioner.Log - 27, // 22: provisioner.Provision.Response.complete:type_name -> provisioner.Provision.Complete - 20, // 23: provisioner.Provisioner.Parse:input_type -> provisioner.Parse.Request - 26, // 24: provisioner.Provisioner.Provision:input_type -> provisioner.Provision.Request - 22, // 25: provisioner.Provisioner.Parse:output_type -> provisioner.Parse.Response - 28, // 26: provisioner.Provisioner.Provision:output_type -> provisioner.Provision.Response - 25, // [25:27] is the sub-list for method output_type - 23, // [23:25] is the sub-list for method input_type - 23, // [23:23] is the sub-list for extension type_name - 23, // [23:23] is the sub-list for extension extendee - 0, // [0:23] is the sub-list for field type_name + 19, // 7: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry + 14, // 8: provisioner.Agent.apps:type_name -> provisioner.App + 15, // 9: provisioner.App.healthcheck:type_name -> provisioner.Healthcheck + 1, // 10: provisioner.App.sharing_level:type_name -> provisioner.AppSharingLevel + 13, // 11: provisioner.Resource.agents:type_name -> provisioner.Agent + 20, // 12: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata + 10, // 13: provisioner.Parse.Complete.parameter_schemas:type_name -> provisioner.ParameterSchema + 11, // 14: provisioner.Parse.Response.log:type_name -> provisioner.Log + 22, // 15: provisioner.Parse.Response.complete:type_name -> provisioner.Parse.Complete + 2, // 16: provisioner.Provision.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition + 9, // 17: provisioner.Provision.Start.parameter_values:type_name -> provisioner.ParameterValue + 24, // 18: provisioner.Provision.Start.metadata:type_name -> provisioner.Provision.Metadata + 25, // 19: provisioner.Provision.Request.start:type_name -> provisioner.Provision.Start + 26, // 20: provisioner.Provision.Request.cancel:type_name -> provisioner.Provision.Cancel + 16, // 21: provisioner.Provision.Complete.resources:type_name -> provisioner.Resource + 11, // 22: provisioner.Provision.Response.log:type_name -> provisioner.Log + 28, // 23: provisioner.Provision.Response.complete:type_name -> provisioner.Provision.Complete + 21, // 24: provisioner.Provisioner.Parse:input_type -> provisioner.Parse.Request + 27, // 25: provisioner.Provisioner.Provision:input_type -> provisioner.Provision.Request + 23, // 26: provisioner.Provisioner.Parse:output_type -> provisioner.Parse.Response + 29, // 27: provisioner.Provisioner.Provision:output_type -> provisioner.Provision.Response + 26, // [26:28] is the sub-list for method output_type + 24, // [24:26] is the sub-list for method input_type + 24, // [24:24] is the sub-list for extension type_name + 24, // [24:24] is the sub-list for extension extendee + 0, // [0:24] is the sub-list for field type_name } func init() { file_provisionersdk_proto_provisioner_proto_init() } @@ -2472,7 +2539,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_provisionersdk_proto_provisioner_proto_rawDesc, - NumEnums: 5, + NumEnums: 6, NumMessages: 24, NumExtensions: 0, NumServices: 1, diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index 26af34f280646..bc6ab711a4add 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -87,6 +87,12 @@ message Agent { } } +enum AppSharingLevel { + OWNER = 0; + AUTHENTICATED = 1; + PUBLIC = 2; +} + // App represents a dev-accessible application on the workspace. message App { string name = 1; @@ -95,6 +101,7 @@ message App { string icon = 4; bool subdomain = 5; Healthcheck healthcheck = 6; + AppSharingLevel sharing_level = 7; } // Healthcheck represents configuration for checking for app readiness. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 09c2da80fb9e2..0c9cf3792bb84 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -9,6 +9,7 @@ export interface APIKey { readonly created_at: string readonly updated_at: string readonly login_type: LoginType + readonly scope: APIKeyScope readonly lifetime_seconds: number } @@ -218,6 +219,11 @@ export interface CreateTestAuditLogRequest { readonly resource_id?: string } +// From codersdk/apikey.go +export interface CreateTokenRequest { + readonly scope: APIKeyScope +} + // From codersdk/users.go export interface CreateUserRequest { readonly email: string @@ -344,7 +350,7 @@ export interface Feature { readonly actual?: number } -// From codersdk/users.go +// From codersdk/apikey.go export interface GenerateAPIKeyResponse { readonly key: string } @@ -783,6 +789,7 @@ export interface WorkspaceApp { readonly command?: string readonly icon?: string readonly subdomain: boolean + readonly sharing_level: WorkspaceAppSharingLevel readonly healthcheck: Healthcheck readonly health: WorkspaceAppHealth } @@ -851,6 +858,9 @@ export interface WorkspaceResourceMetadata { readonly sensitive: boolean } +// From codersdk/apikey.go +export type APIKeyScope = "all" | "application_connect" + // From codersdk/audit.go export type AuditAction = "create" | "delete" | "write" @@ -931,6 +941,9 @@ export type WorkspaceAppHealth = | "initializing" | "unhealthy" +// From codersdk/workspaceapps.go +export type WorkspaceAppSharingLevel = "authenticated" | "owner" | "public" + // From codersdk/workspacebuilds.go export type WorkspaceStatus = | "canceled" diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index b7c7750158130..6e26a4fee5944 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -199,6 +199,7 @@ export const MockWorkspaceApp: TypesGen.WorkspaceApp = { icon: "", subdomain: false, health: "disabled", + sharing_level: "owner", healthcheck: { url: "", interval: 0,