diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 28d9a4fafb1e4..c4bcd5c1631af 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -895,6 +895,19 @@ func (q *querier) DeleteWorkspaceAgentPortShare(ctx context.Context, arg databas return q.db.DeleteWorkspaceAgentPortShare(ctx, arg) } +func (q *querier) DeleteWorkspaceAgentPortSharesByTemplate(ctx context.Context, templateID uuid.UUID) error { + template, err := q.db.GetTemplateByID(ctx, templateID) + if err != nil { + return err + } + + if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil { + return err + } + + return q.db.DeleteWorkspaceAgentPortSharesByTemplate(ctx, templateID) +} + func (q *querier) FavoriteWorkspace(ctx context.Context, id uuid.UUID) error { fetch := func(ctx context.Context, id uuid.UUID) (database.Workspace, error) { return q.db.GetWorkspaceByID(ctx, id) @@ -2538,6 +2551,19 @@ func (q *querier) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID return q.db.ListWorkspaceAgentPortShares(ctx, workspaceID) } +func (q *querier) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error { + template, err := q.db.GetTemplateByID(ctx, templateID) + if err != nil { + return err + } + + if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil { + return err + } + + return q.db.ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx, templateID) +} + func (q *querier) RegisterWorkspaceProxy(ctx context.Context, arg database.RegisterWorkspaceProxyParams) (database.WorkspaceProxy, error) { fetch := func(ctx context.Context, arg database.RegisterWorkspaceProxyParams) (database.WorkspaceProxy, error) { return q.db.GetWorkspaceProxyByID(ctx, arg.ID) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 207f4a64a9b78..fc27e60c167c5 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -1643,6 +1643,20 @@ func (s *MethodTestSuite) TestWorkspacePortSharing() { Port: ps.Port, }).Asserts(ws, rbac.ActionUpdate).Returns() })) + s.Run("DeleteWorkspaceAgentPortSharesByTemplate", s.Subtest(func(db database.Store, check *expects) { + u := dbgen.User(s.T(), db, database.User{}) + t := dbgen.Template(s.T(), db, database.Template{}) + ws := dbgen.Workspace(s.T(), db, database.Workspace{OwnerID: u.ID, TemplateID: t.ID}) + _ = dbgen.WorkspaceAgentPortShare(s.T(), db, database.WorkspaceAgentPortShare{WorkspaceID: ws.ID}) + check.Args(t.ID).Asserts(t, rbac.ActionUpdate).Returns() + })) + s.Run("ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate", s.Subtest(func(db database.Store, check *expects) { + u := dbgen.User(s.T(), db, database.User{}) + t := dbgen.Template(s.T(), db, database.Template{}) + ws := dbgen.Workspace(s.T(), db, database.Workspace{OwnerID: u.ID, TemplateID: t.ID}) + _ = dbgen.WorkspaceAgentPortShare(s.T(), db, database.WorkspaceAgentPortShare{WorkspaceID: ws.ID}) + check.Args(t.ID).Asserts(t, rbac.ActionUpdate).Returns() + })) } func (s *MethodTestSuite) TestExtraMethods() { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 5c837130b6a52..59a9eeee23ac5 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1312,6 +1312,30 @@ func (q *FakeQuerier) DeleteWorkspaceAgentPortShare(_ context.Context, arg datab return nil } +func (q *FakeQuerier) DeleteWorkspaceAgentPortSharesByTemplate(_ context.Context, templateID uuid.UUID) error { + err := validateDatabaseType(templateID) + if err != nil { + return err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for _, workspace := range q.workspaces { + if workspace.TemplateID != templateID { + continue + } + for i, share := range q.workspaceAgentPortShares { + if share.WorkspaceID != workspace.ID { + continue + } + q.workspaceAgentPortShares = append(q.workspaceAgentPortShares[:i], q.workspaceAgentPortShares[i+1:]...) + } + } + + return nil +} + func (q *FakeQuerier) FavoriteWorkspace(_ context.Context, arg uuid.UUID) error { err := validateDatabaseType(arg) if err != nil { @@ -6047,6 +6071,33 @@ func (q *FakeQuerier) ListWorkspaceAgentPortShares(_ context.Context, workspaceI return shares, nil } +func (q *FakeQuerier) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(_ context.Context, templateID uuid.UUID) error { + err := validateDatabaseType(templateID) + if err != nil { + return err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for _, workspace := range q.workspaces { + if workspace.TemplateID != templateID { + continue + } + for i, share := range q.workspaceAgentPortShares { + if share.WorkspaceID != workspace.ID { + continue + } + if share.ShareLevel == database.AppSharingLevelPublic { + share.ShareLevel = database.AppSharingLevelAuthenticated + } + q.workspaceAgentPortShares[i] = share + } + } + + return nil +} + func (q *FakeQuerier) RegisterWorkspaceProxy(_ context.Context, arg database.RegisterWorkspaceProxyParams) (database.WorkspaceProxy, error) { q.mutex.Lock() defer q.mutex.Unlock() diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 11d0d275920ac..3fffab3d047fe 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -300,6 +300,13 @@ func (m metricsStore) DeleteWorkspaceAgentPortShare(ctx context.Context, arg dat return r0 } +func (m metricsStore) DeleteWorkspaceAgentPortSharesByTemplate(ctx context.Context, templateID uuid.UUID) error { + start := time.Now() + r0 := m.s.DeleteWorkspaceAgentPortSharesByTemplate(ctx, templateID) + m.queryLatencies.WithLabelValues("DeleteWorkspaceAgentPortSharesByTemplate").Observe(time.Since(start).Seconds()) + return r0 +} + func (m metricsStore) FavoriteWorkspace(ctx context.Context, arg uuid.UUID) error { start := time.Now() r0 := m.s.FavoriteWorkspace(ctx, arg) @@ -1628,6 +1635,13 @@ func (m metricsStore) ListWorkspaceAgentPortShares(ctx context.Context, workspac return r0, r1 } +func (m metricsStore) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error { + start := time.Now() + r0 := m.s.ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx, templateID) + m.queryLatencies.WithLabelValues("ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate").Observe(time.Since(start).Seconds()) + return r0 +} + func (m metricsStore) RegisterWorkspaceProxy(ctx context.Context, arg database.RegisterWorkspaceProxyParams) (database.WorkspaceProxy, error) { start := time.Now() proxy, err := m.s.RegisterWorkspaceProxy(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 1ec23fbc970f6..149b355e8ae0b 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -500,6 +500,20 @@ func (mr *MockStoreMockRecorder) DeleteWorkspaceAgentPortShare(arg0, arg1 any) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWorkspaceAgentPortShare", reflect.TypeOf((*MockStore)(nil).DeleteWorkspaceAgentPortShare), arg0, arg1) } +// DeleteWorkspaceAgentPortSharesByTemplate mocks base method. +func (m *MockStore) DeleteWorkspaceAgentPortSharesByTemplate(arg0 context.Context, arg1 uuid.UUID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteWorkspaceAgentPortSharesByTemplate", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteWorkspaceAgentPortSharesByTemplate indicates an expected call of DeleteWorkspaceAgentPortSharesByTemplate. +func (mr *MockStoreMockRecorder) DeleteWorkspaceAgentPortSharesByTemplate(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWorkspaceAgentPortSharesByTemplate", reflect.TypeOf((*MockStore)(nil).DeleteWorkspaceAgentPortSharesByTemplate), arg0, arg1) +} + // FavoriteWorkspace mocks base method. func (m *MockStore) FavoriteWorkspace(arg0 context.Context, arg1 uuid.UUID) error { m.ctrl.T.Helper() @@ -3441,6 +3455,20 @@ func (mr *MockStoreMockRecorder) Ping(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockStore)(nil).Ping), arg0) } +// ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate mocks base method. +func (m *MockStore) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(arg0 context.Context, arg1 uuid.UUID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate indicates an expected call of ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate. +func (mr *MockStoreMockRecorder) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate", reflect.TypeOf((*MockStore)(nil).ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate), arg0, arg1) +} + // RegisterWorkspaceProxy mocks base method. func (m *MockStore) RegisterWorkspaceProxy(arg0 context.Context, arg1 database.RegisterWorkspaceProxyParams) (database.WorkspaceProxy, error) { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 00353daaef876..cd66228cce829 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -77,6 +77,7 @@ type sqlcQuerier interface { DeleteTailnetPeer(ctx context.Context, arg DeleteTailnetPeerParams) (DeleteTailnetPeerRow, error) DeleteTailnetTunnel(ctx context.Context, arg DeleteTailnetTunnelParams) (DeleteTailnetTunnelRow, error) DeleteWorkspaceAgentPortShare(ctx context.Context, arg DeleteWorkspaceAgentPortShareParams) error + DeleteWorkspaceAgentPortSharesByTemplate(ctx context.Context, templateID uuid.UUID) error FavoriteWorkspace(ctx context.Context, id uuid.UUID) error GetAPIKeyByID(ctx context.Context, id string) (APIKey, error) // there is no unique constraint on empty token names @@ -320,6 +321,7 @@ type sqlcQuerier interface { InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgentPortShare, error) + ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error RegisterWorkspaceProxy(ctx context.Context, arg RegisterWorkspaceProxyParams) (WorkspaceProxy, error) RemoveUserFromAllGroups(ctx context.Context, userID uuid.UUID) error RevokeDBCryptKey(ctx context.Context, activeKeyDigest string) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 5e9577f264f5e..c320db081399e 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -8195,6 +8195,15 @@ func (q *sqlQuerier) DeleteWorkspaceAgentPortShare(ctx context.Context, arg Dele return err } +const deleteWorkspaceAgentPortSharesByTemplate = `-- name: DeleteWorkspaceAgentPortSharesByTemplate :exec +DELETE FROM workspace_agent_port_share WHERE workspace_id IN (SELECT id FROM workspaces WHERE template_id = $1) +` + +func (q *sqlQuerier) DeleteWorkspaceAgentPortSharesByTemplate(ctx context.Context, templateID uuid.UUID) error { + _, err := q.db.ExecContext(ctx, deleteWorkspaceAgentPortSharesByTemplate, templateID) + return err +} + const getWorkspaceAgentPortShare = `-- name: GetWorkspaceAgentPortShare :one SELECT workspace_id, agent_name, port, share_level FROM workspace_agent_port_share WHERE workspace_id = $1 AND agent_name = $2 AND port = $3 ` @@ -8249,6 +8258,15 @@ func (q *sqlQuerier) ListWorkspaceAgentPortShares(ctx context.Context, workspace return items, nil } +const reduceWorkspaceAgentShareLevelToAuthenticatedByTemplate = `-- name: ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate :exec +UPDATE workspace_agent_port_share SET share_level = 'authenticated' WHERE share_level = 'public' AND workspace_id IN (SELECT id FROM workspaces WHERE template_id = $1) +` + +func (q *sqlQuerier) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error { + _, err := q.db.ExecContext(ctx, reduceWorkspaceAgentShareLevelToAuthenticatedByTemplate, templateID) + return err +} + const upsertWorkspaceAgentPortShare = `-- name: UpsertWorkspaceAgentPortShare :one INSERT INTO workspace_agent_port_share (workspace_id, agent_name, port, share_level) VALUES ($1, $2, $3, $4) diff --git a/coderd/database/queries/workspaceagentportshare.sql b/coderd/database/queries/workspaceagentportshare.sql index f19da14688fd6..021089348fdf9 100644 --- a/coderd/database/queries/workspaceagentportshare.sql +++ b/coderd/database/queries/workspaceagentportshare.sql @@ -11,3 +11,9 @@ DELETE FROM workspace_agent_port_share WHERE workspace_id = $1 AND agent_name = INSERT INTO workspace_agent_port_share (workspace_id, agent_name, port, share_level) VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id, agent_name, port) DO UPDATE SET share_level = $4 RETURNING *; + +-- name: ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate :exec +UPDATE workspace_agent_port_share SET share_level = 'authenticated' WHERE share_level = 'public' AND workspace_id IN (SELECT id FROM workspaces WHERE template_id = $1); + +-- name: DeleteWorkspaceAgentPortSharesByTemplate :exec +DELETE FROM workspace_agent_port_share WHERE workspace_id IN (SELECT id FROM workspaces WHERE template_id = $1); diff --git a/coderd/templates.go b/coderd/templates.go index 7b23c910fa3eb..12dfb67d11fb8 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -694,6 +694,21 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { delete(groupACL, template.OrganizationID.String()) } + if template.MaxPortSharingLevel != maxPortShareLevel { + switch maxPortShareLevel { + case database.AppSharingLevelOwner: + err = tx.DeleteWorkspaceAgentPortSharesByTemplate(ctx, template.ID) + if err != nil { + return xerrors.Errorf("delete workspace agent port shares by template: %w", err) + } + case database.AppSharingLevelAuthenticated: + err = tx.ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx, template.ID) + if err != nil { + return xerrors.Errorf("reduce workspace agent share level to authenticated by template: %w", err) + } + } + } + var err error err = tx.UpdateTemplateMetaByID(ctx, database.UpdateTemplateMetaByIDParams{ ID: template.ID, diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go index 260d0603fb504..4e2c91f891079 100644 --- a/enterprise/coderd/templates_test.go +++ b/enterprise/coderd/templates_test.go @@ -22,6 +22,7 @@ import ( "github.com/coder/coder/v2/enterprise/coderd/license" "github.com/coder/coder/v2/enterprise/coderd/schedule" "github.com/coder/coder/v2/provisioner/echo" + "github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/testutil" ) @@ -143,9 +144,12 @@ func TestTemplates(t *testing.T) { t.Run("MaxPortShareLevel", func(t *testing.T) { t.Parallel() + cfg := coderdtest.DeploymentValues(t) + cfg.Experiments = []string{"shared-ports"} owner, user := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ IncludeProvisionerDaemon: true, + DeploymentValues: cfg, }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ @@ -154,9 +158,43 @@ func TestTemplates(t *testing.T) { }, }) client, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.RoleTemplateAdmin()) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Log{ + Log: &proto.Log{ + Level: proto.LogLevel_INFO, + Output: "example", + }, + }, + }, { + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ + Resources: []*proto.Resource{{ + Name: "some", + Type: "example", + Agents: []*proto.Agent{{ + Id: "something", + Auth: &proto.Agent_Token{ + Token: uuid.NewString(), + }, + Name: "test", + }}, + }, { + Name: "another", + Type: "example", + }}, + }, + }, + }}, + }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID) + ws, err := client.Workspace(context.Background(), ws.ID) + require.NoError(t, err) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() @@ -175,6 +213,39 @@ func TestTemplates(t *testing.T) { MaxPortShareLevel: &level, }) require.ErrorContains(t, err, "invalid max port sharing level") + + // Create public port share + _, err = client.UpsertWorkspaceAgentPortShare(ctx, ws.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{ + AgentName: ws.LatestBuild.Resources[0].Agents[0].Name, + Port: 8080, + ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic, + }) + require.NoError(t, err) + + // Reduce max level to authenticated + level = codersdk.WorkspaceAgentPortShareLevelAuthenticated + _, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ + MaxPortShareLevel: &level, + }) + require.NoError(t, err) + + // Ensure previously public port is now authenticated + wpsr, err := client.GetWorkspaceAgentPortShares(ctx, ws.ID) + require.NoError(t, err) + require.Len(t, wpsr.Shares, 1) + assert.Equal(t, codersdk.WorkspaceAgentPortShareLevelAuthenticated, wpsr.Shares[0].ShareLevel) + + // reduce max level to owner + level = codersdk.WorkspaceAgentPortShareLevelOwner + _, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ + MaxPortShareLevel: &level, + }) + require.NoError(t, err) + + // Ensure previously authenticated port is removed + wpsr, err = client.GetWorkspaceAgentPortShares(ctx, ws.ID) + require.NoError(t, err) + require.Empty(t, wpsr.Shares) }) t.Run("BlockDisablingAutoOffWithMaxTTL", func(t *testing.T) {