From 6b68642355cbb36a332205e45817b36e1dbe2ee9 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 5 Oct 2022 19:01:50 +0000 Subject: [PATCH 01/12] feat: app sharing pt.1 --- coderd/coderd.go | 9 + coderd/database/dump.sql | 10 +- .../000055_app_share_level.down.sql | 5 + .../migrations/000055_app_share_level.up.sql | 15 ++ coderd/database/models.go | 22 ++ coderd/database/queries.sql.go | 15 +- coderd/workspaceapps.go | 177 +++++++++++++---- enterprise/coderd/appsharing.go | 74 +++++++ enterprise/coderd/appsharing_test.go | 188 ++++++++++++++++++ enterprise/coderd/workspaceagents_test.go | 48 ++++- provisioner/terraform/resources.go | 1 + 11 files changed, 508 insertions(+), 56 deletions(-) create mode 100644 coderd/database/migrations/000055_app_share_level.down.sql create mode 100644 coderd/database/migrations/000055_app_share_level.up.sql create mode 100644 enterprise/coderd/appsharing.go create mode 100644 enterprise/coderd/appsharing_test.go diff --git a/coderd/coderd.go b/coderd/coderd.go index 011f29927d92e..080d40ed98638 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -57,6 +57,7 @@ type Options struct { Auditor audit.Auditor WorkspaceQuotaEnforcer workspacequota.Enforcer + AppAuthorizer AppAuthorizer AgentConnectionUpdateFrequency time.Duration AgentInactiveDisconnectTimeout time.Duration // APIRateLimit is the minutely throughput rate limit per user or ip. @@ -126,6 +127,11 @@ func New(options *Options) *API { if options.WorkspaceQuotaEnforcer == nil { options.WorkspaceQuotaEnforcer = workspacequota.NewNop() } + if options.AppAuthorizer == nil { + options.AppAuthorizer = &AGPLAppAuthorizer{ + RBAC: options.Authorizer, + } + } siteCacheDir := options.CacheDir if siteCacheDir != "" { @@ -154,9 +160,11 @@ func New(options *Options) *API { metricsCache: metricsCache, Auditor: atomic.Pointer[audit.Auditor]{}, WorkspaceQuotaEnforcer: atomic.Pointer[workspacequota.Enforcer]{}, + AppAuthorizer: atomic.Pointer[AppAuthorizer]{}, } api.Auditor.Store(&options.Auditor) api.WorkspaceQuotaEnforcer.Store(&options.WorkspaceQuotaEnforcer) + api.AppAuthorizer.Store(&options.AppAuthorizer) api.workspaceAgentCache = wsconncache.New(api.dialWorkspaceAgentTailnet, 0) api.derpServer = derp.NewServer(key.NewNode(), tailnet.Logger(options.Logger)) oauthConfigs := &httpmw.OAuth2Configs{ @@ -517,6 +525,7 @@ type API struct { Auditor atomic.Pointer[audit.Auditor] WorkspaceClientCoordinateOverride atomic.Pointer[func(rw http.ResponseWriter) bool] WorkspaceQuotaEnforcer atomic.Pointer[workspacequota.Enforcer] + AppAuthorizer atomic.Pointer[AppAuthorizer] HTTPAuth *HTTPAuthorizer // APIHandler serves "/api/v2" diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 9413e0822d154..ff40676f3221e 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -5,6 +5,13 @@ CREATE TYPE api_key_scope AS ENUM ( 'application_connect' ); +CREATE TYPE app_share_level AS ENUM ( + 'owner', + 'template', + 'authenticated', + 'public' +); + CREATE TYPE audit_action AS ENUM ( 'create', 'write', @@ -356,7 +363,8 @@ CREATE TABLE workspace_apps ( healthcheck_url text DEFAULT ''::text NOT NULL, 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 + health workspace_app_health DEFAULT 'disabled'::public.workspace_app_health NOT NULL, + share_level app_share_level DEFAULT 'owner'::public.app_share_level NOT NULL ); CREATE TABLE workspace_builds ( diff --git a/coderd/database/migrations/000055_app_share_level.down.sql b/coderd/database/migrations/000055_app_share_level.down.sql new file mode 100644 index 0000000000000..501e62880df44 --- /dev/null +++ b/coderd/database/migrations/000055_app_share_level.down.sql @@ -0,0 +1,5 @@ +-- Drop column share_level from workspace_apps +ALTER TABLE workspace_apps DROP COLUMN share_level; + +-- Drop type app_share_level +DROP TYPE app_share_level; diff --git a/coderd/database/migrations/000055_app_share_level.up.sql b/coderd/database/migrations/000055_app_share_level.up.sql new file mode 100644 index 0000000000000..6f4dcfceceec4 --- /dev/null +++ b/coderd/database/migrations/000055_app_share_level.up.sql @@ -0,0 +1,15 @@ +-- Add enum app_share_level +CREATE TYPE app_share_level AS ENUM ( + -- only the workspace owner can access the app + 'owner', + -- the workspace owner and other users that can read the workspace template + -- can access the app + 'template', + -- 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 share_level column to workspace_apps table +ALTER TABLE workspace_apps ADD COLUMN share_level app_share_level NOT NULL DEFAULT 'owner'::app_share_level; diff --git a/coderd/database/models.go b/coderd/database/models.go index bfd7d3f7af1ad..846fd85aa573d 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -33,6 +33,27 @@ func (e *APIKeyScope) Scan(src interface{}) error { return nil } +type AppShareLevel string + +const ( + AppShareLevelOwner AppShareLevel = "owner" + AppShareLevelTemplate AppShareLevel = "template" + AppShareLevelAuthenticated AppShareLevel = "authenticated" + AppShareLevelPublic AppShareLevel = "public" +) + +func (e *AppShareLevel) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = AppShareLevel(s) + case string: + *e = AppShareLevel(s) + default: + return fmt.Errorf("unsupported scan type for AppShareLevel: %T", src) + } + return nil +} + type AuditAction string const ( @@ -610,6 +631,7 @@ type WorkspaceApp struct { HealthcheckInterval int32 `db:"healthcheck_interval" json:"healthcheck_interval"` HealthcheckThreshold int32 `db:"healthcheck_threshold" json:"healthcheck_threshold"` Health WorkspaceAppHealth `db:"health" json:"health"` + ShareLevel AppShareLevel `db:"share_level" json:"share_level"` } type WorkspaceBuild struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 66a08f1ccfe66..18f34f9fe214e 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3895,7 +3895,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, relative_path, healthcheck_url, healthcheck_interval, healthcheck_threshold, health FROM workspace_apps WHERE agent_id = $1 AND name = $2 +SELECT id, created_at, agent_id, name, icon, command, url, relative_path, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, share_level FROM workspace_apps WHERE agent_id = $1 AND name = $2 ` type GetWorkspaceAppByAgentIDAndNameParams struct { @@ -3919,12 +3919,13 @@ func (q *sqlQuerier) GetWorkspaceAppByAgentIDAndName(ctx context.Context, arg Ge &i.HealthcheckInterval, &i.HealthcheckThreshold, &i.Health, + &i.ShareLevel, ) return i, err } const getWorkspaceAppsByAgentID = `-- name: GetWorkspaceAppsByAgentID :many -SELECT id, created_at, agent_id, name, icon, command, url, relative_path, healthcheck_url, healthcheck_interval, healthcheck_threshold, health FROM workspace_apps WHERE agent_id = $1 ORDER BY name ASC +SELECT id, created_at, agent_id, name, icon, command, url, relative_path, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, share_level FROM workspace_apps WHERE agent_id = $1 ORDER BY name ASC ` func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceApp, error) { @@ -3949,6 +3950,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid &i.HealthcheckInterval, &i.HealthcheckThreshold, &i.Health, + &i.ShareLevel, ); err != nil { return nil, err } @@ -3964,7 +3966,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, relative_path, healthcheck_url, healthcheck_interval, healthcheck_threshold, health FROM workspace_apps WHERE agent_id = ANY($1 :: uuid [ ]) ORDER BY name ASC +SELECT id, created_at, agent_id, name, icon, command, url, relative_path, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, share_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) { @@ -3989,6 +3991,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid. &i.HealthcheckInterval, &i.HealthcheckThreshold, &i.Health, + &i.ShareLevel, ); err != nil { return nil, err } @@ -4004,7 +4007,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, relative_path, healthcheck_url, healthcheck_interval, healthcheck_threshold, health FROM workspace_apps WHERE created_at > $1 ORDER BY name ASC +SELECT id, created_at, agent_id, name, icon, command, url, relative_path, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, share_level FROM workspace_apps WHERE created_at > $1 ORDER BY name ASC ` func (q *sqlQuerier) GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceApp, error) { @@ -4029,6 +4032,7 @@ func (q *sqlQuerier) GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt &i.HealthcheckInterval, &i.HealthcheckThreshold, &i.Health, + &i.ShareLevel, ); err != nil { return nil, err } @@ -4060,7 +4064,7 @@ INSERT INTO health ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, agent_id, name, icon, command, url, relative_path, healthcheck_url, healthcheck_interval, healthcheck_threshold, health + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, agent_id, name, icon, command, url, relative_path, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, share_level ` type InsertWorkspaceAppParams struct { @@ -4107,6 +4111,7 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace &i.HealthcheckInterval, &i.HealthcheckThreshold, &i.Health, + &i.ShareLevel, ) return i, err } diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 4b0b6eb94c5d5..e2108b9744789 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -15,10 +15,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" @@ -38,6 +40,32 @@ const ( redirectURIQueryParam = "redirect_uri" ) +type AppAuthorizer interface { + // Authorize returns true if the request is authorized to access an app at + // share level `appShareLevel` in `workspace`. An error is only returned if + // there is a processing error. "Unauthorized" errors should not be + // returned. + Authorize(r *http.Request, db database.Store, appShareLevel database.AppShareLevel, workspace database.Workspace) (bool, error) +} + +type AGPLAppAuthorizer struct { + RBAC rbac.Authorizer +} + +var _ AppAuthorizer = &AGPLAppAuthorizer{} + +// Authorize provides an AGPL implementation of AppAuthorizer. It does not +// support app sharing levels as they are an enterprise feature. +func (a AGPLAppAuthorizer) Authorize(r *http.Request, _ database.Store, _ database.AppShareLevel, workspace database.Workspace) (bool, error) { + roles, ok := httpmw.UserAuthorizationOptional(r) + if !ok { + return false, nil + } + + err := a.RBAC.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), rbac.ActionCreate, workspace.ApplicationConnectRBAC()) + return err == nil, nil +} + func (api *API) appHost(rw http.ResponseWriter, r *http.Request) { httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.GetAppHostResponse{ Host: api.AppHostname, @@ -50,8 +78,18 @@ 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 + } + + shareLevel := database.AppShareLevelOwner + if app.ShareLevel != "" { + shareLevel = app.ShareLevel + } + if !api.checkWorkspaceApplicationAuth(rw, r, workspace, shareLevel) { return } @@ -66,10 +104,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) } @@ -155,16 +192,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) { + shareLevel := database.AppShareLevelOwner + if workspaceAppPtr != nil && workspaceAppPtr.ShareLevel != "" { + shareLevel = workspaceAppPtr.ShareLevel + } + if !api.verifyWorkspaceApplicationAuth(rw, r, host, workspace, shareLevel) { return } api.proxyWorkspaceApplication(proxyApplication{ Workspace: workspace, Agent: agent, - AppName: app.AppName, + App: workspaceAppPtr, Port: app.Port, Path: r.URL.Path, }, rw, r) @@ -230,20 +281,67 @@ func (api *API) parseWorkspaceApplicationHostname(rw http.ResponseWriter, r *htt return app, true } +// 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 +} + +// 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, appShareLevel database.AppShareLevel) bool { + ok, err := (*api.AppAuthorizer.Load()).Authorize(r, api.Database, appShareLevel, 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 + } + if !ok { + renderApplicationNotFound(rw, r, api.AccessURL) + return false + } + + return 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 { +func (api *API) verifyWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Request, host string, workspace database.Workspace, appShareLevel database.AppShareLevel) bool { _, ok := httpmw.APIKeyOptional(r) if ok { - if !api.Authorize(r, rbac.ActionCreate, workspace.ApplicationConnectRBAC()) { - renderApplicationNotFound(rw, r, api.AccessURL) - return false - } - - // Request should be all good to go! - return true + // Request should be all good to go as long as it passes auth checks! + return api.checkWorkspaceApplicationAuth(rw, r, workspace, appShareLevel) } // If the request has the special query param then we need to set a cookie @@ -420,58 +518,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 + + // ShareLevel MUST be set to database.AppShareLevelOwner by default for + // ports. + ShareLevel database.AppShareLevel // 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) + + shareLevel := database.AppShareLevelOwner + if proxyApp.App != nil && proxyApp.App.ShareLevel != "" { + shareLevel = proxyApp.App.ShareLevel + } + if !api.checkWorkspaceApplicationAuth(rw, r, proxyApp.Workspace, shareLevel) { 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) diff --git a/enterprise/coderd/appsharing.go b/enterprise/coderd/appsharing.go new file mode 100644 index 0000000000000..d968cd9ce7b2d --- /dev/null +++ b/enterprise/coderd/appsharing.go @@ -0,0 +1,74 @@ +package coderd + +import ( + "net/http" + + "golang.org/x/xerrors" + + agplcoderd "github.com/coder/coder/coderd" + "github.com/coder/coder/coderd/database" + "github.com/coder/coder/coderd/httpmw" + "github.com/coder/coder/coderd/rbac" +) + +// EnterpriseAppAuthorizer provides an enterprise implementation of +// agplcoderd.AppAuthorizer that allows apps to be shared at certain levels. +type EnterpriseAppAuthorizer struct { + RBAC rbac.Authorizer +} + +var _ agplcoderd.AppAuthorizer = &EnterpriseAppAuthorizer{} + +// Authorize implements agplcoderd.AppAuthorizer. +func (a *EnterpriseAppAuthorizer) Authorize(r *http.Request, db database.Store, shareLevel database.AppShareLevel, 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 shareLevel == database.AppShareLevelPublic, nil + } + + // Do a standard RBAC check. This accounts for share level "owner" and any + // other RBAC rules that may be in place. + err := a.RBAC.ByRoleName(ctx, roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), rbac.ActionCreate, workspace.ApplicationConnectRBAC()) + if err == nil { + return true, nil + } + + switch shareLevel { + case database.AppShareLevelOwner: + // We essentially already did this above. + case database.AppShareLevelTemplate: + // Check if the user has access to the same template as the workspace. + template, err := db.GetTemplateByID(ctx, workspace.TemplateID) + if err != nil { + return false, xerrors.Errorf("get template %q: %w", workspace.TemplateID, err) + } + + err = a.RBAC.ByRoleName(ctx, roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), rbac.ActionRead, template.RBACObject()) + if err == nil { + return true, nil + } + case database.AppShareLevelAuthenticated: + // 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 := a.RBAC.ByRoleName(ctx, roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), rbac.ActionCreate, object) + if err == nil { + return true, nil + } + case database.AppShareLevelPublic: + // 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 + } + + // No checks were successful. + return false, nil +} diff --git a/enterprise/coderd/appsharing_test.go b/enterprise/coderd/appsharing_test.go new file mode 100644 index 0000000000000..8fda2fbb799d3 --- /dev/null +++ b/enterprise/coderd/appsharing_test.go @@ -0,0 +1,188 @@ +package coderd_test + +import ( + "context" + "fmt" + "net" + "net/http" + "net/http/httputil" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/codersdk" + "github.com/coder/coder/enterprise/coderd/coderdenttest" + "github.com/coder/coder/testutil" +) + +func TestEnterpriseAppAuthorizer(t *testing.T) { + t.Parallel() + + //nolint:gosec + const password = "password" + + // Create a hello world server. + //nolint:gosec + ln, err := net.Listen("tcp", ":0") + require.NoError(t, err) + server := http.Server{ + ReadHeaderTimeout: time.Minute, + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("Hello World")) + }), + } + t.Cleanup(func() { + _ = server.Close() + _ = ln.Close() + }) + go server.Serve(ln) + tcpAddr, ok := ln.Addr().(*net.TCPAddr) + require.True(t, ok) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // Setup a user, template with apps, workspace on a coderdtest using the + // EnterpriseAppAuthorizer. + client := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }, + }) + firstUser := coderdtest.CreateFirstUser(t, client) + user, err := client.User(ctx, firstUser.UserID.String()) + require.NoError(t, err) + coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ + // TODO: license stuff + BrowserOnly: true, + }) + workspace, agent := setupWorkspaceAgent(t, client, firstUser, uint16(tcpAddr.Port)) + + // Create a user in the same org (should be able to read the template). + userWithTemplateAccess, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + Email: "template-access@coder.com", + Username: "template-access", + Password: password, + OrganizationID: firstUser.OrganizationID, + }) + require.NoError(t, err) + + clientWithTemplateAccess := codersdk.New(client.URL) + loginRes, err := clientWithTemplateAccess.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{ + Email: userWithTemplateAccess.Email, + Password: password, + }) + require.NoError(t, err) + clientWithTemplateAccess.SessionToken = loginRes.SessionToken + + // Create a user in a different org (should not be able to read the + // template). + differentOrg, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: "a different org", + }) + require.NoError(t, err) + userWithNoTemplateAccess, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + Email: "no-template-access@coder.com", + Username: "no-template-access", + Password: password, + OrganizationID: differentOrg.ID, + }) + require.NoError(t, err) + + clientWithNoTemplateAccess := codersdk.New(client.URL) + loginRes, err = clientWithNoTemplateAccess.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{ + Email: userWithNoTemplateAccess.Email, + Password: password, + }) + require.NoError(t, err) + clientWithNoTemplateAccess.SessionToken = loginRes.SessionToken + + // Create an unauthenticated codersdk client. + clientWithNoAuth := codersdk.New(client.URL) + + verifyAccess := func(t *testing.T, appName string, client *codersdk.Client, shouldHaveAccess bool) { + t.Helper() + + appPath := fmt.Sprintf("/@%s/%s.%s/apps/%s", user.Username, workspace.Name, agent.Name, appName) + res, err := client.Request(ctx, http.MethodGet, appPath, nil) + require.NoError(t, err) + defer res.Body.Close() + + dump, err := httputil.DumpResponse(res, true) + require.NoError(t, err) + t.Logf("response dump: %s", dump) + + if !shouldHaveAccess { + require.Equal(t, http.StatusForbidden, res.StatusCode) + } + + if shouldHaveAccess { + require.Equal(t, http.StatusOK, res.StatusCode) + require.Contains(t, string(dump), "Hello World") + } + } + + t.Run("LevelOwner", func(t *testing.T) { + t.Parallel() + + // Owner should be able to access their own workspace. + verifyAccess(t, testAppNameOwner, client, true) + + // User with or without template access should not have access to a + // workspace that they do not own. + verifyAccess(t, testAppNameOwner, clientWithTemplateAccess, false) + verifyAccess(t, testAppNameOwner, clientWithNoTemplateAccess, false) + + // Unauthenticated user should not have any access. + verifyAccess(t, testAppNameOwner, clientWithNoAuth, false) + }) + + t.Run("LevelTemplate", func(t *testing.T) { + t.Parallel() + + // Owner should be able to access their own workspace. + verifyAccess(t, testAppNameTemplate, client, true) + + // User with template access should be able to access the workspace. + verifyAccess(t, testAppNameTemplate, clientWithTemplateAccess, true) + + // User without template access should not have access to a workspace + // that they do not own. + verifyAccess(t, testAppNameTemplate, clientWithNoTemplateAccess, false) + + // Unauthenticated user should not have any access. + verifyAccess(t, testAppNameTemplate, clientWithNoAuth, false) + }) + + t.Run("LevelAuthenticated", func(t *testing.T) { + t.Parallel() + + // Owner should be able to access their own workspace. + verifyAccess(t, testAppNameAuthenticated, client, true) + + // User with or without template access should be able to access the + // workspace. + verifyAccess(t, testAppNameAuthenticated, clientWithTemplateAccess, true) + verifyAccess(t, testAppNameAuthenticated, clientWithNoTemplateAccess, true) + + // Unauthenticated user should not have any access. + verifyAccess(t, testAppNameAuthenticated, clientWithNoAuth, false) + }) + + t.Run("LevelPublic", func(t *testing.T) { + t.Parallel() + + // Owner should be able to access their own workspace. + verifyAccess(t, testAppNamePublic, client, true) + + // User with or without template access should be able to access the + // workspace. + verifyAccess(t, testAppNamePublic, clientWithTemplateAccess, true) + verifyAccess(t, testAppNamePublic, clientWithNoTemplateAccess, true) + + // Unauthenticated user should be able to access the workspace. + verifyAccess(t, testAppNamePublic, clientWithNoAuth, true) + }) +} diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go index 3bb40b75b00f8..90a4ecfe6c139 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,15 @@ 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" + testAppNameTemplate = "test-app-template" + testAppNameAuthenticated = "test-app-authenticated" + testAppNamePublic = "test-app-public" ) func TestBlockNonBrowser(t *testing.T) { @@ -32,8 +42,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 +59,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 +82,25 @@ func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.Cr Auth: &proto.Agent_Token{ Token: authToken, }, + // TODO: sharing levels + Apps: []*proto.App{ + { + Name: testAppNameOwner, + Url: fmt.Sprintf("http://localhost:%d", appPort), + }, + { + Name: testAppNameTemplate, + Url: fmt.Sprintf("http://localhost:%d", appPort), + }, + { + Name: testAppNameAuthenticated, + Url: fmt.Sprintf("http://localhost:%d", appPort), + }, + { + Name: testAppNamePublic, + Url: fmt.Sprintf("http://localhost:%d", appPort), + }, + }, }}, }}, }, @@ -92,6 +121,13 @@ func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.Cr defer 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 + agent, err := client.WorkspaceAgent(ctx, resources[0].Agents[0].ID) + require.NoError(t, err) + + return workspace, agent } diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index 22685c566120a..ca2507796d2b6 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -32,6 +32,7 @@ type agentAppAttributes struct { Icon string `mapstructure:"icon"` URL string `mapstructure:"url"` Command string `mapstructure:"command"` + ShareLevel string `mapstructure:"share_level"` RelativePath bool `mapstructure:"relative_path"` Healthcheck []appHealthcheckAttributes `mapstructure:"healthcheck"` } From 67a7057da520c36d60c9657b5585c9f9628eb11e Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 5 Oct 2022 20:30:49 +0000 Subject: [PATCH 02/12] feat: app sharing pt.2 --- codersdk/features.go | 12 ++- enterprise/coderd/appsharing.go | 31 +++++- enterprise/coderd/appsharing_test.go | 42 +++++--- enterprise/coderd/coderd.go | 96 ++++++++++++++++--- .../coderd/coderdenttest/coderdenttest.go | 67 +++++++------ enterprise/coderd/licenses.go | 11 ++- 6 files changed, 192 insertions(+), 67 deletions(-) diff --git a/codersdk/features.go b/codersdk/features.go index 3b57d6eeb3853..25213d3e0f828 100644 --- a/codersdk/features.go +++ b/codersdk/features.go @@ -15,11 +15,12 @@ const ( ) const ( - FeatureUserLimit = "user_limit" - FeatureAuditLog = "audit_log" - FeatureBrowserOnly = "browser_only" - FeatureSCIM = "scim" - FeatureWorkspaceQuota = "workspace_quota" + FeatureUserLimit = "user_limit" + FeatureAuditLog = "audit_log" + FeatureBrowserOnly = "browser_only" + FeatureSCIM = "scim" + FeatureWorkspaceQuota = "workspace_quota" + FeatureApplicationSharing = "application_sharing" ) var FeatureNames = []string{ @@ -28,6 +29,7 @@ var FeatureNames = []string{ FeatureBrowserOnly, FeatureSCIM, FeatureWorkspaceQuota, + FeatureApplicationSharing, } type Feature struct { diff --git a/enterprise/coderd/appsharing.go b/enterprise/coderd/appsharing.go index d968cd9ce7b2d..4ee9fad54844e 100644 --- a/enterprise/coderd/appsharing.go +++ b/enterprise/coderd/appsharing.go @@ -14,7 +14,11 @@ import ( // EnterpriseAppAuthorizer provides an enterprise implementation of // agplcoderd.AppAuthorizer that allows apps to be shared at certain levels. type EnterpriseAppAuthorizer struct { - RBAC rbac.Authorizer + RBAC rbac.Authorizer + LevelOwnerAllowed bool + LevelTemplateAllowed bool + LevelAuthenticatedAllowed bool + LevelPublicAllowed bool } var _ agplcoderd.AppAuthorizer = &EnterpriseAppAuthorizer{} @@ -23,6 +27,28 @@ var _ agplcoderd.AppAuthorizer = &EnterpriseAppAuthorizer{} func (a *EnterpriseAppAuthorizer) Authorize(r *http.Request, db database.Store, shareLevel database.AppShareLevel, workspace database.Workspace) (bool, error) { ctx := r.Context() + // TODO: better errors displayed to the user in this case + switch shareLevel { + case database.AppShareLevelOwner: + if !a.LevelOwnerAllowed { + return false, nil + } + case database.AppShareLevelTemplate: + if !a.LevelTemplateAllowed { + return false, nil + } + case database.AppShareLevelAuthenticated: + if !a.LevelAuthenticatedAllowed { + return false, nil + } + case database.AppShareLevelPublic: + if !a.LevelPublicAllowed { + return false, nil + } + default: + return false, xerrors.Errorf("unknown workspace app sharing level %q", shareLevel) + } + // Short circuit if not authenticated. roles, ok := httpmw.UserAuthorizationOptional(r) if !ok { @@ -33,6 +59,9 @@ func (a *EnterpriseAppAuthorizer) Authorize(r *http.Request, db database.Store, // 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, the owner of the workspace can always access + // applications. err := a.RBAC.ByRoleName(ctx, roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), rbac.ActionCreate, workspace.ApplicationConnectRBAC()) if err == nil { return true, nil diff --git a/enterprise/coderd/appsharing_test.go b/enterprise/coderd/appsharing_test.go index 8fda2fbb799d3..bd03dcf294bdb 100644 --- a/enterprise/coderd/appsharing_test.go +++ b/enterprise/coderd/appsharing_test.go @@ -12,14 +12,13 @@ import ( "github.com/stretchr/testify/require" "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/coderd/database" "github.com/coder/coder/codersdk" "github.com/coder/coder/enterprise/coderd/coderdenttest" "github.com/coder/coder/testutil" ) -func TestEnterpriseAppAuthorizer(t *testing.T) { - t.Parallel() - +func setupAppAuthorizerTest(t *testing.T, allowedSharingLevels []database.AppShareLevel) (workspace codersdk.Workspace, agent codersdk.WorkspaceAgent, user codersdk.User, client *codersdk.Client, clientWithTemplateAccess *codersdk.Client, clientWithNoTemplateAccess *codersdk.Client, clientWithNoAuth *codersdk.Client) { //nolint:gosec const password = "password" @@ -42,23 +41,23 @@ func TestEnterpriseAppAuthorizer(t *testing.T) { require.True(t, ok) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + t.Cleanup(cancel) // Setup a user, template with apps, workspace on a coderdtest using the // EnterpriseAppAuthorizer. - client := coderdenttest.New(t, &coderdenttest.Options{ + client = coderdenttest.New(t, &coderdenttest.Options{ + AllowedApplicationSharingLevels: allowedSharingLevels, Options: &coderdtest.Options{ IncludeProvisionerDaemon: true, }, }) firstUser := coderdtest.CreateFirstUser(t, client) - user, err := client.User(ctx, firstUser.UserID.String()) + user, err = client.User(ctx, firstUser.UserID.String()) require.NoError(t, err) coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - // TODO: license stuff - BrowserOnly: true, + ApplicationSharing: true, }) - workspace, agent := setupWorkspaceAgent(t, client, firstUser, uint16(tcpAddr.Port)) + workspace, agent = setupWorkspaceAgent(t, client, firstUser, uint16(tcpAddr.Port)) // Create a user in the same org (should be able to read the template). userWithTemplateAccess, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ @@ -69,7 +68,7 @@ func TestEnterpriseAppAuthorizer(t *testing.T) { }) require.NoError(t, err) - clientWithTemplateAccess := codersdk.New(client.URL) + clientWithTemplateAccess = codersdk.New(client.URL) loginRes, err := clientWithTemplateAccess.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{ Email: userWithTemplateAccess.Email, Password: password, @@ -80,7 +79,7 @@ func TestEnterpriseAppAuthorizer(t *testing.T) { // Create a user in a different org (should not be able to read the // template). differentOrg, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "a different org", + Name: "a-different-org", }) require.NoError(t, err) userWithNoTemplateAccess, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ @@ -91,7 +90,7 @@ func TestEnterpriseAppAuthorizer(t *testing.T) { }) require.NoError(t, err) - clientWithNoTemplateAccess := codersdk.New(client.URL) + clientWithNoTemplateAccess = codersdk.New(client.URL) loginRes, err = clientWithNoTemplateAccess.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{ Email: userWithNoTemplateAccess.Email, Password: password, @@ -100,11 +99,28 @@ func TestEnterpriseAppAuthorizer(t *testing.T) { clientWithNoTemplateAccess.SessionToken = loginRes.SessionToken // Create an unauthenticated codersdk client. - clientWithNoAuth := codersdk.New(client.URL) + clientWithNoAuth = codersdk.New(client.URL) + + return workspace, agent, user, client, clientWithTemplateAccess, clientWithNoTemplateAccess, clientWithNoAuth +} + +func TestEnterpriseAppAuthorizer(t *testing.T) { + t.Parallel() + + // For the purposes of these tests we allow all levels. + workspace, agent, user, client, clientWithTemplateAccess, clientWithNoTemplateAccess, clientWithNoAuth := setupAppAuthorizerTest(t, []database.AppShareLevel{ + database.AppShareLevelOwner, + database.AppShareLevelTemplate, + database.AppShareLevelAuthenticated, + database.AppShareLevelPublic, + }) verifyAccess := func(t *testing.T, appName string, client *codersdk.Client, shouldHaveAccess bool) { t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + appPath := fmt.Sprintf("/@%s/%s.%s/apps/%s", user.Username, workspace.Name, agent.Name, appName) res, err := client.Request(ctx, http.MethodGet, appPath, nil) require.NoError(t, err) diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 96e25a256ed81..f9279451cc675 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -15,8 +15,10 @@ import ( "cdr.dev/slog" "github.com/coder/coder/coderd" + "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/httpmw" + "github.com/coder/coder/coderd/rbac" "github.com/coder/coder/codersdk" "github.com/coder/coder/enterprise/audit" "github.com/coder/coder/enterprise/audit/backends" @@ -32,6 +34,42 @@ 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() + } + if options.Options.AppAuthorizer == nil { + var ( + // The default is that only level "owner" should be allowed. + levelOwnerAllowed = len(options.AllowedApplicationSharingLevels) == 0 + levelTemplateAllowed = false + levelAuthenticatedAllowed = false + levelPublicAllowed = false + ) + for _, v := range options.AllowedApplicationSharingLevels { + switch v { + case database.AppShareLevelOwner: + levelOwnerAllowed = true + case database.AppShareLevelTemplate: + levelTemplateAllowed = true + case database.AppShareLevelAuthenticated: + levelAuthenticatedAllowed = true + case database.AppShareLevelPublic: + levelPublicAllowed = true + default: + return nil, xerrors.Errorf("unknown workspace app sharing level %q", v) + } + } + options.Options.AppAuthorizer = &EnterpriseAppAuthorizer{ + RBAC: options.Options.Authorizer, + LevelOwnerAllowed: levelOwnerAllowed, + LevelTemplateAllowed: levelTemplateAllowed, + LevelAuthenticatedAllowed: levelAuthenticatedAllowed, + LevelPublicAllowed: levelPublicAllowed, + } + } ctx, cancelFunc := context.WithCancel(ctx) api := &API{ AGPL: coderd.New(options.Options), @@ -42,10 +80,11 @@ func New(ctx context.Context, options *Options) (*API, error) { Entitlement: codersdk.EntitlementNotEntitled, Enabled: false, }, - auditLogs: codersdk.EntitlementNotEntitled, - browserOnly: codersdk.EntitlementNotEntitled, - scim: codersdk.EntitlementNotEntitled, - workspaceQuota: codersdk.EntitlementNotEntitled, + auditLogs: codersdk.EntitlementNotEntitled, + browserOnly: codersdk.EntitlementNotEntitled, + scim: codersdk.EntitlementNotEntitled, + workspaceQuota: codersdk.EntitlementNotEntitled, + applicationSharing: codersdk.EntitlementNotEntitled, }, cancelEntitlementsLoop: cancelFunc, } @@ -106,6 +145,9 @@ type Options struct { BrowserOnly bool SCIMAPIKey []byte UserWorkspaceQuota int + // Defaults to []database.AppShareLevel{database.AppShareLevelOwner} which + // essentially means "function identically to AGPL Coder". + AllowedApplicationSharingLevels []database.AppShareLevel EntitlementsUpdateInterval time.Duration Keys map[string]ed25519.PublicKey @@ -121,12 +163,13 @@ type API struct { } type entitlements struct { - hasLicense bool - activeUsers codersdk.Feature - auditLogs codersdk.Entitlement - browserOnly codersdk.Entitlement - scim codersdk.Entitlement - workspaceQuota codersdk.Entitlement + hasLicense bool + activeUsers codersdk.Feature + auditLogs codersdk.Entitlement + browserOnly codersdk.Entitlement + scim codersdk.Entitlement + workspaceQuota codersdk.Entitlement + applicationSharing codersdk.Entitlement } func (api *API) Close() error { @@ -150,10 +193,11 @@ func (api *API) updateEntitlements(ctx context.Context) error { Enabled: false, Entitlement: codersdk.EntitlementNotEntitled, }, - auditLogs: codersdk.EntitlementNotEntitled, - scim: codersdk.EntitlementNotEntitled, - browserOnly: codersdk.EntitlementNotEntitled, - workspaceQuota: codersdk.EntitlementNotEntitled, + auditLogs: codersdk.EntitlementNotEntitled, + scim: codersdk.EntitlementNotEntitled, + browserOnly: codersdk.EntitlementNotEntitled, + workspaceQuota: codersdk.EntitlementNotEntitled, + applicationSharing: codersdk.EntitlementNotEntitled, } // Here we loop through licenses to detect enabled features. @@ -195,6 +239,9 @@ func (api *API) updateEntitlements(ctx context.Context) error { if claims.Features.WorkspaceQuota > 0 { entitlements.workspaceQuota = entitlement } + if claims.Features.ApplicationSharing > 0 { + entitlements.applicationSharing = entitlement + } } if entitlements.auditLogs != api.entitlements.auditLogs { @@ -308,6 +355,27 @@ func (api *API) serveEntitlements(rw http.ResponseWriter, r *http.Request) { } } + // App sharing is disabled if no levels are allowed or the only allowed + // level is "owner". + appSharingEnabled := true + if len(api.AllowedApplicationSharingLevels) == 0 || (len(api.AllowedApplicationSharingLevels) == 1 && api.AllowedApplicationSharingLevels[0] == database.AppShareLevelOwner) { + appSharingEnabled = false + } + resp.Features[codersdk.FeatureApplicationSharing] = codersdk.Feature{ + Entitlement: entitlements.applicationSharing, + Enabled: appSharingEnabled, + } + if appSharingEnabled { + if entitlements.applicationSharing == codersdk.EntitlementNotEntitled { + resp.Warnings = append(resp.Warnings, + "Application sharing is enabled but your license is not entitled to this feature.") + } + if entitlements.applicationSharing == codersdk.EntitlementGracePeriod { + resp.Warnings = append(resp.Warnings, + "Application sharing is enabled but your license for this feature is expired.") + } + } + httpapi.Write(ctx, rw, http.StatusOK, resp) } diff --git a/enterprise/coderd/coderdenttest/coderdenttest.go b/enterprise/coderd/coderdenttest/coderdenttest.go index acf3a206ef04d..bc063294cf6f3 100644 --- a/enterprise/coderd/coderdenttest/coderdenttest.go +++ b/enterprise/coderd/coderdenttest/coderdenttest.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/require" "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/coderd/database" "github.com/coder/coder/codersdk" "github.com/coder/coder/enterprise/coderd" ) @@ -36,11 +37,12 @@ func init() { type Options struct { *coderdtest.Options - AuditLogging bool - BrowserOnly bool - EntitlementsUpdateInterval time.Duration - SCIMAPIKey []byte - UserWorkspaceQuota int + AuditLogging bool + BrowserOnly bool + EntitlementsUpdateInterval time.Duration + SCIMAPIKey []byte + UserWorkspaceQuota int + AllowedApplicationSharingLevels []database.AppShareLevel } // New constructs a codersdk client connected to an in-memory Enterprise API instance. @@ -58,12 +60,13 @@ func NewWithAPI(t *testing.T, options *Options) (*codersdk.Client, io.Closer, *c } srv, cancelFunc, oop := coderdtest.NewOptions(t, options.Options) coderAPI, err := coderd.New(context.Background(), &coderd.Options{ - AuditLogging: options.AuditLogging, - BrowserOnly: options.BrowserOnly, - SCIMAPIKey: options.SCIMAPIKey, - UserWorkspaceQuota: options.UserWorkspaceQuota, - Options: oop, - EntitlementsUpdateInterval: options.EntitlementsUpdateInterval, + AuditLogging: options.AuditLogging, + BrowserOnly: options.BrowserOnly, + SCIMAPIKey: options.SCIMAPIKey, + UserWorkspaceQuota: options.UserWorkspaceQuota, + AllowedApplicationSharingLevels: options.AllowedApplicationSharingLevels, + Options: oop, + EntitlementsUpdateInterval: options.EntitlementsUpdateInterval, Keys: map[string]ed25519.PublicKey{ testKeyID: testPublicKey, }, @@ -83,15 +86,16 @@ func NewWithAPI(t *testing.T, options *Options) (*codersdk.Client, io.Closer, *c } type LicenseOptions struct { - AccountType string - AccountID string - GraceAt time.Time - ExpiresAt time.Time - UserLimit int64 - AuditLog bool - BrowserOnly bool - SCIM bool - WorkspaceQuota bool + AccountType string + AccountID string + GraceAt time.Time + ExpiresAt time.Time + UserLimit int64 + AuditLog bool + BrowserOnly bool + SCIM bool + WorkspaceQuota bool + ApplicationSharing bool } // AddLicense generates a new license with the options provided and inserts it. @@ -111,22 +115,26 @@ 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 } + var applicationSharing int64 + if options.ApplicationSharing { + applicationSharing = 1 + } c := &coderd.Claims{ RegisteredClaims: jwt.RegisteredClaims{ @@ -140,11 +148,12 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string { AccountID: options.AccountID, Version: coderd.CurrentVersion, Features: coderd.Features{ - UserLimit: options.UserLimit, - AuditLog: auditLog, - BrowserOnly: browserOnly, - SCIM: scim, - WorkspaceQuota: workspaceQuota, + UserLimit: options.UserLimit, + AuditLog: auditLog, + BrowserOnly: browserOnly, + SCIM: scim, + WorkspaceQuota: workspaceQuota, + ApplicationSharing: applicationSharing, }, } tok := jwt.NewWithClaims(jwt.SigningMethodEdDSA, c) diff --git a/enterprise/coderd/licenses.go b/enterprise/coderd/licenses.go index 9d43bbe6c2996..a0afce40cb49e 100644 --- a/enterprise/coderd/licenses.go +++ b/enterprise/coderd/licenses.go @@ -45,11 +45,12 @@ var key20220812 []byte var Keys = map[string]ed25519.PublicKey{"2022-08-12": ed25519.PublicKey(key20220812)} type Features struct { - UserLimit int64 `json:"user_limit"` - AuditLog int64 `json:"audit_log"` - BrowserOnly int64 `json:"browser_only"` - SCIM int64 `json:"scim"` - WorkspaceQuota int64 `json:"workspace_quota"` + UserLimit int64 `json:"user_limit"` + AuditLog int64 `json:"audit_log"` + BrowserOnly int64 `json:"browser_only"` + SCIM int64 `json:"scim"` + WorkspaceQuota int64 `json:"workspace_quota"` + ApplicationSharing int64 `json:"application_sharing"` } type Claims struct { From d7403ec8ea1cc148c09e40206dad5dcd7a6564e9 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 6 Oct 2022 16:16:45 +0000 Subject: [PATCH 03/12] feat: app sharing pt.3 --- coderd/coderd.go | 20 +- coderd/database/databasefake/databasefake.go | 5 + coderd/database/dump.sql | 4 +- ....sql => 000056_app_sharing_level.down.sql} | 0 ...up.sql => 000056_app_sharing_level.up.sql} | 8 +- coderd/database/models.go | 20 +- coderd/database/queries.sql.go | 23 +- coderd/database/queries/workspaceapps.sql | 3 +- coderd/httpmw/apikey.go | 51 +- coderd/httpmw/organizationparam_test.go | 4 +- coderd/httpmw/userparam.go | 19 +- coderd/httpmw/userparam_test.go | 6 +- coderd/httpmw/workspaceparam_test.go | 2 +- coderd/provisionerdaemons.go | 11 + coderd/workspaceagents.go | 11 +- coderd/workspaceapps.go | 103 ++-- codersdk/workspaceapps.go | 12 +- enterprise/cli/features_test.go | 4 +- enterprise/coderd/appsharing.go | 62 +-- enterprise/coderd/appsharing_test.go | 259 ++++++++-- enterprise/coderd/coderd.go | 16 +- .../coderd/coderdenttest/coderdenttest.go | 2 +- enterprise/coderd/licenses_test.go | 22 +- enterprise/coderd/workspaceagents_test.go | 24 +- provisioner/terraform/resources.go | 14 +- provisionersdk/proto/provisioner.pb.go | 465 ++++++++++-------- provisionersdk/proto/provisioner.proto | 8 + site/src/api/typesGenerated.ts | 4 + 28 files changed, 765 insertions(+), 417 deletions(-) rename coderd/database/migrations/{000056_app_share_level.down.sql => 000056_app_sharing_level.down.sql} (100%) rename coderd/database/migrations/{000056_app_share_level.up.sql => 000056_app_sharing_level.up.sql} (58%) diff --git a/coderd/coderd.go b/coderd/coderd.go index 080d40ed98638..5da9e61a3c0d4 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -204,7 +204,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. @@ -221,8 +221,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), ) @@ -312,7 +322,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) @@ -391,7 +401,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 c5923ca6cffca..3b74820304a71 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -2060,6 +2060,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, @@ -2070,6 +2074,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 556888c33cc9e..f215edd9ab093 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -5,7 +5,7 @@ CREATE TYPE api_key_scope AS ENUM ( 'application_connect' ); -CREATE TYPE app_share_level AS ENUM ( +CREATE TYPE app_sharing_level AS ENUM ( 'owner', 'template', 'authenticated', @@ -364,7 +364,7 @@ CREATE TABLE workspace_apps ( 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, - share_level app_share_level DEFAULT 'owner'::public.app_share_level 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/000056_app_share_level.down.sql b/coderd/database/migrations/000056_app_sharing_level.down.sql similarity index 100% rename from coderd/database/migrations/000056_app_share_level.down.sql rename to coderd/database/migrations/000056_app_sharing_level.down.sql diff --git a/coderd/database/migrations/000056_app_share_level.up.sql b/coderd/database/migrations/000056_app_sharing_level.up.sql similarity index 58% rename from coderd/database/migrations/000056_app_share_level.up.sql rename to coderd/database/migrations/000056_app_sharing_level.up.sql index 6f4dcfceceec4..de6ce3fceb7de 100644 --- a/coderd/database/migrations/000056_app_share_level.up.sql +++ b/coderd/database/migrations/000056_app_sharing_level.up.sql @@ -1,5 +1,5 @@ --- Add enum app_share_level -CREATE TYPE app_share_level AS ENUM ( +-- Add enum app_sharing_level +CREATE TYPE app_sharing_level AS ENUM ( -- only the workspace owner can access the app 'owner', -- the workspace owner and other users that can read the workspace template @@ -11,5 +11,5 @@ CREATE TYPE app_share_level AS ENUM ( 'public' ); --- Add share_level column to workspace_apps table -ALTER TABLE workspace_apps ADD COLUMN share_level app_share_level NOT NULL DEFAULT 'owner'::app_share_level; +-- 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 4c5361de45e44..dd149ab479f67 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -33,23 +33,23 @@ func (e *APIKeyScope) Scan(src interface{}) error { return nil } -type AppShareLevel string +type AppSharingLevel string const ( - AppShareLevelOwner AppShareLevel = "owner" - AppShareLevelTemplate AppShareLevel = "template" - AppShareLevelAuthenticated AppShareLevel = "authenticated" - AppShareLevelPublic AppShareLevel = "public" + AppSharingLevelOwner AppSharingLevel = "owner" + AppSharingLevelTemplate AppSharingLevel = "template" + AppSharingLevelAuthenticated AppSharingLevel = "authenticated" + AppSharingLevelPublic AppSharingLevel = "public" ) -func (e *AppShareLevel) Scan(src interface{}) error { +func (e *AppSharingLevel) Scan(src interface{}) error { switch s := src.(type) { case []byte: - *e = AppShareLevel(s) + *e = AppSharingLevel(s) case string: - *e = AppShareLevel(s) + *e = AppSharingLevel(s) default: - return fmt.Errorf("unsupported scan type for AppShareLevel: %T", src) + return fmt.Errorf("unsupported scan type for AppSharingLevel: %T", src) } return nil } @@ -631,7 +631,7 @@ type WorkspaceApp struct { HealthcheckThreshold int32 `db:"healthcheck_threshold" json:"healthcheck_threshold"` Health WorkspaceAppHealth `db:"health" json:"health"` Subdomain bool `db:"subdomain" json:"subdomain"` - ShareLevel AppShareLevel `db:"share_level" json:"share_level"` + 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 495bc201f2a7b..ad1149ba64bad 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3895,7 +3895,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, share_level 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 { @@ -3919,13 +3919,13 @@ func (q *sqlQuerier) GetWorkspaceAppByAgentIDAndName(ctx context.Context, arg Ge &i.HealthcheckThreshold, &i.Health, &i.Subdomain, - &i.ShareLevel, + &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, share_level 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) { @@ -3950,7 +3950,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid &i.HealthcheckThreshold, &i.Health, &i.Subdomain, - &i.ShareLevel, + &i.SharingLevel, ); err != nil { return nil, err } @@ -3966,7 +3966,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, share_level 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) { @@ -3991,7 +3991,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid. &i.HealthcheckThreshold, &i.Health, &i.Subdomain, - &i.ShareLevel, + &i.SharingLevel, ); err != nil { return nil, err } @@ -4007,7 +4007,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, share_level 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) { @@ -4032,7 +4032,7 @@ func (q *sqlQuerier) GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt &i.HealthcheckThreshold, &i.Health, &i.Subdomain, - &i.ShareLevel, + &i.SharingLevel, ); err != nil { return nil, err } @@ -4058,13 +4058,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, share_level + ($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 { @@ -4076,6 +4077,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"` @@ -4092,6 +4094,7 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace arg.Command, arg.Url, arg.Subdomain, + arg.SharingLevel, arg.HealthcheckUrl, arg.HealthcheckInterval, arg.HealthcheckThreshold, @@ -4111,7 +4114,7 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace &i.HealthcheckThreshold, &i.Health, &i.Subdomain, - &i.ShareLevel, + &i.SharingLevel, ) return i, err } diff --git a/coderd/database/queries/workspaceapps.sql b/coderd/database/queries/workspaceapps.sql index 3336bfda4ad74..8099f350345fb 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 11433624eb644..d453b931a23d3 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -82,8 +82,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 { @@ -118,21 +118,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 } @@ -156,7 +142,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 @@ -165,7 +151,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 @@ -175,7 +161,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 @@ -191,7 +177,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 @@ -254,7 +240,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 @@ -420,3 +406,24 @@ 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) + return +} 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..5ac87c2dcfefb 100644 --- a/coderd/httpmw/userparam.go +++ b/coderd/httpmw/userparam.go @@ -33,8 +33,9 @@ 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. +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 +54,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 bef1110ec3bb0..cd6ca6f229aaa 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -814,6 +814,16 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. health = database.WorkspaceAppHealthInitializing } + sharingLevel := database.AppSharingLevelOwner + switch app.SharingLevel { + case sdkproto.AppSharingLevel_TEMPLATE: + sharingLevel = database.AppSharingLevelTemplate + 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 +839,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/workspaceagents.go b/coderd/workspaceagents.go index 7581bb2d216f9..e23cd23bd2fb4 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -494,11 +494,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 e2108b9744789..b5013a660540f 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -42,10 +42,13 @@ const ( type AppAuthorizer interface { // Authorize returns true if the request is authorized to access an app at - // share level `appShareLevel` in `workspace`. An error is only returned if + // share level `AppSharingLevel` in `workspace`. An error is only returned if // there is a processing error. "Unauthorized" errors should not be // returned. - Authorize(r *http.Request, db database.Store, appShareLevel database.AppShareLevel, workspace database.Workspace) (bool, error) + // + // It must be able to handle optional user authorization. Use + // `httpmw.*Optional` methods. + Authorize(r *http.Request, db database.Store, AppSharingLevel database.AppSharingLevel, workspace database.Workspace) (bool, error) } type AGPLAppAuthorizer struct { @@ -56,7 +59,7 @@ var _ AppAuthorizer = &AGPLAppAuthorizer{} // Authorize provides an AGPL implementation of AppAuthorizer. It does not // support app sharing levels as they are an enterprise feature. -func (a AGPLAppAuthorizer) Authorize(r *http.Request, _ database.Store, _ database.AppShareLevel, workspace database.Workspace) (bool, error) { +func (a AGPLAppAuthorizer) Authorize(r *http.Request, _ database.Store, _ database.AppSharingLevel, workspace database.Workspace) (bool, error) { roles, ok := httpmw.UserAuthorizationOptional(r) if !ok { return false, nil @@ -85,11 +88,25 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) return } - shareLevel := database.AppShareLevelOwner - if app.ShareLevel != "" { - shareLevel = app.ShareLevel + AppSharingLevel := database.AppSharingLevelOwner + if app.SharingLevel != "" { + AppSharingLevel = app.SharingLevel + } + authed, ok := api.fetchWorkspaceApplicationAuth(rw, r, workspace, AppSharingLevel) + if !ok { + return } - if !api.checkWorkspaceApplicationAuth(rw, r, workspace, shareLevel) { + 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 } @@ -204,11 +221,11 @@ func (api *API) handleSubdomainApplications(middlewares ...func(http.Handler) ht // Verify application auth. This function will redirect or // return an error page if the user doesn't have permission. - shareLevel := database.AppShareLevelOwner - if workspaceAppPtr != nil && workspaceAppPtr.ShareLevel != "" { - shareLevel = workspaceAppPtr.ShareLevel + SharingLevel := database.AppSharingLevelOwner + if workspaceAppPtr != nil && workspaceAppPtr.SharingLevel != "" { + SharingLevel = workspaceAppPtr.SharingLevel } - if !api.verifyWorkspaceApplicationAuth(rw, r, host, workspace, shareLevel) { + if !api.verifyWorkspaceApplicationSubdomainAuth(rw, r, host, workspace, SharingLevel) { return } @@ -308,12 +325,12 @@ func (api *API) lookupWorkspaceApp(rw http.ResponseWriter, r *http.Request, agen return app, 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 +// 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) checkWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Request, workspace database.Workspace, appShareLevel database.AppShareLevel) bool { - ok, err := (*api.AppAuthorizer.Load()).Authorize(r, api.Database, appShareLevel, workspace) +func (api *API) fetchWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Request, workspace database.Workspace, AppSharingLevel database.AppSharingLevel) (authed bool, ok bool) { + ok, err := (*api.AppAuthorizer.Load()).Authorize(r, api.Database, AppSharingLevel, workspace) if err != nil { api.Logger.Error(r.Context(), "authorize workspace app", slog.Error(err)) site.RenderStaticErrorPage(rw, r, site.ErrorPageData{ @@ -323,9 +340,22 @@ func (api *API) checkWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Re RetryEnabled: true, DashboardURL: api.AccessURL.String(), }) - return false + 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 } @@ -333,15 +363,24 @@ func (api *API) checkWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Re return true } -// verifyWorkspaceApplicationAuth checks that the request is authorized to -// access the given application. If the user does not have a app session key, +// 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) verifyWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Request, host string, workspace database.Workspace, appShareLevel database.AppShareLevel) bool { - _, ok := httpmw.APIKeyOptional(r) - if ok { - // Request should be all good to go as long as it passes auth checks! - return api.checkWorkspaceApplicationAuth(rw, r, workspace, appShareLevel) +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 @@ -522,9 +561,9 @@ type proxyApplication struct { App *database.WorkspaceApp Port uint16 - // ShareLevel MUST be set to database.AppShareLevelOwner by default for + // SharingLevel MUST be set to database.AppSharingLevelOwner by default for // ports. - ShareLevel database.AppShareLevel + SharingLevel database.AppSharingLevel // Path must either be empty or have a leading slash. Path string } @@ -532,11 +571,11 @@ type proxyApplication struct { func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - shareLevel := database.AppShareLevelOwner - if proxyApp.App != nil && proxyApp.App.ShareLevel != "" { - shareLevel = proxyApp.App.ShareLevel + SharingLevel := database.AppSharingLevelOwner + if proxyApp.App != nil && proxyApp.App.SharingLevel != "" { + SharingLevel = proxyApp.App.SharingLevel } - if !api.checkWorkspaceApplicationAuth(rw, r, proxyApp.Workspace, shareLevel) { + if !api.checkWorkspaceApplicationAuth(rw, r, proxyApp.Workspace, SharingLevel) { return } @@ -759,8 +798,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/codersdk/workspaceapps.go b/codersdk/workspaceapps.go index 7de4217bac999..bf6dec874ba25 100644 --- a/codersdk/workspaceapps.go +++ b/codersdk/workspaceapps.go @@ -13,6 +13,15 @@ const ( WorkspaceAppHealthUnhealthy WorkspaceAppHealth = "unhealthy" ) +type WorkspaceAppSharingLevel string + +const ( + WorkspaceAppSharingLevelOwner WorkspaceAppSharingLevel = "owner" + WorkspaceAppSharingLevelTemplate WorkspaceAppSharingLevel = "template" + 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 +34,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/enterprise/cli/features_test.go b/enterprise/cli/features_test.go index da2425634cab9..9f606a43f2ae1 100644 --- a/enterprise/cli/features_test.go +++ b/enterprise/cli/features_test.go @@ -57,7 +57,7 @@ func TestFeaturesList(t *testing.T) { var entitlements codersdk.Entitlements err := json.Unmarshal(buf.Bytes(), &entitlements) require.NoError(t, err, "unmarshal JSON output") - assert.Len(t, entitlements.Features, 4) + assert.Len(t, entitlements.Features, 5) assert.Empty(t, entitlements.Warnings) assert.Equal(t, codersdk.EntitlementNotEntitled, entitlements.Features[codersdk.FeatureUserLimit].Entitlement) @@ -67,6 +67,8 @@ func TestFeaturesList(t *testing.T) { entitlements.Features[codersdk.FeatureBrowserOnly].Entitlement) assert.Equal(t, codersdk.EntitlementNotEntitled, entitlements.Features[codersdk.FeatureWorkspaceQuota].Entitlement) + assert.Equal(t, codersdk.EntitlementNotEntitled, + entitlements.Features[codersdk.FeatureApplicationSharing].Entitlement) assert.False(t, entitlements.HasLicense) assert.False(t, entitlements.Experimental) }) diff --git a/enterprise/coderd/appsharing.go b/enterprise/coderd/appsharing.go index 4ee9fad54844e..f1c2f3891d5c7 100644 --- a/enterprise/coderd/appsharing.go +++ b/enterprise/coderd/appsharing.go @@ -24,53 +24,53 @@ type EnterpriseAppAuthorizer struct { var _ agplcoderd.AppAuthorizer = &EnterpriseAppAuthorizer{} // Authorize implements agplcoderd.AppAuthorizer. -func (a *EnterpriseAppAuthorizer) Authorize(r *http.Request, db database.Store, shareLevel database.AppShareLevel, workspace database.Workspace) (bool, error) { +func (a *EnterpriseAppAuthorizer) Authorize(r *http.Request, db database.Store, SharingLevel database.AppSharingLevel, workspace database.Workspace) (bool, error) { ctx := r.Context() - // TODO: better errors displayed to the user in this case - switch shareLevel { - case database.AppShareLevelOwner: + // 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 and the public level is allowed. + return SharingLevel == database.AppSharingLevelPublic && a.LevelPublicAllowed, 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. + err := a.RBAC.ByRoleName(ctx, roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), rbac.ActionCreate, workspace.ApplicationConnectRBAC()) + if err == nil { + return true, nil + } + + // Ensure the app's share level is allowed. + switch SharingLevel { + case database.AppSharingLevelOwner: if !a.LevelOwnerAllowed { return false, nil } - case database.AppShareLevelTemplate: + case database.AppSharingLevelTemplate: if !a.LevelTemplateAllowed { return false, nil } - case database.AppShareLevelAuthenticated: + case database.AppSharingLevelAuthenticated: if !a.LevelAuthenticatedAllowed { return false, nil } - case database.AppShareLevelPublic: + case database.AppSharingLevelPublic: if !a.LevelPublicAllowed { return false, nil } default: - return false, xerrors.Errorf("unknown workspace app sharing level %q", shareLevel) - } - - // 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 shareLevel == database.AppShareLevelPublic, 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, the owner of the workspace can always access - // applications. - err := a.RBAC.ByRoleName(ctx, roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), rbac.ActionCreate, workspace.ApplicationConnectRBAC()) - if err == nil { - return true, nil + return false, xerrors.Errorf("unknown workspace app sharing level %q", SharingLevel) } - switch shareLevel { - case database.AppShareLevelOwner: + switch SharingLevel { + case database.AppSharingLevelOwner: // We essentially already did this above. - case database.AppShareLevelTemplate: + case database.AppSharingLevelTemplate: // Check if the user has access to the same template as the workspace. template, err := db.GetTemplateByID(ctx, workspace.TemplateID) if err != nil { @@ -81,7 +81,7 @@ func (a *EnterpriseAppAuthorizer) Authorize(r *http.Request, db database.Store, if err == nil { return true, nil } - case database.AppShareLevelAuthenticated: + 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 @@ -91,7 +91,7 @@ func (a *EnterpriseAppAuthorizer) Authorize(r *http.Request, db database.Store, if err == nil { return true, nil } - case database.AppShareLevelPublic: + 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. diff --git a/enterprise/coderd/appsharing_test.go b/enterprise/coderd/appsharing_test.go index bd03dcf294bdb..6f70dae73b48c 100644 --- a/enterprise/coderd/appsharing_test.go +++ b/enterprise/coderd/appsharing_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/coder/coder/coderd/coderdtest" @@ -18,7 +19,7 @@ import ( "github.com/coder/coder/testutil" ) -func setupAppAuthorizerTest(t *testing.T, allowedSharingLevels []database.AppShareLevel) (workspace codersdk.Workspace, agent codersdk.WorkspaceAgent, user codersdk.User, client *codersdk.Client, clientWithTemplateAccess *codersdk.Client, clientWithNoTemplateAccess *codersdk.Client, clientWithNoAuth *codersdk.Client) { +func setupAppAuthorizerTest(t *testing.T, allowedSharingLevels []database.AppSharingLevel) (workspace codersdk.Workspace, agent codersdk.WorkspaceAgent, user codersdk.User, client *codersdk.Client, clientWithTemplateAccess *codersdk.Client, clientWithNoTemplateAccess *codersdk.Client, clientWithNoAuth *codersdk.Client) { //nolint:gosec const password = "password" @@ -51,6 +52,10 @@ func setupAppAuthorizerTest(t *testing.T, allowedSharingLevels []database.AppSha IncludeProvisionerDaemon: true, }, }) + client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + firstUser := coderdtest.CreateFirstUser(t, client) user, err = client.User(ctx, firstUser.UserID.String()) require.NoError(t, err) @@ -59,6 +64,21 @@ func setupAppAuthorizerTest(t *testing.T, allowedSharingLevels []database.AppSha }) workspace, agent = setupWorkspaceAgent(t, client, firstUser, uint16(tcpAddr.Port)) + // Verify that the apps have the correct sharing levels set. + workspaceBuild, err := client.WorkspaceBuild(ctx, workspace.LatestBuild.ID) + require.NoError(t, err) + found := map[string]codersdk.WorkspaceAppSharingLevel{} + expected := map[string]codersdk.WorkspaceAppSharingLevel{ + testAppNameOwner: codersdk.WorkspaceAppSharingLevelOwner, + testAppNameTemplate: codersdk.WorkspaceAppSharingLevelTemplate, + testAppNameAuthenticated: codersdk.WorkspaceAppSharingLevelAuthenticated, + testAppNamePublic: codersdk.WorkspaceAppSharingLevelPublic, + } + for _, app := range workspaceBuild.Resources[0].Agents[0].Apps { + found[app.Name] = app.SharingLevel + } + require.Equal(t, expected, found, "apps have incorrect sharing levels") + // Create a user in the same org (should be able to read the template). userWithTemplateAccess, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ Email: "template-access@coder.com", @@ -75,6 +95,13 @@ func setupAppAuthorizerTest(t *testing.T, allowedSharingLevels []database.AppSha }) require.NoError(t, err) clientWithTemplateAccess.SessionToken = loginRes.SessionToken + clientWithTemplateAccess.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + + // Double check that the user can read the template. + _, err = clientWithTemplateAccess.Template(ctx, workspace.TemplateID) + require.NoError(t, err) // Create a user in a different org (should not be able to read the // template). @@ -97,9 +124,19 @@ func setupAppAuthorizerTest(t *testing.T, allowedSharingLevels []database.AppSha }) require.NoError(t, err) clientWithNoTemplateAccess.SessionToken = loginRes.SessionToken + clientWithNoTemplateAccess.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + + // Double check that the user cannot read the template. + _, err = clientWithNoTemplateAccess.Template(ctx, workspace.TemplateID) + require.Error(t, err) // 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, agent, user, client, clientWithTemplateAccess, clientWithNoTemplateAccess, clientWithNoAuth } @@ -107,21 +144,13 @@ func setupAppAuthorizerTest(t *testing.T, allowedSharingLevels []database.AppSha func TestEnterpriseAppAuthorizer(t *testing.T) { t.Parallel() - // For the purposes of these tests we allow all levels. - workspace, agent, user, client, clientWithTemplateAccess, clientWithNoTemplateAccess, clientWithNoAuth := setupAppAuthorizerTest(t, []database.AppShareLevel{ - database.AppShareLevelOwner, - database.AppShareLevelTemplate, - database.AppShareLevelAuthenticated, - database.AppShareLevelPublic, - }) - - verifyAccess := func(t *testing.T, appName string, client *codersdk.Client, shouldHaveAccess bool) { + 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() - appPath := fmt.Sprintf("/@%s/%s.%s/apps/%s", user.Username, workspace.Name, agent.Name, appName) + appPath := fmt.Sprintf("/@%s/%s.%s/apps/%s/", username, workspaceName, agentName, appName) res, err := client.Request(ctx, http.MethodGet, appPath, nil) require.NoError(t, err) defer res.Body.Close() @@ -131,74 +160,202 @@ func TestEnterpriseAppAuthorizer(t *testing.T) { t.Logf("response dump: %s", dump) if !shouldHaveAccess { - require.Equal(t, http.StatusForbidden, res.StatusCode) + if shouldRedirectToLogin { + assert.Equal(t, http.StatusTemporaryRedirect, res.StatusCode, "should not have access, expected temporary redirect") + location, err := res.Location() + require.NoError(t, err) + assert.Equal(t, "/login", location.Path, "should not have access, expected redirect to /login") + } 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") + } } if shouldHaveAccess { - require.Equal(t, http.StatusOK, res.StatusCode) - require.Contains(t, string(dump), "Hello World") + assert.Equal(t, http.StatusOK, res.StatusCode, "should have access, expected ok") + assert.Contains(t, string(dump), "Hello World", "should have access, expected hello world") } } - t.Run("LevelOwner", func(t *testing.T) { + t.Run("Disabled", func(t *testing.T) { t.Parallel() + workspace, agent, user, client, clientWithTemplateAccess, clientWithNoTemplateAccess, clientWithNoAuth := setupAppAuthorizerTest(t, []database.AppSharingLevel{ + // Disabled basically means only the owner level is allowed. This + // should have feature parity with the AGPL version. + database.AppSharingLevelOwner, + }) // Owner should be able to access their own workspace. - verifyAccess(t, testAppNameOwner, client, true) + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, client, true, false) // User with or without template access should not have access to a // workspace that they do not own. - verifyAccess(t, testAppNameOwner, clientWithTemplateAccess, false) - verifyAccess(t, testAppNameOwner, clientWithNoTemplateAccess, false) + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, clientWithTemplateAccess, false, false) + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, clientWithNoTemplateAccess, false, false) // Unauthenticated user should not have any access. - verifyAccess(t, testAppNameOwner, clientWithNoAuth, false) + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, clientWithNoAuth, false, true) }) - t.Run("LevelTemplate", func(t *testing.T) { + t.Run("Level", func(t *testing.T) { t.Parallel() - // Owner should be able to access their own workspace. - verifyAccess(t, testAppNameTemplate, client, true) + // For the purposes of the level tests we allow all levels. + workspace, agent, user, client, clientWithTemplateAccess, clientWithNoTemplateAccess, clientWithNoAuth := setupAppAuthorizerTest(t, []database.AppSharingLevel{ + database.AppSharingLevelOwner, + database.AppSharingLevelTemplate, + database.AppSharingLevelAuthenticated, + database.AppSharingLevelPublic, + }) - // User with template access should be able to access the workspace. - verifyAccess(t, testAppNameTemplate, clientWithTemplateAccess, true) + t.Run("Owner", func(t *testing.T) { + t.Parallel() - // User without template access should not have access to a workspace - // that they do not own. - verifyAccess(t, testAppNameTemplate, clientWithNoTemplateAccess, false) + // Owner should be able to access their own workspace. + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, client, true, false) - // Unauthenticated user should not have any access. - verifyAccess(t, testAppNameTemplate, clientWithNoAuth, false) - }) + // User with or without template access should not have access to a + // workspace that they do not own. + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, clientWithTemplateAccess, false, false) + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, clientWithNoTemplateAccess, false, false) - t.Run("LevelAuthenticated", func(t *testing.T) { - t.Parallel() + // Unauthenticated user should not have any access. + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, clientWithNoAuth, false, true) + }) - // Owner should be able to access their own workspace. - verifyAccess(t, testAppNameAuthenticated, client, true) + t.Run("Template", func(t *testing.T) { + t.Parallel() - // User with or without template access should be able to access the - // workspace. - verifyAccess(t, testAppNameAuthenticated, clientWithTemplateAccess, true) - verifyAccess(t, testAppNameAuthenticated, clientWithNoTemplateAccess, true) + // Owner should be able to access their own workspace. + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameTemplate, client, true, false) - // Unauthenticated user should not have any access. - verifyAccess(t, testAppNameAuthenticated, clientWithNoAuth, false) - }) + // User with template access should be able to access the workspace. + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameTemplate, clientWithTemplateAccess, true, false) - t.Run("LevelPublic", func(t *testing.T) { - t.Parallel() + // User without template access should not have access to a workspace + // that they do not own. + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameTemplate, clientWithNoTemplateAccess, false, false) - // Owner should be able to access their own workspace. - verifyAccess(t, testAppNamePublic, client, true) + // Unauthenticated user should not have any access. + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameTemplate, 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, testAppNameAuthenticated, client, true, false) - // User with or without template access should be able to access the - // workspace. - verifyAccess(t, testAppNamePublic, clientWithTemplateAccess, true) - verifyAccess(t, testAppNamePublic, clientWithNoTemplateAccess, true) + // User with or without template access should be able to access the + // workspace. + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameAuthenticated, clientWithTemplateAccess, true, false) + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameAuthenticated, clientWithNoTemplateAccess, true, false) + + // Unauthenticated user should not have any access. + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameAuthenticated, 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, testAppNamePublic, client, true, false) + + // User with or without template access should be able to access the + // workspace. + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNamePublic, clientWithTemplateAccess, true, false) + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNamePublic, clientWithNoTemplateAccess, true, false) + + // Unauthenticated user should be able to access the workspace. + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNamePublic, clientWithNoAuth, true, false) + }) + }) + + t.Run("LevelBlockedByAdmin", func(t *testing.T) { + t.Parallel() - // Unauthenticated user should be able to access the workspace. - verifyAccess(t, testAppNamePublic, clientWithNoAuth, true) + t.Run("Owner", func(t *testing.T) { + t.Parallel() + + // All levels allowed except owner. + workspace, agent, user, client, clientWithTemplateAccess, clientWithNoTemplateAccess, clientWithNoAuth := setupAppAuthorizerTest(t, []database.AppSharingLevel{ + database.AppSharingLevelTemplate, + database.AppSharingLevelAuthenticated, + database.AppSharingLevelPublic, + }) + + // Owner can always access their own workspace. + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, client, true, false) + + // All other users should always be blocked anyways. + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, clientWithTemplateAccess, false, false) + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, clientWithNoTemplateAccess, false, false) + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, clientWithNoAuth, false, true) + }) + + t.Run("Template", func(t *testing.T) { + t.Parallel() + + // All levels allowed except template. + workspace, agent, user, client, clientWithTemplateAccess, clientWithNoTemplateAccess, clientWithNoAuth := setupAppAuthorizerTest(t, []database.AppSharingLevel{ + database.AppSharingLevelOwner, + database.AppSharingLevelAuthenticated, + database.AppSharingLevelPublic, + }) + + // Owner can always access their own workspace. + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameTemplate, client, true, false) + + // User with template access should not be able to access the + // workspace as the template level is disallowed. + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameTemplate, clientWithTemplateAccess, false, false) + + // All other users should always be blocked anyways. + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameTemplate, clientWithNoTemplateAccess, false, false) + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameTemplate, clientWithNoAuth, false, true) + }) + + t.Run("Authenticated", func(t *testing.T) { + t.Parallel() + + // All levels allowed except authenticated. + workspace, agent, user, client, clientWithTemplateAccess, clientWithNoTemplateAccess, clientWithNoAuth := setupAppAuthorizerTest(t, []database.AppSharingLevel{ + database.AppSharingLevelOwner, + database.AppSharingLevelTemplate, + database.AppSharingLevelPublic, + }) + + // Owner can always access their own workspace. + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameAuthenticated, client, true, false) + + // User with or without template access should not be able to access + // the workspace as the authenticated level is disallowed. + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameAuthenticated, clientWithTemplateAccess, false, false) + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameAuthenticated, clientWithNoTemplateAccess, false, false) + + // Unauthenticated users should be blocked anyways. + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameAuthenticated, clientWithNoAuth, false, true) + }) + + t.Run("Public", func(t *testing.T) { + t.Parallel() + + // All levels allowed except public. + workspace, agent, user, client, clientWithTemplateAccess, clientWithNoTemplateAccess, clientWithNoAuth := setupAppAuthorizerTest(t, []database.AppSharingLevel{ + database.AppSharingLevelOwner, + database.AppSharingLevelTemplate, + database.AppSharingLevelAuthenticated, + }) + + // Owner can always access their own workspace. + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNamePublic, client, true, false) + + // All other users should be blocked because the public level is + // disallowed. + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNamePublic, clientWithTemplateAccess, false, false) + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNamePublic, clientWithNoTemplateAccess, false, false) + verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNamePublic, clientWithNoAuth, false, true) + }) }) } diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index f9279451cc675..c29ab3bc509b9 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -50,13 +50,13 @@ func New(ctx context.Context, options *Options) (*API, error) { ) for _, v := range options.AllowedApplicationSharingLevels { switch v { - case database.AppShareLevelOwner: + case database.AppSharingLevelOwner: levelOwnerAllowed = true - case database.AppShareLevelTemplate: + case database.AppSharingLevelTemplate: levelTemplateAllowed = true - case database.AppShareLevelAuthenticated: + case database.AppSharingLevelAuthenticated: levelAuthenticatedAllowed = true - case database.AppShareLevelPublic: + case database.AppSharingLevelPublic: levelPublicAllowed = true default: return nil, xerrors.Errorf("unknown workspace app sharing level %q", v) @@ -109,7 +109,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) }) }) @@ -145,9 +145,9 @@ type Options struct { BrowserOnly bool SCIMAPIKey []byte UserWorkspaceQuota int - // Defaults to []database.AppShareLevel{database.AppShareLevelOwner} which + // Defaults to []database.AppSharingLevel{database.AppSharingLevelOwner} which // essentially means "function identically to AGPL Coder". - AllowedApplicationSharingLevels []database.AppShareLevel + AllowedApplicationSharingLevels []database.AppSharingLevel EntitlementsUpdateInterval time.Duration Keys map[string]ed25519.PublicKey @@ -358,7 +358,7 @@ func (api *API) serveEntitlements(rw http.ResponseWriter, r *http.Request) { // App sharing is disabled if no levels are allowed or the only allowed // level is "owner". appSharingEnabled := true - if len(api.AllowedApplicationSharingLevels) == 0 || (len(api.AllowedApplicationSharingLevels) == 1 && api.AllowedApplicationSharingLevels[0] == database.AppShareLevelOwner) { + if len(api.AllowedApplicationSharingLevels) == 0 || (len(api.AllowedApplicationSharingLevels) == 1 && api.AllowedApplicationSharingLevels[0] == database.AppSharingLevelOwner) { appSharingEnabled = false } resp.Features[codersdk.FeatureApplicationSharing] = codersdk.Feature{ diff --git a/enterprise/coderd/coderdenttest/coderdenttest.go b/enterprise/coderd/coderdenttest/coderdenttest.go index bc063294cf6f3..655eccff843ac 100644 --- a/enterprise/coderd/coderdenttest/coderdenttest.go +++ b/enterprise/coderd/coderdenttest/coderdenttest.go @@ -42,7 +42,7 @@ type Options struct { EntitlementsUpdateInterval time.Duration SCIMAPIKey []byte UserWorkspaceQuota int - AllowedApplicationSharingLevels []database.AppShareLevel + AllowedApplicationSharingLevels []database.AppSharingLevel } // New constructs a codersdk client connected to an in-memory Enterprise API instance. diff --git a/enterprise/coderd/licenses_test.go b/enterprise/coderd/licenses_test.go index c4b7111597079..33b4773b1ddd2 100644 --- a/enterprise/coderd/licenses_test.go +++ b/enterprise/coderd/licenses_test.go @@ -98,20 +98,22 @@ func TestGetLicense(t *testing.T) { assert.Equal(t, int32(1), licenses[0].ID) assert.Equal(t, "testing", licenses[0].Claims["account_id"]) assert.Equal(t, map[string]interface{}{ - codersdk.FeatureUserLimit: json.Number("0"), - codersdk.FeatureAuditLog: json.Number("1"), - codersdk.FeatureSCIM: json.Number("1"), - codersdk.FeatureBrowserOnly: json.Number("1"), - codersdk.FeatureWorkspaceQuota: json.Number("0"), + codersdk.FeatureUserLimit: json.Number("0"), + codersdk.FeatureAuditLog: json.Number("1"), + codersdk.FeatureSCIM: json.Number("1"), + codersdk.FeatureBrowserOnly: json.Number("1"), + codersdk.FeatureWorkspaceQuota: json.Number("0"), + codersdk.FeatureApplicationSharing: json.Number("0"), }, licenses[0].Claims["features"]) assert.Equal(t, int32(2), licenses[1].ID) assert.Equal(t, "testing2", licenses[1].Claims["account_id"]) assert.Equal(t, map[string]interface{}{ - codersdk.FeatureUserLimit: json.Number("200"), - codersdk.FeatureAuditLog: json.Number("1"), - codersdk.FeatureSCIM: json.Number("1"), - codersdk.FeatureBrowserOnly: json.Number("1"), - codersdk.FeatureWorkspaceQuota: json.Number("0"), + codersdk.FeatureUserLimit: json.Number("200"), + codersdk.FeatureAuditLog: json.Number("1"), + codersdk.FeatureSCIM: json.Number("1"), + codersdk.FeatureBrowserOnly: json.Number("1"), + codersdk.FeatureWorkspaceQuota: json.Number("0"), + codersdk.FeatureApplicationSharing: json.Number("0"), }, licenses[1].Claims["features"]) }) } diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go index 90a4ecfe6c139..4a9f22b5a1766 100644 --- a/enterprise/coderd/workspaceagents_test.go +++ b/enterprise/coderd/workspaceagents_test.go @@ -85,20 +85,24 @@ func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.Cr // TODO: sharing levels Apps: []*proto.App{ { - Name: testAppNameOwner, - Url: fmt.Sprintf("http://localhost:%d", appPort), + Name: testAppNameOwner, + SharingLevel: proto.AppSharingLevel_OWNER, + Url: fmt.Sprintf("http://localhost:%d", appPort), }, { - Name: testAppNameTemplate, - Url: fmt.Sprintf("http://localhost:%d", appPort), + Name: testAppNameTemplate, + SharingLevel: proto.AppSharingLevel_TEMPLATE, + Url: fmt.Sprintf("http://localhost:%d", appPort), }, { - Name: testAppNameAuthenticated, - Url: fmt.Sprintf("http://localhost:%d", appPort), + Name: testAppNameAuthenticated, + SharingLevel: proto.AppSharingLevel_AUTHENTICATED, + Url: fmt.Sprintf("http://localhost:%d", appPort), }, { - Name: testAppNamePublic, - Url: fmt.Sprintf("http://localhost:%d", appPort), + Name: testAppNamePublic, + SharingLevel: proto.AppSharingLevel_PUBLIC, + Url: fmt.Sprintf("http://localhost:%d", appPort), }, }, }}, @@ -118,9 +122,9 @@ 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() diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index 3118d08c772de..c4b122877994a 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -25,13 +25,13 @@ type agentAttributes struct { // A mapping of attributes on the "coder_app" resource. type agentAppAttributes struct { - AgentID string `mapstructure:"agent_id"` - Name string `mapstructure:"name"` - Icon string `mapstructure:"icon"` - URL string `mapstructure:"url"` - Command string `mapstructure:"command"` - ShareLevel string `mapstructure:"share_level"` - Subdomain bool `mapstructure:"subdomain"` + AgentID string `mapstructure:"agent_id"` + Name string `mapstructure:"name"` + Icon string `mapstructure:"icon"` + URL string `mapstructure:"url"` + Command string `mapstructure:"command"` + SharingLevel string `mapstructure:"share_level"` + Subdomain bool `mapstructure:"subdomain"` // RelativePath is deprecated in favor of Subdomain. This value is a pointer // because we prefer it over Subdomain it was explicitly set. RelativePath *bool `mapstructure:"relative_path"` diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index b88fc0ac1db44..5f079d9effe14 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v3.21.5 +// protoc v3.6.1 // source: provisionersdk/proto/provisioner.proto package proto @@ -76,6 +76,58 @@ func (LogLevel) EnumDescriptor() ([]byte, []int) { return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{0} } +type AppSharingLevel int32 + +const ( + AppSharingLevel_OWNER AppSharingLevel = 0 + AppSharingLevel_TEMPLATE AppSharingLevel = 1 + AppSharingLevel_AUTHENTICATED AppSharingLevel = 2 + AppSharingLevel_PUBLIC AppSharingLevel = 3 +) + +// Enum value maps for AppSharingLevel. +var ( + AppSharingLevel_name = map[int32]string{ + 0: "OWNER", + 1: "TEMPLATE", + 2: "AUTHENTICATED", + 3: "PUBLIC", + } + AppSharingLevel_value = map[string]int32{ + "OWNER": 0, + "TEMPLATE": 1, + "AUTHENTICATED": 2, + "PUBLIC": 3, + } +) + +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 +161,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 +174,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 +204,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 +250,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 +296,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 +902,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 +985,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 +2012,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,128 +2024,137 @@ 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, + 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, 0x49, 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, 0x0c, 0x0a, 0x08, 0x54, 0x45, 0x4d, 0x50, 0x4c, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, + 0x11, 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, + 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x03, 0x2a, 0x37, + 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, + 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, + 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x02, 0x32, 0xa3, 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, + 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, + 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, + 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x50, 0x0a, 0x09, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 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, 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, - 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, - 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, - 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, - 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x02, 0x32, 0xa3, 0x01, 0x0a, 0x0b, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x05, 0x50, 0x61, 0x72, - 0x73, 0x65, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, - 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x50, 0x0a, - 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, - 0x2d, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x2d, 0x5a, + 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2100,72 +2169,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 +2543,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..74cddc5dba618 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -87,6 +87,13 @@ message Agent { } } +enum AppSharingLevel { + OWNER = 0; + TEMPLATE = 1; + AUTHENTICATED = 2; + PUBLIC = 3; +} + // App represents a dev-accessible application on the workspace. message App { string name = 1; @@ -95,6 +102,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 5b9117c1672e6..5e7e7b256a7e2 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -604,6 +604,7 @@ export interface WorkspaceApp { readonly command?: string readonly icon?: string readonly subdomain: boolean + readonly sharing_level: WorkspaceAppSharingLevel readonly healthcheck: Healthcheck readonly health: WorkspaceAppHealth } @@ -738,6 +739,9 @@ export type WorkspaceAgentStatus = "connected" | "connecting" | "disconnected" // From codersdk/workspaceapps.go export type WorkspaceAppHealth = "disabled" | "healthy" | "initializing" | "unhealthy" +// From codersdk/workspaceapps.go +export type WorkspaceAppSharingLevel = "authenticated" | "owner" | "public" | "template" + // From codersdk/workspacebuilds.go export type WorkspaceStatus = | "canceled" From a2eacaac1f0f7b7acc044f6ab2085a0f744a08c5 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 6 Oct 2022 19:10:33 +0000 Subject: [PATCH 04/12] feat: app sharing pt.4 --- enterprise/cli/server.go | 30 ++++++++++++++-------- provisioner/terraform/resources.go | 41 ++++++++++++++++++++---------- 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/enterprise/cli/server.go b/enterprise/cli/server.go index 6dde1c31cd75c..67a0d354b2bab 100644 --- a/enterprise/cli/server.go +++ b/enterprise/cli/server.go @@ -7,6 +7,7 @@ import ( "github.com/coder/coder/cli/cliflag" "github.com/coder/coder/cli/cliui" + "github.com/coder/coder/coderd/database" "github.com/coder/coder/enterprise/coderd" agpl "github.com/coder/coder/cli" @@ -15,26 +16,33 @@ import ( func server() *cobra.Command { var ( - auditLogging bool - browserOnly bool - scimAuthHeader string - userWorkspaceQuota int + auditLogging bool + browserOnly bool + scimAuthHeader string + userWorkspaceQuota int + allowedApplicationSharingLevels []string ) cmd := agpl.Server(func(ctx context.Context, options *agplcoderd.Options) (*agplcoderd.API, error) { + appSharingLevels := make([]database.AppSharingLevel, len(allowedApplicationSharingLevels)) + for i, val := range allowedApplicationSharingLevels { + appSharingLevels[i] = database.AppSharingLevel(val) + } + api, err := coderd.New(ctx, &coderd.Options{ - AuditLogging: auditLogging, - BrowserOnly: browserOnly, - SCIMAPIKey: []byte(scimAuthHeader), - UserWorkspaceQuota: userWorkspaceQuota, - Options: options, + AuditLogging: auditLogging, + BrowserOnly: browserOnly, + SCIMAPIKey: []byte(scimAuthHeader), + UserWorkspaceQuota: userWorkspaceQuota, + AllowedApplicationSharingLevels: appSharingLevels, + Options: options, }) if err != nil { return nil, err } return api.AGPL, nil }) - enterpriseOnly := cliui.Styles.Keyword.Render("This is an Enterprise feature. Contact sales@coder.com for licensing") + enterpriseOnly := cliui.Styles.Keyword.Render("This is an Enterprise feature. Contact sales@coder.com for licensing") cliflag.BoolVarP(cmd.Flags(), &auditLogging, "audit-logging", "", "CODER_AUDIT_LOGGING", true, "Specifies whether audit logging is enabled. "+enterpriseOnly) cliflag.BoolVarP(cmd.Flags(), &browserOnly, "browser-only", "", "CODER_BROWSER_ONLY", false, @@ -43,6 +51,8 @@ func server() *cobra.Command { "Enables SCIM and sets the authentication header for the built-in SCIM server. New users are automatically created with OIDC authentication. "+enterpriseOnly) cliflag.IntVarP(cmd.Flags(), &userWorkspaceQuota, "user-workspace-quota", "", "CODER_USER_WORKSPACE_QUOTA", 0, "A positive number applies a limit on how many workspaces each user can create. "+enterpriseOnly) + cliflag.StringArrayVarP(cmd.Flags(), &allowedApplicationSharingLevels, "permitted-app-sharing-levels", "", "CODER_PERMITTED_APP_SHARING_LEVELS", []string{"owner"}, + `Specifies the application sharing levels that are available site-wide. Available values are "owner", "template", "authenticated", "public". Multiple values can be specified, comma separated. `+enterpriseOnly) return cmd } diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index a452bb2989573..5148f86c0e761 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -25,14 +25,14 @@ type agentAttributes struct { // A mapping of attributes on the "coder_app" resource. type agentAppAttributes struct { - AgentID string `mapstructure:"agent_id"` - Name string `mapstructure:"name"` - Icon string `mapstructure:"icon"` - URL string `mapstructure:"url"` - Command string `mapstructure:"command"` - SharingLevel string `mapstructure:"share_level"` - Subdomain bool `mapstructure:"subdomain"` - Healthcheck []appHealthcheckAttributes `mapstructure:"healthcheck"` + AgentID string `mapstructure:"agent_id"` + Name string `mapstructure:"name"` + Icon string `mapstructure:"icon"` + URL string `mapstructure:"url"` + Command string `mapstructure:"command"` + Share string `mapstructure:"share"` + Subdomain bool `mapstructure:"subdomain"` + Healthcheck []appHealthcheckAttributes `mapstructure:"healthcheck"` } // A mapping of attributes on the "healthcheck" resource. @@ -236,6 +236,18 @@ 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 "template": + sharingLevel = proto.AppSharingLevel_TEMPLATE + 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! @@ -243,12 +255,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, }) } } From e4f6fd6ad962c4cc6ad02505d9b1af356497f889 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 12 Oct 2022 16:46:04 +0000 Subject: [PATCH 05/12] chore: move app sharing to open source --- coderd/coderd.go | 9 - coderd/users.go | 4 +- coderd/workspaceapps.go | 96 +++-- coderd/workspaceapps_test.go | 272 ++++++++++++- codersdk/features.go | 12 +- enterprise/cli/features_test.go | 4 +- enterprise/cli/server.go | 28 +- enterprise/coderd/appsharing.go | 103 ----- enterprise/coderd/appsharing_test.go | 361 ------------------ enterprise/coderd/coderd.go | 58 --- .../coderd/coderdenttest/coderdenttest.go | 59 ++- enterprise/coderd/licenses.go | 11 +- enterprise/coderd/licenses_test.go | 22 +- enterprise/coderd/workspaceagents_test.go | 1 - 14 files changed, 369 insertions(+), 671 deletions(-) delete mode 100644 enterprise/coderd/appsharing.go delete mode 100644 enterprise/coderd/appsharing_test.go diff --git a/coderd/coderd.go b/coderd/coderd.go index e68ebf68c6e33..2f64739cbb5f4 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -57,7 +57,6 @@ type Options struct { Auditor audit.Auditor WorkspaceQuotaEnforcer workspacequota.Enforcer - AppAuthorizer AppAuthorizer AgentConnectionUpdateFrequency time.Duration AgentInactiveDisconnectTimeout time.Duration // APIRateLimit is the minutely throughput rate limit per user or ip. @@ -127,11 +126,6 @@ func New(options *Options) *API { if options.WorkspaceQuotaEnforcer == nil { options.WorkspaceQuotaEnforcer = workspacequota.NewNop() } - if options.AppAuthorizer == nil { - options.AppAuthorizer = &AGPLAppAuthorizer{ - RBAC: options.Authorizer, - } - } siteCacheDir := options.CacheDir if siteCacheDir != "" { @@ -160,11 +154,9 @@ func New(options *Options) *API { metricsCache: metricsCache, Auditor: atomic.Pointer[audit.Auditor]{}, WorkspaceQuotaEnforcer: atomic.Pointer[workspacequota.Enforcer]{}, - AppAuthorizer: atomic.Pointer[AppAuthorizer]{}, } api.Auditor.Store(&options.Auditor) api.WorkspaceQuotaEnforcer.Store(&options.WorkspaceQuotaEnforcer) - api.AppAuthorizer.Store(&options.AppAuthorizer) api.workspaceAgentCache = wsconncache.New(api.dialWorkspaceAgentTailnet, 0) api.derpServer = derp.NewServer(key.NewNode(), tailnet.Logger(options.Logger)) oauthConfigs := &httpmw.OAuth2Configs{ @@ -536,7 +528,6 @@ type API struct { Auditor atomic.Pointer[audit.Auditor] WorkspaceClientCoordinateOverride atomic.Pointer[func(rw http.ResponseWriter) bool] WorkspaceQuotaEnforcer atomic.Pointer[workspacequota.Enforcer] - AppAuthorizer atomic.Pointer[AppAuthorizer] HTTPAuth *HTTPAuthorizer // APIHandler serves "/api/v2" diff --git a/coderd/users.go b/coderd/users.go index 17c788f72b8f2..898cefddbaa43 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -958,7 +958,7 @@ func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) { UserID: user.ID, LoginType: database.LoginTypePassword, RemoteAddr: r.RemoteAddr, - // All api generated keys will last 1 week. Browser login tokens have + // All API generated keys will last 1 week. Browser login tokens have // a shorter life. ExpiresAt: database.Now().Add(lifeTime), LifetimeSeconds: int64(lifeTime.Seconds()), @@ -972,7 +972,7 @@ func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) { } // We intentionally do not set the cookie on the response here. - // Setting the cookie will couple the browser sesion to the API + // Setting the cookie will couple the browser session to the API // key we return here, meaning logging out of the website would // invalid your CLI key. httpapi.Write(ctx, rw, http.StatusCreated, codersdk.GenerateAPIKeyResponse{Key: cookie.Value}) diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 9858c65045a28..afb6030875a71 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -34,42 +34,11 @@ 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" ) -type AppAuthorizer interface { - // Authorize returns true if the request is authorized to access an app at - // share level `AppSharingLevel` in `workspace`. An error is only returned if - // there is a processing error. "Unauthorized" errors should not be - // returned. - // - // It must be able to handle optional user authorization. Use - // `httpmw.*Optional` methods. - Authorize(r *http.Request, db database.Store, AppSharingLevel database.AppSharingLevel, workspace database.Workspace) (bool, error) -} - -type AGPLAppAuthorizer struct { - RBAC rbac.Authorizer -} - -var _ AppAuthorizer = &AGPLAppAuthorizer{} - -// Authorize provides an AGPL implementation of AppAuthorizer. It does not -// support app sharing levels as they are an enterprise feature. -func (a AGPLAppAuthorizer) Authorize(r *http.Request, _ database.Store, _ database.AppSharingLevel, workspace database.Workspace) (bool, error) { - roles, ok := httpmw.UserAuthorizationOptional(r) - if !ok { - return false, nil - } - - err := a.RBAC.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), rbac.ActionCreate, workspace.ApplicationConnectRBAC()) - return err == nil, nil -} - func (api *API) appHost(rw http.ResponseWriter, r *http.Request) { httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.GetAppHostResponse{ Host: api.AppHostname, @@ -326,12 +295,69 @@ func (api *API) lookupWorkspaceApp(rw http.ResponseWriter, r *http.Request, agen 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 key's + // scope allows it). + err := api.Authorizer.ByRoleName(ctx, roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), rbac.ActionCreate, workspace.ApplicationConnectRBAC()) + if err == nil { + return true, nil + } + + switch sharingLevel { + case database.AppSharingLevelOwner: + // We essentially already did this above. + case database.AppSharingLevelTemplate: + // Check if the user has access to the same template as the workspace. + template, err := api.Database.GetTemplateByID(ctx, workspace.TemplateID) + if err != nil { + return false, xerrors.Errorf("get template %q: %w", workspace.TemplateID, err) + } + + err = api.Authorizer.ByRoleName(ctx, roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), rbac.ActionRead, template.RBACObject()) + if err == nil { + return true, nil + } + 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(), 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 + } + + // 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.AppAuthorizer.Load()).Authorize(r, api.Database, AppSharingLevel, workspace) +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{ @@ -351,8 +377,8 @@ func (api *API) fetchWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Re // 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) +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 } diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go index b1a090aba3431..8152f52bb3728 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,14 @@ 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" + proxyTestAppNameTemplate = "test-app-template" + proxyTestAppNameAuthenticated = "test-app-authenticated" + proxyTestAppNamePublic = "test-app-public" + proxyTestAppQuery = "query=true" + proxyTestAppBody = "hello world" proxyTestSubdomain = "test.coder.com" ) @@ -101,6 +105,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 +124,31 @@ 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: proxyTestAppNameTemplate, + SharingLevel: proto.AppSharingLevel_TEMPLATE, + Url: appURL, + }, + { + Name: proxyTestAppNameAuthenticated, + SharingLevel: proto.AppSharingLevel_AUTHENTICATED, + Url: appURL, + }, + { + Name: proxyTestAppNamePublic, + SharingLevel: proto.AppSharingLevel_PUBLIC, + Url: appURL, + }, }, }}, }}, @@ -180,7 +204,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 +225,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 +237,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 +249,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 +264,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 +279,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 +305,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) @@ -611,7 +635,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) @@ -623,7 +647,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() @@ -640,7 +664,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() @@ -657,7 +681,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) @@ -687,7 +711,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) @@ -712,3 +736,209 @@ 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, clientWithTemplateAccess *codersdk.Client, clientWithNoTemplateAccess *codersdk.Client, clientWithNoAuth *codersdk.Client) { + //nolint:gosec + const password = "password" + + client, firstUser, 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, + proxyTestAppNameTemplate: codersdk.WorkspaceAppSharingLevelTemplate, + 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 the same org (should be able to read the template). + userWithTemplateAccess, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + Email: "template-access@coder.com", + Username: "template-access", + Password: password, + OrganizationID: firstUser.OrganizationID, + }) + require.NoError(t, err) + + clientWithTemplateAccess = codersdk.New(client.URL) + loginRes, err := clientWithTemplateAccess.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{ + Email: userWithTemplateAccess.Email, + Password: password, + }) + require.NoError(t, err) + clientWithTemplateAccess.SessionToken = loginRes.SessionToken + clientWithTemplateAccess.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + + // Double check that the user can read the template. + _, err = clientWithTemplateAccess.Template(ctx, workspace.TemplateID) + require.NoError(t, err) + + // Create a user in a different org (should not be able to read the + // template). + differentOrg, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: "a-different-org", + }) + require.NoError(t, err) + userWithNoTemplateAccess, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + Email: "no-template-access@coder.com", + Username: "no-template-access", + Password: password, + OrganizationID: differentOrg.ID, + }) + require.NoError(t, err) + + clientWithNoTemplateAccess = codersdk.New(client.URL) + loginRes, err = clientWithNoTemplateAccess.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{ + Email: userWithNoTemplateAccess.Email, + Password: password, + }) + require.NoError(t, err) + clientWithNoTemplateAccess.SessionToken = loginRes.SessionToken + clientWithNoTemplateAccess.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + + // Double check that the user cannot read the template. + _, err = clientWithNoTemplateAccess.Template(ctx, workspace.TemplateID) + require.Error(t, err) + + // 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, clientWithTemplateAccess, clientWithNoTemplateAccess, 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} + // TODO: generate scoped token and add to slice + + 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, clientWithTemplateAccess, clientWithNoTemplateAccess, 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) + + // User with or without template access should not have access to a + // workspace that they do not own. + verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameOwner, clientWithTemplateAccess, false, false) + verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameOwner, clientWithNoTemplateAccess, false, false) + + // Unauthenticated user should not have any access. + verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameOwner, clientWithNoAuth, false, true) + }) + + t.Run("Template", func(t *testing.T) { + t.Parallel() + + // Owner should be able to access their own workspace. + verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameTemplate, client, true, false) + + // User with template access should be able to access the workspace. + verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameTemplate, clientWithTemplateAccess, true, false) + + // User without template access should not have access to a workspace + // that they do not own. + verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameTemplate, clientWithNoTemplateAccess, false, false) + + // Unauthenticated user should not have any access. + verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameTemplate, 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) + + // User with or without template access should be able to access the + // workspace. + verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameAuthenticated, clientWithTemplateAccess, true, false) + verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameAuthenticated, clientWithNoTemplateAccess, 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) + + // User with or without template access should be able to access the + // workspace. + verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNamePublic, clientWithTemplateAccess, true, false) + verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNamePublic, clientWithNoTemplateAccess, 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/features.go b/codersdk/features.go index 25213d3e0f828..3b57d6eeb3853 100644 --- a/codersdk/features.go +++ b/codersdk/features.go @@ -15,12 +15,11 @@ const ( ) const ( - FeatureUserLimit = "user_limit" - FeatureAuditLog = "audit_log" - FeatureBrowserOnly = "browser_only" - FeatureSCIM = "scim" - FeatureWorkspaceQuota = "workspace_quota" - FeatureApplicationSharing = "application_sharing" + FeatureUserLimit = "user_limit" + FeatureAuditLog = "audit_log" + FeatureBrowserOnly = "browser_only" + FeatureSCIM = "scim" + FeatureWorkspaceQuota = "workspace_quota" ) var FeatureNames = []string{ @@ -29,7 +28,6 @@ var FeatureNames = []string{ FeatureBrowserOnly, FeatureSCIM, FeatureWorkspaceQuota, - FeatureApplicationSharing, } type Feature struct { diff --git a/enterprise/cli/features_test.go b/enterprise/cli/features_test.go index 9f606a43f2ae1..da2425634cab9 100644 --- a/enterprise/cli/features_test.go +++ b/enterprise/cli/features_test.go @@ -57,7 +57,7 @@ func TestFeaturesList(t *testing.T) { var entitlements codersdk.Entitlements err := json.Unmarshal(buf.Bytes(), &entitlements) require.NoError(t, err, "unmarshal JSON output") - assert.Len(t, entitlements.Features, 5) + assert.Len(t, entitlements.Features, 4) assert.Empty(t, entitlements.Warnings) assert.Equal(t, codersdk.EntitlementNotEntitled, entitlements.Features[codersdk.FeatureUserLimit].Entitlement) @@ -67,8 +67,6 @@ func TestFeaturesList(t *testing.T) { entitlements.Features[codersdk.FeatureBrowserOnly].Entitlement) assert.Equal(t, codersdk.EntitlementNotEntitled, entitlements.Features[codersdk.FeatureWorkspaceQuota].Entitlement) - assert.Equal(t, codersdk.EntitlementNotEntitled, - entitlements.Features[codersdk.FeatureApplicationSharing].Entitlement) assert.False(t, entitlements.HasLicense) assert.False(t, entitlements.Experimental) }) diff --git a/enterprise/cli/server.go b/enterprise/cli/server.go index 67a0d354b2bab..5912a9f3fa731 100644 --- a/enterprise/cli/server.go +++ b/enterprise/cli/server.go @@ -7,7 +7,6 @@ import ( "github.com/coder/coder/cli/cliflag" "github.com/coder/coder/cli/cliui" - "github.com/coder/coder/coderd/database" "github.com/coder/coder/enterprise/coderd" agpl "github.com/coder/coder/cli" @@ -16,25 +15,18 @@ import ( func server() *cobra.Command { var ( - auditLogging bool - browserOnly bool - scimAuthHeader string - userWorkspaceQuota int - allowedApplicationSharingLevels []string + auditLogging bool + browserOnly bool + scimAuthHeader string + userWorkspaceQuota int ) cmd := agpl.Server(func(ctx context.Context, options *agplcoderd.Options) (*agplcoderd.API, error) { - appSharingLevels := make([]database.AppSharingLevel, len(allowedApplicationSharingLevels)) - for i, val := range allowedApplicationSharingLevels { - appSharingLevels[i] = database.AppSharingLevel(val) - } - api, err := coderd.New(ctx, &coderd.Options{ - AuditLogging: auditLogging, - BrowserOnly: browserOnly, - SCIMAPIKey: []byte(scimAuthHeader), - UserWorkspaceQuota: userWorkspaceQuota, - AllowedApplicationSharingLevels: appSharingLevels, - Options: options, + AuditLogging: auditLogging, + BrowserOnly: browserOnly, + SCIMAPIKey: []byte(scimAuthHeader), + UserWorkspaceQuota: userWorkspaceQuota, + Options: options, }) if err != nil { return nil, err @@ -51,8 +43,6 @@ func server() *cobra.Command { "Enables SCIM and sets the authentication header for the built-in SCIM server. New users are automatically created with OIDC authentication. "+enterpriseOnly) cliflag.IntVarP(cmd.Flags(), &userWorkspaceQuota, "user-workspace-quota", "", "CODER_USER_WORKSPACE_QUOTA", 0, "A positive number applies a limit on how many workspaces each user can create. "+enterpriseOnly) - cliflag.StringArrayVarP(cmd.Flags(), &allowedApplicationSharingLevels, "permitted-app-sharing-levels", "", "CODER_PERMITTED_APP_SHARING_LEVELS", []string{"owner"}, - `Specifies the application sharing levels that are available site-wide. Available values are "owner", "template", "authenticated", "public". Multiple values can be specified, comma separated. `+enterpriseOnly) return cmd } diff --git a/enterprise/coderd/appsharing.go b/enterprise/coderd/appsharing.go deleted file mode 100644 index f1c2f3891d5c7..0000000000000 --- a/enterprise/coderd/appsharing.go +++ /dev/null @@ -1,103 +0,0 @@ -package coderd - -import ( - "net/http" - - "golang.org/x/xerrors" - - agplcoderd "github.com/coder/coder/coderd" - "github.com/coder/coder/coderd/database" - "github.com/coder/coder/coderd/httpmw" - "github.com/coder/coder/coderd/rbac" -) - -// EnterpriseAppAuthorizer provides an enterprise implementation of -// agplcoderd.AppAuthorizer that allows apps to be shared at certain levels. -type EnterpriseAppAuthorizer struct { - RBAC rbac.Authorizer - LevelOwnerAllowed bool - LevelTemplateAllowed bool - LevelAuthenticatedAllowed bool - LevelPublicAllowed bool -} - -var _ agplcoderd.AppAuthorizer = &EnterpriseAppAuthorizer{} - -// Authorize implements agplcoderd.AppAuthorizer. -func (a *EnterpriseAppAuthorizer) Authorize(r *http.Request, db database.Store, 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 and the public level is allowed. - return SharingLevel == database.AppSharingLevelPublic && a.LevelPublicAllowed, 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. - err := a.RBAC.ByRoleName(ctx, roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), rbac.ActionCreate, workspace.ApplicationConnectRBAC()) - if err == nil { - return true, nil - } - - // Ensure the app's share level is allowed. - switch SharingLevel { - case database.AppSharingLevelOwner: - if !a.LevelOwnerAllowed { - return false, nil - } - case database.AppSharingLevelTemplate: - if !a.LevelTemplateAllowed { - return false, nil - } - case database.AppSharingLevelAuthenticated: - if !a.LevelAuthenticatedAllowed { - return false, nil - } - case database.AppSharingLevelPublic: - if !a.LevelPublicAllowed { - return false, nil - } - default: - return false, xerrors.Errorf("unknown workspace app sharing level %q", SharingLevel) - } - - switch SharingLevel { - case database.AppSharingLevelOwner: - // We essentially already did this above. - case database.AppSharingLevelTemplate: - // Check if the user has access to the same template as the workspace. - template, err := db.GetTemplateByID(ctx, workspace.TemplateID) - if err != nil { - return false, xerrors.Errorf("get template %q: %w", workspace.TemplateID, err) - } - - err = a.RBAC.ByRoleName(ctx, roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), rbac.ActionRead, template.RBACObject()) - if err == nil { - return true, nil - } - 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 := a.RBAC.ByRoleName(ctx, roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), 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 - } - - // No checks were successful. - return false, nil -} diff --git a/enterprise/coderd/appsharing_test.go b/enterprise/coderd/appsharing_test.go deleted file mode 100644 index 6f70dae73b48c..0000000000000 --- a/enterprise/coderd/appsharing_test.go +++ /dev/null @@ -1,361 +0,0 @@ -package coderd_test - -import ( - "context" - "fmt" - "net" - "net/http" - "net/http/httputil" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/coder/coder/coderd/coderdtest" - "github.com/coder/coder/coderd/database" - "github.com/coder/coder/codersdk" - "github.com/coder/coder/enterprise/coderd/coderdenttest" - "github.com/coder/coder/testutil" -) - -func setupAppAuthorizerTest(t *testing.T, allowedSharingLevels []database.AppSharingLevel) (workspace codersdk.Workspace, agent codersdk.WorkspaceAgent, user codersdk.User, client *codersdk.Client, clientWithTemplateAccess *codersdk.Client, clientWithNoTemplateAccess *codersdk.Client, clientWithNoAuth *codersdk.Client) { - //nolint:gosec - const password = "password" - - // Create a hello world server. - //nolint:gosec - ln, err := net.Listen("tcp", ":0") - require.NoError(t, err) - server := http.Server{ - ReadHeaderTimeout: time.Minute, - Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte("Hello World")) - }), - } - t.Cleanup(func() { - _ = server.Close() - _ = ln.Close() - }) - go server.Serve(ln) - tcpAddr, ok := ln.Addr().(*net.TCPAddr) - require.True(t, ok) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - t.Cleanup(cancel) - - // Setup a user, template with apps, workspace on a coderdtest using the - // EnterpriseAppAuthorizer. - client = coderdenttest.New(t, &coderdenttest.Options{ - AllowedApplicationSharingLevels: allowedSharingLevels, - Options: &coderdtest.Options{ - IncludeProvisionerDaemon: true, - }, - }) - client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - } - - firstUser := coderdtest.CreateFirstUser(t, client) - user, err = client.User(ctx, firstUser.UserID.String()) - require.NoError(t, err) - coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - ApplicationSharing: true, - }) - workspace, agent = setupWorkspaceAgent(t, client, firstUser, uint16(tcpAddr.Port)) - - // Verify that the apps have the correct sharing levels set. - workspaceBuild, err := client.WorkspaceBuild(ctx, workspace.LatestBuild.ID) - require.NoError(t, err) - found := map[string]codersdk.WorkspaceAppSharingLevel{} - expected := map[string]codersdk.WorkspaceAppSharingLevel{ - testAppNameOwner: codersdk.WorkspaceAppSharingLevelOwner, - testAppNameTemplate: codersdk.WorkspaceAppSharingLevelTemplate, - testAppNameAuthenticated: codersdk.WorkspaceAppSharingLevelAuthenticated, - testAppNamePublic: codersdk.WorkspaceAppSharingLevelPublic, - } - for _, app := range workspaceBuild.Resources[0].Agents[0].Apps { - found[app.Name] = app.SharingLevel - } - require.Equal(t, expected, found, "apps have incorrect sharing levels") - - // Create a user in the same org (should be able to read the template). - userWithTemplateAccess, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ - Email: "template-access@coder.com", - Username: "template-access", - Password: password, - OrganizationID: firstUser.OrganizationID, - }) - require.NoError(t, err) - - clientWithTemplateAccess = codersdk.New(client.URL) - loginRes, err := clientWithTemplateAccess.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{ - Email: userWithTemplateAccess.Email, - Password: password, - }) - require.NoError(t, err) - clientWithTemplateAccess.SessionToken = loginRes.SessionToken - clientWithTemplateAccess.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - } - - // Double check that the user can read the template. - _, err = clientWithTemplateAccess.Template(ctx, workspace.TemplateID) - require.NoError(t, err) - - // Create a user in a different org (should not be able to read the - // template). - differentOrg, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "a-different-org", - }) - require.NoError(t, err) - userWithNoTemplateAccess, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ - Email: "no-template-access@coder.com", - Username: "no-template-access", - Password: password, - OrganizationID: differentOrg.ID, - }) - require.NoError(t, err) - - clientWithNoTemplateAccess = codersdk.New(client.URL) - loginRes, err = clientWithNoTemplateAccess.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{ - Email: userWithNoTemplateAccess.Email, - Password: password, - }) - require.NoError(t, err) - clientWithNoTemplateAccess.SessionToken = loginRes.SessionToken - clientWithNoTemplateAccess.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - } - - // Double check that the user cannot read the template. - _, err = clientWithNoTemplateAccess.Template(ctx, workspace.TemplateID) - require.Error(t, err) - - // 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, agent, user, client, clientWithTemplateAccess, clientWithNoTemplateAccess, clientWithNoAuth -} - -func TestEnterpriseAppAuthorizer(t *testing.T) { - t.Parallel() - - 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() - - appPath := fmt.Sprintf("/@%s/%s.%s/apps/%s/", username, workspaceName, agentName, appName) - res, err := client.Request(ctx, http.MethodGet, appPath, nil) - require.NoError(t, err) - defer res.Body.Close() - - dump, err := httputil.DumpResponse(res, true) - require.NoError(t, err) - t.Logf("response dump: %s", dump) - - if !shouldHaveAccess { - if shouldRedirectToLogin { - assert.Equal(t, http.StatusTemporaryRedirect, res.StatusCode, "should not have access, expected temporary redirect") - location, err := res.Location() - require.NoError(t, err) - assert.Equal(t, "/login", location.Path, "should not have access, expected redirect to /login") - } 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") - } - } - - if shouldHaveAccess { - assert.Equal(t, http.StatusOK, res.StatusCode, "should have access, expected ok") - assert.Contains(t, string(dump), "Hello World", "should have access, expected hello world") - } - } - - t.Run("Disabled", func(t *testing.T) { - t.Parallel() - workspace, agent, user, client, clientWithTemplateAccess, clientWithNoTemplateAccess, clientWithNoAuth := setupAppAuthorizerTest(t, []database.AppSharingLevel{ - // Disabled basically means only the owner level is allowed. This - // should have feature parity with the AGPL version. - database.AppSharingLevelOwner, - }) - - // Owner should be able to access their own workspace. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, client, true, false) - - // User with or without template access should not have access to a - // workspace that they do not own. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, clientWithTemplateAccess, false, false) - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, clientWithNoTemplateAccess, false, false) - - // Unauthenticated user should not have any access. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, clientWithNoAuth, false, true) - }) - - t.Run("Level", func(t *testing.T) { - t.Parallel() - - // For the purposes of the level tests we allow all levels. - workspace, agent, user, client, clientWithTemplateAccess, clientWithNoTemplateAccess, clientWithNoAuth := setupAppAuthorizerTest(t, []database.AppSharingLevel{ - database.AppSharingLevelOwner, - database.AppSharingLevelTemplate, - database.AppSharingLevelAuthenticated, - database.AppSharingLevelPublic, - }) - - 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, testAppNameOwner, client, true, false) - - // User with or without template access should not have access to a - // workspace that they do not own. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, clientWithTemplateAccess, false, false) - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, clientWithNoTemplateAccess, false, false) - - // Unauthenticated user should not have any access. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, clientWithNoAuth, false, true) - }) - - t.Run("Template", func(t *testing.T) { - t.Parallel() - - // Owner should be able to access their own workspace. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameTemplate, client, true, false) - - // User with template access should be able to access the workspace. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameTemplate, clientWithTemplateAccess, true, false) - - // User without template access should not have access to a workspace - // that they do not own. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameTemplate, clientWithNoTemplateAccess, false, false) - - // Unauthenticated user should not have any access. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameTemplate, 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, testAppNameAuthenticated, client, true, false) - - // User with or without template access should be able to access the - // workspace. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameAuthenticated, clientWithTemplateAccess, true, false) - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameAuthenticated, clientWithNoTemplateAccess, true, false) - - // Unauthenticated user should not have any access. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameAuthenticated, 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, testAppNamePublic, client, true, false) - - // User with or without template access should be able to access the - // workspace. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNamePublic, clientWithTemplateAccess, true, false) - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNamePublic, clientWithNoTemplateAccess, true, false) - - // Unauthenticated user should be able to access the workspace. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNamePublic, clientWithNoAuth, true, false) - }) - }) - - t.Run("LevelBlockedByAdmin", func(t *testing.T) { - t.Parallel() - - t.Run("Owner", func(t *testing.T) { - t.Parallel() - - // All levels allowed except owner. - workspace, agent, user, client, clientWithTemplateAccess, clientWithNoTemplateAccess, clientWithNoAuth := setupAppAuthorizerTest(t, []database.AppSharingLevel{ - database.AppSharingLevelTemplate, - database.AppSharingLevelAuthenticated, - database.AppSharingLevelPublic, - }) - - // Owner can always access their own workspace. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, client, true, false) - - // All other users should always be blocked anyways. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, clientWithTemplateAccess, false, false) - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, clientWithNoTemplateAccess, false, false) - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameOwner, clientWithNoAuth, false, true) - }) - - t.Run("Template", func(t *testing.T) { - t.Parallel() - - // All levels allowed except template. - workspace, agent, user, client, clientWithTemplateAccess, clientWithNoTemplateAccess, clientWithNoAuth := setupAppAuthorizerTest(t, []database.AppSharingLevel{ - database.AppSharingLevelOwner, - database.AppSharingLevelAuthenticated, - database.AppSharingLevelPublic, - }) - - // Owner can always access their own workspace. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameTemplate, client, true, false) - - // User with template access should not be able to access the - // workspace as the template level is disallowed. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameTemplate, clientWithTemplateAccess, false, false) - - // All other users should always be blocked anyways. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameTemplate, clientWithNoTemplateAccess, false, false) - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameTemplate, clientWithNoAuth, false, true) - }) - - t.Run("Authenticated", func(t *testing.T) { - t.Parallel() - - // All levels allowed except authenticated. - workspace, agent, user, client, clientWithTemplateAccess, clientWithNoTemplateAccess, clientWithNoAuth := setupAppAuthorizerTest(t, []database.AppSharingLevel{ - database.AppSharingLevelOwner, - database.AppSharingLevelTemplate, - database.AppSharingLevelPublic, - }) - - // Owner can always access their own workspace. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameAuthenticated, client, true, false) - - // User with or without template access should not be able to access - // the workspace as the authenticated level is disallowed. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameAuthenticated, clientWithTemplateAccess, false, false) - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameAuthenticated, clientWithNoTemplateAccess, false, false) - - // Unauthenticated users should be blocked anyways. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNameAuthenticated, clientWithNoAuth, false, true) - }) - - t.Run("Public", func(t *testing.T) { - t.Parallel() - - // All levels allowed except public. - workspace, agent, user, client, clientWithTemplateAccess, clientWithNoTemplateAccess, clientWithNoAuth := setupAppAuthorizerTest(t, []database.AppSharingLevel{ - database.AppSharingLevelOwner, - database.AppSharingLevelTemplate, - database.AppSharingLevelAuthenticated, - }) - - // Owner can always access their own workspace. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNamePublic, client, true, false) - - // All other users should be blocked because the public level is - // disallowed. - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNamePublic, clientWithTemplateAccess, false, false) - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNamePublic, clientWithNoTemplateAccess, false, false) - verifyAccess(t, user.Username, workspace.Name, agent.Name, testAppNamePublic, clientWithNoAuth, false, true) - }) - }) -} diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 40e38c5a25cab..d6ddedf91f106 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -15,7 +15,6 @@ import ( "cdr.dev/slog" "github.com/coder/coder/coderd" - "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/httpmw" "github.com/coder/coder/coderd/rbac" @@ -40,36 +39,6 @@ func New(ctx context.Context, options *Options) (*API, error) { if options.Options.Authorizer == nil { options.Options.Authorizer = rbac.NewAuthorizer() } - if options.Options.AppAuthorizer == nil { - var ( - // The default is that only level "owner" should be allowed. - levelOwnerAllowed = len(options.AllowedApplicationSharingLevels) == 0 - levelTemplateAllowed = false - levelAuthenticatedAllowed = false - levelPublicAllowed = false - ) - for _, v := range options.AllowedApplicationSharingLevels { - switch v { - case database.AppSharingLevelOwner: - levelOwnerAllowed = true - case database.AppSharingLevelTemplate: - levelTemplateAllowed = true - case database.AppSharingLevelAuthenticated: - levelAuthenticatedAllowed = true - case database.AppSharingLevelPublic: - levelPublicAllowed = true - default: - return nil, xerrors.Errorf("unknown workspace app sharing level %q", v) - } - } - options.Options.AppAuthorizer = &EnterpriseAppAuthorizer{ - RBAC: options.Options.Authorizer, - LevelOwnerAllowed: levelOwnerAllowed, - LevelTemplateAllowed: levelTemplateAllowed, - LevelAuthenticatedAllowed: levelAuthenticatedAllowed, - LevelPublicAllowed: levelPublicAllowed, - } - } ctx, cancelFunc := context.WithCancel(ctx) api := &API{ AGPL: coderd.New(options.Options), @@ -145,9 +114,6 @@ type Options struct { BrowserOnly bool SCIMAPIKey []byte UserWorkspaceQuota int - // Defaults to []database.AppSharingLevel{database.AppSharingLevelOwner} which - // essentially means "function identically to AGPL Coder". - AllowedApplicationSharingLevels []database.AppSharingLevel EntitlementsUpdateInterval time.Duration Keys map[string]ed25519.PublicKey @@ -239,9 +205,6 @@ func (api *API) updateEntitlements(ctx context.Context) error { if claims.Features.WorkspaceQuota > 0 { entitlements.workspaceQuota = entitlement } - if claims.Features.ApplicationSharing > 0 { - entitlements.applicationSharing = entitlement - } } if entitlements.auditLogs != api.entitlements.auditLogs { @@ -357,27 +320,6 @@ func (api *API) serveEntitlements(rw http.ResponseWriter, r *http.Request) { } } - // App sharing is disabled if no levels are allowed or the only allowed - // level is "owner". - appSharingEnabled := true - if len(api.AllowedApplicationSharingLevels) == 0 || (len(api.AllowedApplicationSharingLevels) == 1 && api.AllowedApplicationSharingLevels[0] == database.AppSharingLevelOwner) { - appSharingEnabled = false - } - resp.Features[codersdk.FeatureApplicationSharing] = codersdk.Feature{ - Entitlement: entitlements.applicationSharing, - Enabled: appSharingEnabled, - } - if appSharingEnabled { - if entitlements.applicationSharing == codersdk.EntitlementNotEntitled { - resp.Warnings = append(resp.Warnings, - "Application sharing is enabled but your license is not entitled to this feature.") - } - if entitlements.applicationSharing == codersdk.EntitlementGracePeriod { - resp.Warnings = append(resp.Warnings, - "Application sharing is enabled but your license for this feature is expired.") - } - } - httpapi.Write(ctx, rw, http.StatusOK, resp) } diff --git a/enterprise/coderd/coderdenttest/coderdenttest.go b/enterprise/coderd/coderdenttest/coderdenttest.go index 655eccff843ac..04297f04400f6 100644 --- a/enterprise/coderd/coderdenttest/coderdenttest.go +++ b/enterprise/coderd/coderdenttest/coderdenttest.go @@ -13,7 +13,6 @@ import ( "github.com/stretchr/testify/require" "github.com/coder/coder/coderd/coderdtest" - "github.com/coder/coder/coderd/database" "github.com/coder/coder/codersdk" "github.com/coder/coder/enterprise/coderd" ) @@ -37,12 +36,11 @@ func init() { type Options struct { *coderdtest.Options - AuditLogging bool - BrowserOnly bool - EntitlementsUpdateInterval time.Duration - SCIMAPIKey []byte - UserWorkspaceQuota int - AllowedApplicationSharingLevels []database.AppSharingLevel + AuditLogging bool + BrowserOnly bool + EntitlementsUpdateInterval time.Duration + SCIMAPIKey []byte + UserWorkspaceQuota int } // New constructs a codersdk client connected to an in-memory Enterprise API instance. @@ -60,13 +58,12 @@ func NewWithAPI(t *testing.T, options *Options) (*codersdk.Client, io.Closer, *c } srv, cancelFunc, oop := coderdtest.NewOptions(t, options.Options) coderAPI, err := coderd.New(context.Background(), &coderd.Options{ - AuditLogging: options.AuditLogging, - BrowserOnly: options.BrowserOnly, - SCIMAPIKey: options.SCIMAPIKey, - UserWorkspaceQuota: options.UserWorkspaceQuota, - AllowedApplicationSharingLevels: options.AllowedApplicationSharingLevels, - Options: oop, - EntitlementsUpdateInterval: options.EntitlementsUpdateInterval, + AuditLogging: options.AuditLogging, + BrowserOnly: options.BrowserOnly, + SCIMAPIKey: options.SCIMAPIKey, + UserWorkspaceQuota: options.UserWorkspaceQuota, + Options: oop, + EntitlementsUpdateInterval: options.EntitlementsUpdateInterval, Keys: map[string]ed25519.PublicKey{ testKeyID: testPublicKey, }, @@ -86,16 +83,15 @@ func NewWithAPI(t *testing.T, options *Options) (*codersdk.Client, io.Closer, *c } type LicenseOptions struct { - AccountType string - AccountID string - GraceAt time.Time - ExpiresAt time.Time - UserLimit int64 - AuditLog bool - BrowserOnly bool - SCIM bool - WorkspaceQuota bool - ApplicationSharing bool + AccountType string + AccountID string + GraceAt time.Time + ExpiresAt time.Time + UserLimit int64 + AuditLog bool + BrowserOnly bool + SCIM bool + WorkspaceQuota bool } // AddLicense generates a new license with the options provided and inserts it. @@ -131,10 +127,6 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string { if options.WorkspaceQuota { workspaceQuota = 1 } - var applicationSharing int64 - if options.ApplicationSharing { - applicationSharing = 1 - } c := &coderd.Claims{ RegisteredClaims: jwt.RegisteredClaims{ @@ -148,12 +140,11 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string { AccountID: options.AccountID, Version: coderd.CurrentVersion, Features: coderd.Features{ - UserLimit: options.UserLimit, - AuditLog: auditLog, - BrowserOnly: browserOnly, - SCIM: scim, - WorkspaceQuota: workspaceQuota, - ApplicationSharing: applicationSharing, + UserLimit: options.UserLimit, + AuditLog: auditLog, + BrowserOnly: browserOnly, + SCIM: scim, + WorkspaceQuota: workspaceQuota, }, } tok := jwt.NewWithClaims(jwt.SigningMethodEdDSA, c) diff --git a/enterprise/coderd/licenses.go b/enterprise/coderd/licenses.go index a0afce40cb49e..9d43bbe6c2996 100644 --- a/enterprise/coderd/licenses.go +++ b/enterprise/coderd/licenses.go @@ -45,12 +45,11 @@ var key20220812 []byte var Keys = map[string]ed25519.PublicKey{"2022-08-12": ed25519.PublicKey(key20220812)} type Features struct { - UserLimit int64 `json:"user_limit"` - AuditLog int64 `json:"audit_log"` - BrowserOnly int64 `json:"browser_only"` - SCIM int64 `json:"scim"` - WorkspaceQuota int64 `json:"workspace_quota"` - ApplicationSharing int64 `json:"application_sharing"` + UserLimit int64 `json:"user_limit"` + AuditLog int64 `json:"audit_log"` + BrowserOnly int64 `json:"browser_only"` + SCIM int64 `json:"scim"` + WorkspaceQuota int64 `json:"workspace_quota"` } type Claims struct { diff --git a/enterprise/coderd/licenses_test.go b/enterprise/coderd/licenses_test.go index 33b4773b1ddd2..c4b7111597079 100644 --- a/enterprise/coderd/licenses_test.go +++ b/enterprise/coderd/licenses_test.go @@ -98,22 +98,20 @@ func TestGetLicense(t *testing.T) { assert.Equal(t, int32(1), licenses[0].ID) assert.Equal(t, "testing", licenses[0].Claims["account_id"]) assert.Equal(t, map[string]interface{}{ - codersdk.FeatureUserLimit: json.Number("0"), - codersdk.FeatureAuditLog: json.Number("1"), - codersdk.FeatureSCIM: json.Number("1"), - codersdk.FeatureBrowserOnly: json.Number("1"), - codersdk.FeatureWorkspaceQuota: json.Number("0"), - codersdk.FeatureApplicationSharing: json.Number("0"), + codersdk.FeatureUserLimit: json.Number("0"), + codersdk.FeatureAuditLog: json.Number("1"), + codersdk.FeatureSCIM: json.Number("1"), + codersdk.FeatureBrowserOnly: json.Number("1"), + codersdk.FeatureWorkspaceQuota: json.Number("0"), }, licenses[0].Claims["features"]) assert.Equal(t, int32(2), licenses[1].ID) assert.Equal(t, "testing2", licenses[1].Claims["account_id"]) assert.Equal(t, map[string]interface{}{ - codersdk.FeatureUserLimit: json.Number("200"), - codersdk.FeatureAuditLog: json.Number("1"), - codersdk.FeatureSCIM: json.Number("1"), - codersdk.FeatureBrowserOnly: json.Number("1"), - codersdk.FeatureWorkspaceQuota: json.Number("0"), - codersdk.FeatureApplicationSharing: json.Number("0"), + codersdk.FeatureUserLimit: json.Number("200"), + codersdk.FeatureAuditLog: json.Number("1"), + codersdk.FeatureSCIM: json.Number("1"), + codersdk.FeatureBrowserOnly: json.Number("1"), + codersdk.FeatureWorkspaceQuota: json.Number("0"), }, licenses[1].Claims["features"]) }) } diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go index 4a9f22b5a1766..f25481ac7d317 100644 --- a/enterprise/coderd/workspaceagents_test.go +++ b/enterprise/coderd/workspaceagents_test.go @@ -82,7 +82,6 @@ func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.Cr Auth: &proto.Agent_Token{ Token: authToken, }, - // TODO: sharing levels Apps: []*proto.App{ { Name: testAppNameOwner, From 89a75d0414f705f0f438ccd2cbed612cf7bc7931 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 12 Oct 2022 17:24:49 +0000 Subject: [PATCH 06/12] fixup! Merge branch 'main' into dean/app-sharing --- coderd/httpmw/apikey.go | 1 - coderd/httpmw/userparam.go | 2 ++ coderd/workspaceapps.go | 18 +++++++++--------- enterprise/coderd/workspaceagents_test.go | 4 ++-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index 1f3f4e7941603..cc331983ce1ee 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -427,5 +427,4 @@ func RedirectToLogin(rw http.ResponseWriter, r *http.Request, message string) { } http.Redirect(rw, r, u.String(), http.StatusTemporaryRedirect) - return } diff --git a/coderd/httpmw/userparam.go b/coderd/httpmw/userparam.go index 5ac87c2dcfefb..74119d503a97b 100644 --- a/coderd/httpmw/userparam.go +++ b/coderd/httpmw/userparam.go @@ -35,6 +35,8 @@ func UserParam(r *http.Request) database.User { // 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) { diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 3f8b5cf99e615..79e30c35333e6 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -58,11 +58,11 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) return } - AppSharingLevel := database.AppSharingLevelOwner + appSharingLevel := database.AppSharingLevelOwner if app.SharingLevel != "" { - AppSharingLevel = app.SharingLevel + appSharingLevel = app.SharingLevel } - authed, ok := api.fetchWorkspaceApplicationAuth(rw, r, workspace, AppSharingLevel) + authed, ok := api.fetchWorkspaceApplicationAuth(rw, r, workspace, appSharingLevel) if !ok { return } @@ -191,11 +191,11 @@ func (api *API) handleSubdomainApplications(middlewares ...func(http.Handler) ht // Verify application auth. This function will redirect or // return an error page if the user doesn't have permission. - SharingLevel := database.AppSharingLevelOwner + sharingLevel := database.AppSharingLevelOwner if workspaceAppPtr != nil && workspaceAppPtr.SharingLevel != "" { - SharingLevel = workspaceAppPtr.SharingLevel + sharingLevel = workspaceAppPtr.SharingLevel } - if !api.verifyWorkspaceApplicationSubdomainAuth(rw, r, host, workspace, SharingLevel) { + if !api.verifyWorkspaceApplicationSubdomainAuth(rw, r, host, workspace, sharingLevel) { return } @@ -598,11 +598,11 @@ type proxyApplication struct { func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - SharingLevel := database.AppSharingLevelOwner + sharingLevel := database.AppSharingLevelOwner if proxyApp.App != nil && proxyApp.App.SharingLevel != "" { - SharingLevel = proxyApp.App.SharingLevel + sharingLevel = proxyApp.App.SharingLevel } - if !api.checkWorkspaceApplicationAuth(rw, r, proxyApp.Workspace, SharingLevel) { + if !api.checkWorkspaceApplicationAuth(rw, r, proxyApp.Workspace, sharingLevel) { return } diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go index f25481ac7d317..645e2ec3dd8d1 100644 --- a/enterprise/coderd/workspaceagents_test.go +++ b/enterprise/coderd/workspaceagents_test.go @@ -129,8 +129,8 @@ func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.Cr defer cancel() resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) - agent, err := client.WorkspaceAgent(ctx, resources[0].Agents[0].ID) + agnt, err := client.WorkspaceAgent(ctx, resources[0].Agents[0].ID) require.NoError(t, err) - return workspace, agent + return workspace, agnt } From 4f6aac886543b9dd6f4e30784e935ac530b81ac8 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 12 Oct 2022 19:49:09 +0000 Subject: [PATCH 07/12] chore: add test for app sharing with scoped keys --- cli/tokens.go | 2 +- coderd/apikey.go | 17 ++++++++ coderd/apikey_test.go | 73 +++++++++++++++++++++++--------- coderd/users.go | 1 + coderd/users_test.go | 4 +- coderd/workspaceapps.go | 13 +++++- coderd/workspaceapps_test.go | 13 +++++- codersdk/apikey.go | 56 ++++++++++++++++-------- codersdk/users.go | 5 --- site/src/api/typesGenerated.ts | 11 ++++- site/src/testHelpers/entities.ts | 1 + 11 files changed, 146 insertions(+), 50 deletions(-) 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/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/workspaceapps.go b/coderd/workspaceapps.go index 79e30c35333e6..b954df25bc459 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -327,7 +327,18 @@ func (api *API) authorizeWorkspaceApp(r *http.Request, sharingLevel database.App return false, xerrors.Errorf("get template %q: %w", workspace.TemplateID, err) } - err = api.Authorizer.ByRoleName(ctx, roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), []string{}, rbac.ActionRead, template.RBACObject()) + // We have to perform this check without scopes enabled because + // otherwise this check will always fail on a scoped API key. + err = api.Authorizer.ByRoleName(ctx, roles.ID.String(), roles.Roles, rbac.ScopeAll, []string{}, rbac.ActionRead, template.RBACObject()) + if err != nil { + // Exit early if the user doesn't have access to the template. + return false, nil + } + + // Now check if the user has ApplicationConnect access to their own + // workspaces. + 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 } diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go index 34806af1549e0..4a0965172b460 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -836,7 +836,18 @@ func TestAppSharing(t *testing.T) { // If the client has a session token, we also want to check that a // scoped key works. clients := []*codersdk.Client{client} - // TODO: generate scoped token and add to slice + 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) 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/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index afad2481f9420..0f302c6cce2d1 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 } @@ -852,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" diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index a391a8ea298ea..cfe39959b24db 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, From 9f458507436ec6250ceabcc1a50fbda93b34e85e Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 12 Oct 2022 20:03:30 +0000 Subject: [PATCH 08/12] chore: add share to all example templates --- .../migrations/000059_app_sharing_level.down.sql | 8 ++++---- dogfood/main.tf | 13 +++++++------ examples/templates/aws-ecs-container/main.tf | 3 ++- examples/templates/aws-linux/main.tf | 12 +++++++----- examples/templates/aws-windows/main.tf | 2 +- examples/templates/azure-linux/main.tf | 2 +- examples/templates/bare/main.tf | 10 ++++++---- examples/templates/do-linux/main.tf | 2 +- examples/templates/docker-code-server/main.tf | 11 +++++++---- examples/templates/docker-image-builds/main.tf | 12 +++++++----- examples/templates/docker-with-dotfiles/main.tf | 2 +- examples/templates/docker/main.tf | 13 ++++++++----- examples/templates/gcp-linux/main.tf | 3 ++- examples/templates/gcp-vm-container/main.tf | 3 ++- examples/templates/gcp-windows/main.tf | 2 +- examples/templates/kubernetes/main.tf | 3 ++- provisionersdk/proto/provisioner.pb.go | 2 +- 17 files changed, 60 insertions(+), 43 deletions(-) diff --git a/coderd/database/migrations/000059_app_sharing_level.down.sql b/coderd/database/migrations/000059_app_sharing_level.down.sql index 501e62880df44..757a7f8792740 100644 --- a/coderd/database/migrations/000059_app_sharing_level.down.sql +++ b/coderd/database/migrations/000059_app_sharing_level.down.sql @@ -1,5 +1,5 @@ --- Drop column share_level from workspace_apps -ALTER TABLE workspace_apps DROP COLUMN share_level; +-- Drop column sharing_level from workspace_apps +ALTER TABLE workspace_apps DROP COLUMN sharing_level; --- Drop type app_share_level -DROP TYPE app_share_level; +-- Drop type app_sharing_level +DROP TYPE app_sharing_level; diff --git a/dogfood/main.tf b/dogfood/main.tf index 5635223c52227..d5686895ea07e 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.2" } 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/examples/templates/aws-ecs-container/main.tf b/examples/templates/aws-ecs-container/main.tf index e21381ad543e1..fae78d07e0a95 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.2" } } } @@ -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..9f5c05912eced 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.2" } } } @@ -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..eff83dfa968a6 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.2" } } } diff --git a/examples/templates/azure-linux/main.tf b/examples/templates/azure-linux/main.tf index e40fb4f5104a4..4d27f998d6674 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.2" } 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..d7d38e5066333 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.2" } digitalocean = { source = "digitalocean/digitalocean" diff --git a/examples/templates/docker-code-server/main.tf b/examples/templates/docker-code-server/main.tf index 3ac932a1d0d63..26e71c3df5a17 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.2" } 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..1c07a76428ddf 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.2" } 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..04c42b7864486 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.2" } docker = { source = "kreuzwerker/docker" diff --git a/examples/templates/docker/main.tf b/examples/templates/docker/main.tf index 2463eeef9f766..03cfcb885b828 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.2" } 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..c45cc23ae6fc6 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.2" } 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..c519585ce603a 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.2" } 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..4610b75c7287c 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.2" } google = { source = "hashicorp/google" diff --git a/examples/templates/kubernetes/main.tf b/examples/templates/kubernetes/main.tf index a3656df07e41e..5f709a811d371 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.2" } 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/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index 5f079d9effe14..3f247fabaa21f 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v3.6.1 +// protoc v3.21.5 // source: provisionersdk/proto/provisioner.proto package proto From cedc57ddf95e9b9a518f2dd8670ea15ca97814f6 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 12 Oct 2022 20:08:47 +0000 Subject: [PATCH 09/12] fixup! chore: add share to all example templates --- coderd/workspaceapps.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index b954df25bc459..7be37a0c7e5c9 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -405,8 +405,8 @@ func (api *API) checkWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Re // 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) +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 } From f8268e74d03f56d01fc462bf9ea46789aace88bf Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 13 Oct 2022 19:01:32 +0000 Subject: [PATCH 10/12] chore: remove app sharing level 'template' --- coderd/database/dump.sql | 1 - .../000059_app_sharing_level.up.sql | 3 - coderd/database/models.go | 1 - coderd/database/queries.sql.go | 2 +- coderd/database/queries/workspaceapps.sql | 2 +- coderd/provisionerdaemons.go | 2 - coderd/workspaceapps.go | 28 +----- coderd/workspaceapps_test.go | 98 ++++--------------- codersdk/workspaceapps.go | 1 - enterprise/coderd/workspaceagents_test.go | 6 -- provisioner/terraform/resources.go | 2 - provisionersdk/proto/provisioner.pb.go | 58 +++++------ provisionersdk/proto/provisioner.proto | 5 +- site/src/api/typesGenerated.ts | 6 +- 14 files changed, 56 insertions(+), 159 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 75c985e8acd31..f91a56a2fc3dc 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -7,7 +7,6 @@ CREATE TYPE api_key_scope AS ENUM ( CREATE TYPE app_sharing_level AS ENUM ( 'owner', - 'template', 'authenticated', 'public' ); diff --git a/coderd/database/migrations/000059_app_sharing_level.up.sql b/coderd/database/migrations/000059_app_sharing_level.up.sql index de6ce3fceb7de..b339ab9726af8 100644 --- a/coderd/database/migrations/000059_app_sharing_level.up.sql +++ b/coderd/database/migrations/000059_app_sharing_level.up.sql @@ -2,9 +2,6 @@ CREATE TYPE app_sharing_level AS ENUM ( -- only the workspace owner can access the app 'owner', - -- the workspace owner and other users that can read the workspace template - -- can access the app - 'template', -- any authenticated user on the site can access the app 'authenticated', -- any user can access the app even if they are not authenticated diff --git a/coderd/database/models.go b/coderd/database/models.go index ea768bae00e57..bd162290ac1b9 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -38,7 +38,6 @@ type AppSharingLevel string const ( AppSharingLevelOwner AppSharingLevel = "owner" - AppSharingLevelTemplate AppSharingLevel = "template" AppSharingLevelAuthenticated AppSharingLevel = "authenticated" AppSharingLevelPublic AppSharingLevel = "public" ) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index e9386d7ec63be..324feed6b6a44 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -4451,7 +4451,7 @@ INSERT INTO command, url, subdomain, - sharing_level, + sharing_level, healthcheck_url, healthcheck_interval, healthcheck_threshold, diff --git a/coderd/database/queries/workspaceapps.sql b/coderd/database/queries/workspaceapps.sql index 8099f350345fb..36494a8e9aeb2 100644 --- a/coderd/database/queries/workspaceapps.sql +++ b/coderd/database/queries/workspaceapps.sql @@ -21,7 +21,7 @@ INSERT INTO command, url, subdomain, - sharing_level, + sharing_level, healthcheck_url, healthcheck_interval, healthcheck_threshold, diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index cd6ca6f229aaa..1e0b5c57b3fd5 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -816,8 +816,6 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. sharingLevel := database.AppSharingLevelOwner switch app.SharingLevel { - case sdkproto.AppSharingLevel_TEMPLATE: - sharingLevel = database.AppSharingLevelTemplate case sdkproto.AppSharingLevel_AUTHENTICATED: sharingLevel = database.AppSharingLevelAuthenticated case sdkproto.AppSharingLevel_PUBLIC: diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 7be37a0c7e5c9..55f73d2c52ae1 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -310,7 +310,7 @@ func (api *API) authorizeWorkspaceApp(r *http.Request, sharingLevel database.App // 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 key's + // 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 { @@ -319,29 +319,9 @@ func (api *API) authorizeWorkspaceApp(r *http.Request, sharingLevel database.App switch sharingLevel { case database.AppSharingLevelOwner: - // We essentially already did this above. - case database.AppSharingLevelTemplate: - // Check if the user has access to the same template as the workspace. - template, err := api.Database.GetTemplateByID(ctx, workspace.TemplateID) - if err != nil { - return false, xerrors.Errorf("get template %q: %w", workspace.TemplateID, err) - } - - // We have to perform this check without scopes enabled because - // otherwise this check will always fail on a scoped API key. - err = api.Authorizer.ByRoleName(ctx, roles.ID.String(), roles.Roles, rbac.ScopeAll, []string{}, rbac.ActionRead, template.RBACObject()) - if err != nil { - // Exit early if the user doesn't have access to the template. - return false, nil - } - - // Now check if the user has ApplicationConnect access to their own - // workspaces. - 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 - } + // 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 diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go index 4a0965172b460..ed2f536ef2c68 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -32,7 +32,6 @@ const ( proxyTestAgentName = "agent-name" proxyTestAppNameFake = "test-app-fake" proxyTestAppNameOwner = "test-app-owner" - proxyTestAppNameTemplate = "test-app-template" proxyTestAppNameAuthenticated = "test-app-authenticated" proxyTestAppNamePublic = "test-app-public" proxyTestAppQuery = "query=true" @@ -134,11 +133,6 @@ func setupProxyTest(t *testing.T, workspaceMutators ...func(*codersdk.CreateWork SharingLevel: proto.AppSharingLevel_OWNER, Url: appURL, }, - { - Name: proxyTestAppNameTemplate, - SharingLevel: proto.AppSharingLevel_TEMPLATE, - Url: appURL, - }, { Name: proxyTestAppNameAuthenticated, SharingLevel: proto.AppSharingLevel_AUTHENTICATED, @@ -736,11 +730,11 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) { func TestAppSharing(t *testing.T) { t.Parallel() - setup := func(t *testing.T) (workspace codersdk.Workspace, agnt codersdk.WorkspaceAgent, user codersdk.User, client *codersdk.Client, clientWithTemplateAccess *codersdk.Client, clientWithNoTemplateAccess *codersdk.Client, clientWithNoAuth *codersdk.Client) { + 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, firstUser, workspace, _ := setupProxyTest(t) + client, _, workspace, _ = setupProxyTest(t) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) @@ -756,7 +750,6 @@ func TestAppSharing(t *testing.T) { expected := map[string]codersdk.WorkspaceAppSharingLevel{ proxyTestAppNameFake: codersdk.WorkspaceAppSharingLevelOwner, proxyTestAppNameOwner: codersdk.WorkspaceAppSharingLevelOwner, - proxyTestAppNameTemplate: codersdk.WorkspaceAppSharingLevelTemplate, proxyTestAppNameAuthenticated: codersdk.WorkspaceAppSharingLevelAuthenticated, proxyTestAppNamePublic: codersdk.WorkspaceAppSharingLevelPublic, } @@ -765,66 +758,37 @@ func TestAppSharing(t *testing.T) { } require.Equal(t, expected, found, "apps have incorrect sharing levels") - // Create a user in the same org (should be able to read the template). - userWithTemplateAccess, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ - Email: "template-access@coder.com", - Username: "template-access", - Password: password, - OrganizationID: firstUser.OrganizationID, - }) - require.NoError(t, err) - - clientWithTemplateAccess = codersdk.New(client.URL) - loginRes, err := clientWithTemplateAccess.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{ - Email: userWithTemplateAccess.Email, - Password: password, - }) - require.NoError(t, err) - clientWithTemplateAccess.SessionToken = loginRes.SessionToken - clientWithTemplateAccess.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - } - - // Double check that the user can read the template. - _, err = clientWithTemplateAccess.Template(ctx, workspace.TemplateID) - require.NoError(t, err) - - // Create a user in a different org (should not be able to read the - // template). - differentOrg, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + // Create a user in a different org. + otherOrg, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ Name: "a-different-org", }) require.NoError(t, err) - userWithNoTemplateAccess, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + userInOtherOrg, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ Email: "no-template-access@coder.com", Username: "no-template-access", Password: password, - OrganizationID: differentOrg.ID, + OrganizationID: otherOrg.ID, }) require.NoError(t, err) - clientWithNoTemplateAccess = codersdk.New(client.URL) - loginRes, err = clientWithNoTemplateAccess.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{ - Email: userWithNoTemplateAccess.Email, + clientInOtherOrg = codersdk.New(client.URL) + loginRes, err := clientInOtherOrg.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{ + Email: userInOtherOrg.Email, Password: password, }) require.NoError(t, err) - clientWithNoTemplateAccess.SessionToken = loginRes.SessionToken - clientWithNoTemplateAccess.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + clientInOtherOrg.SessionToken = loginRes.SessionToken + clientInOtherOrg.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse } - // Double check that the user cannot read the template. - _, err = clientWithNoTemplateAccess.Template(ctx, workspace.TemplateID) - require.Error(t, err) - // 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, clientWithTemplateAccess, clientWithNoTemplateAccess, clientWithNoAuth + return workspace, agnt, user, client, clientInOtherOrg, clientWithNoAuth } verifyAccess := func(t *testing.T, username, workspaceName, agentName, appName string, client *codersdk.Client, shouldHaveAccess, shouldRedirectToLogin bool) { @@ -884,7 +848,7 @@ func TestAppSharing(t *testing.T) { t.Run("Level", func(t *testing.T) { t.Parallel() - workspace, agent, user, client, clientWithTemplateAccess, clientWithNoTemplateAccess, clientWithNoAuth := setup(t) + workspace, agent, user, client, clientInOtherOrg, clientWithNoAuth := setup(t) t.Run("Owner", func(t *testing.T) { t.Parallel() @@ -892,42 +856,22 @@ func TestAppSharing(t *testing.T) { // Owner should be able to access their own workspace. verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameOwner, client, true, false) - // User with or without template access should not have access to a - // workspace that they do not own. - verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameOwner, clientWithTemplateAccess, false, false) - verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameOwner, clientWithNoTemplateAccess, false, 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("Template", func(t *testing.T) { - t.Parallel() - - // Owner should be able to access their own workspace. - verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameTemplate, client, true, false) - - // User with template access should be able to access the workspace. - verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameTemplate, clientWithTemplateAccess, true, false) - - // User without template access should not have access to a workspace - // that they do not own. - verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameTemplate, clientWithNoTemplateAccess, false, false) - - // Unauthenticated user should not have any access. - verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameTemplate, 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) - // User with or without template access should be able to access the - // workspace. - verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameAuthenticated, clientWithTemplateAccess, true, false) - verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameAuthenticated, clientWithNoTemplateAccess, 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) @@ -939,10 +883,8 @@ func TestAppSharing(t *testing.T) { // Owner should be able to access their own workspace. verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNamePublic, client, true, false) - // User with or without template access should be able to access the - // workspace. - verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNamePublic, clientWithTemplateAccess, true, false) - verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNamePublic, clientWithNoTemplateAccess, 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/workspaceapps.go b/codersdk/workspaceapps.go index bf6dec874ba25..6faf4bd3c3ba2 100644 --- a/codersdk/workspaceapps.go +++ b/codersdk/workspaceapps.go @@ -17,7 +17,6 @@ type WorkspaceAppSharingLevel string const ( WorkspaceAppSharingLevelOwner WorkspaceAppSharingLevel = "owner" - WorkspaceAppSharingLevelTemplate WorkspaceAppSharingLevel = "template" WorkspaceAppSharingLevelAuthenticated WorkspaceAppSharingLevel = "authenticated" WorkspaceAppSharingLevelPublic WorkspaceAppSharingLevel = "public" ) diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go index 645e2ec3dd8d1..9fe3cfeaa3064 100644 --- a/enterprise/coderd/workspaceagents_test.go +++ b/enterprise/coderd/workspaceagents_test.go @@ -23,7 +23,6 @@ import ( // App names for each app sharing level. const ( testAppNameOwner = "test-app-owner" - testAppNameTemplate = "test-app-template" testAppNameAuthenticated = "test-app-authenticated" testAppNamePublic = "test-app-public" ) @@ -88,11 +87,6 @@ func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.Cr SharingLevel: proto.AppSharingLevel_OWNER, Url: fmt.Sprintf("http://localhost:%d", appPort), }, - { - Name: testAppNameTemplate, - SharingLevel: proto.AppSharingLevel_TEMPLATE, - Url: fmt.Sprintf("http://localhost:%d", appPort), - }, { Name: testAppNameAuthenticated, SharingLevel: proto.AppSharingLevel_AUTHENTICATED, diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index 5148f86c0e761..604c99c7fbbb6 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -240,8 +240,6 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res switch strings.ToLower(attrs.Share) { case "owner": sharingLevel = proto.AppSharingLevel_OWNER - case "template": - sharingLevel = proto.AppSharingLevel_TEMPLATE case "authenticated": sharingLevel = proto.AppSharingLevel_AUTHENTICATED case "public": diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index 3f247fabaa21f..0e70c8f919185 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -80,24 +80,21 @@ type AppSharingLevel int32 const ( AppSharingLevel_OWNER AppSharingLevel = 0 - AppSharingLevel_TEMPLATE AppSharingLevel = 1 - AppSharingLevel_AUTHENTICATED AppSharingLevel = 2 - AppSharingLevel_PUBLIC AppSharingLevel = 3 + AppSharingLevel_AUTHENTICATED AppSharingLevel = 1 + AppSharingLevel_PUBLIC AppSharingLevel = 2 ) // Enum value maps for AppSharingLevel. var ( AppSharingLevel_name = map[int32]string{ 0: "OWNER", - 1: "TEMPLATE", - 2: "AUTHENTICATED", - 3: "PUBLIC", + 1: "AUTHENTICATED", + 2: "PUBLIC", } AppSharingLevel_value = map[string]int32{ "OWNER": 0, - "TEMPLATE": 1, - "AUTHENTICATED": 2, - "PUBLIC": 3, + "AUTHENTICATED": 1, + "PUBLIC": 2, } ) @@ -2132,29 +2129,28 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 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, 0x49, 0x0a, 0x0f, 0x41, 0x70, 0x70, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, + 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, 0x0c, 0x0a, 0x08, 0x54, 0x45, 0x4d, 0x50, 0x4c, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, - 0x11, 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, - 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x03, 0x2a, 0x37, - 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, - 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, - 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x02, 0x32, 0xa3, 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, - 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, - 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, - 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x50, 0x0a, 0x09, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x2d, 0x5a, - 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x65, 0x72, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 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, + 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x02, 0x32, 0xa3, 0x01, 0x0a, 0x0b, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x05, 0x50, 0x61, 0x72, + 0x73, 0x65, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, + 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, + 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x50, 0x0a, + 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, + 0x2d, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index 74cddc5dba618..bc6ab711a4add 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -89,9 +89,8 @@ message Agent { enum AppSharingLevel { OWNER = 0; - TEMPLATE = 1; - AUTHENTICATED = 2; - PUBLIC = 3; + AUTHENTICATED = 1; + PUBLIC = 2; } // App represents a dev-accessible application on the workspace. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 0f302c6cce2d1..7a9f4f55bd0e1 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -942,11 +942,7 @@ export type WorkspaceAppHealth = | "unhealthy" // From codersdk/workspaceapps.go -export type WorkspaceAppSharingLevel = - | "authenticated" - | "owner" - | "public" - | "template" +export type WorkspaceAppSharingLevel = "authenticated" | "owner" | "public" // From codersdk/workspacebuilds.go export type WorkspaceStatus = From aae96deff28a8c47d3f21e5530bf0a8fbc4ca9ff Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 13 Oct 2022 19:12:22 +0000 Subject: [PATCH 11/12] chore: upgrade coder tf provider to 0.5.3 --- dogfood/main.tf | 2 +- examples/templates/aws-ecs-container/main.tf | 2 +- examples/templates/aws-linux/main.tf | 2 +- examples/templates/aws-windows/main.tf | 2 +- examples/templates/azure-linux/main.tf | 2 +- examples/templates/do-linux/main.tf | 2 +- examples/templates/docker-code-server/main.tf | 2 +- examples/templates/docker-image-builds/main.tf | 2 +- examples/templates/docker-with-dotfiles/main.tf | 2 +- examples/templates/docker/main.tf | 2 +- examples/templates/gcp-linux/main.tf | 2 +- examples/templates/gcp-vm-container/main.tf | 2 +- examples/templates/gcp-windows/main.tf | 2 +- examples/templates/kubernetes/main.tf | 2 +- provisioner/terraform/testdata/calling-module/calling-module.tf | 2 +- .../terraform/testdata/chaining-resources/chaining-resources.tf | 2 +- .../testdata/conflicting-resources/conflicting-resources.tf | 2 +- provisioner/terraform/testdata/instance-id/instance-id.tf | 2 +- .../terraform/testdata/multiple-agents/multiple-agents.tf | 2 +- provisioner/terraform/testdata/multiple-apps/multiple-apps.tf | 2 +- .../terraform/testdata/resource-metadata/resource-metadata.tf | 2 +- 21 files changed, 21 insertions(+), 21 deletions(-) diff --git a/dogfood/main.tf b/dogfood/main.tf index d5686895ea07e..cc65f6b1c6f42 100644 --- a/dogfood/main.tf +++ b/dogfood/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.2" + version = "0.5.3" } docker = { source = "kreuzwerker/docker" diff --git a/examples/templates/aws-ecs-container/main.tf b/examples/templates/aws-ecs-container/main.tf index fae78d07e0a95..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.2" + version = "0.5.3" } } } diff --git a/examples/templates/aws-linux/main.tf b/examples/templates/aws-linux/main.tf index 9f5c05912eced..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.2" + version = "0.5.3" } } } diff --git a/examples/templates/aws-windows/main.tf b/examples/templates/aws-windows/main.tf index eff83dfa968a6..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.2" + version = "0.5.3" } } } diff --git a/examples/templates/azure-linux/main.tf b/examples/templates/azure-linux/main.tf index 4d27f998d6674..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.2" + version = "0.5.3" } azurerm = { source = "hashicorp/azurerm" diff --git a/examples/templates/do-linux/main.tf b/examples/templates/do-linux/main.tf index d7d38e5066333..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.2" + 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 26e71c3df5a17..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.2" + version = "0.5.3" } docker = { source = "kreuzwerker/docker" diff --git a/examples/templates/docker-image-builds/main.tf b/examples/templates/docker-image-builds/main.tf index 1c07a76428ddf..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.2" + version = "0.5.3" } docker = { source = "kreuzwerker/docker" diff --git a/examples/templates/docker-with-dotfiles/main.tf b/examples/templates/docker-with-dotfiles/main.tf index 04c42b7864486..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.2" + version = "0.5.3" } docker = { source = "kreuzwerker/docker" diff --git a/examples/templates/docker/main.tf b/examples/templates/docker/main.tf index 03cfcb885b828..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.2" + version = "0.5.3" } docker = { source = "kreuzwerker/docker" diff --git a/examples/templates/gcp-linux/main.tf b/examples/templates/gcp-linux/main.tf index c45cc23ae6fc6..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.2" + version = "0.5.3" } google = { source = "hashicorp/google" diff --git a/examples/templates/gcp-vm-container/main.tf b/examples/templates/gcp-vm-container/main.tf index c519585ce603a..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.2" + version = "0.5.3" } google = { source = "hashicorp/google" diff --git a/examples/templates/gcp-windows/main.tf b/examples/templates/gcp-windows/main.tf index 4610b75c7287c..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.2" + version = "0.5.3" } google = { source = "hashicorp/google" diff --git a/examples/templates/kubernetes/main.tf b/examples/templates/kubernetes/main.tf index 5f709a811d371..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.2" + version = "0.5.3" } kubernetes = { source = "hashicorp/kubernetes" 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" } } } From 077cb62da70b8affe8a74a0090c40072dfcfbb42 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Fri, 14 Oct 2022 16:16:52 +0000 Subject: [PATCH 12/12] fixup! Merge branch 'main' into dean/app-sharing --- ...p_sharing_level.down.sql => 000060_app_sharing_level.down.sql} | 0 ...9_app_sharing_level.up.sql => 000060_app_sharing_level.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000059_app_sharing_level.down.sql => 000060_app_sharing_level.down.sql} (100%) rename coderd/database/migrations/{000059_app_sharing_level.up.sql => 000060_app_sharing_level.up.sql} (100%) diff --git a/coderd/database/migrations/000059_app_sharing_level.down.sql b/coderd/database/migrations/000060_app_sharing_level.down.sql similarity index 100% rename from coderd/database/migrations/000059_app_sharing_level.down.sql rename to coderd/database/migrations/000060_app_sharing_level.down.sql diff --git a/coderd/database/migrations/000059_app_sharing_level.up.sql b/coderd/database/migrations/000060_app_sharing_level.up.sql similarity index 100% rename from coderd/database/migrations/000059_app_sharing_level.up.sql rename to coderd/database/migrations/000060_app_sharing_level.up.sql