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

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy