Skip to content

Commit 997493d

Browse files
authored
feat: add template setting to require active template version (#10277)
1 parent 1ad998e commit 997493d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+802
-70
lines changed

cli/server.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -940,7 +940,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
940940
autobuildTicker := time.NewTicker(vals.AutobuildPollInterval.Value())
941941
defer autobuildTicker.Stop()
942942
autobuildExecutor := autobuild.NewExecutor(
943-
ctx, options.Database, options.Pubsub, coderAPI.TemplateScheduleStore, &coderAPI.Auditor, logger, autobuildTicker.C)
943+
ctx, options.Database, options.Pubsub, coderAPI.TemplateScheduleStore, &coderAPI.Auditor, coderAPI.AccessControlStore, logger, autobuildTicker.C)
944944
autobuildExecutor.Run()
945945

946946
hangDetectorTicker := time.NewTicker(vals.JobHangDetectorInterval.Value())

cli/templates.go

-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import (
66
"github.com/google/uuid"
77
"golang.org/x/xerrors"
88

9-
"github.com/coder/pretty"
10-
119
"github.com/coder/coder/v2/cli/clibase"
1210
"github.com/coder/coder/v2/cli/cliui"
1311
"github.com/coder/coder/v2/codersdk"

cli/templateversionarchive.go

-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import (
88

99
"golang.org/x/xerrors"
1010

11-
"github.com/coder/pretty"
12-
1311
"github.com/coder/coder/v2/cli/clibase"
1412
"github.com/coder/coder/v2/cli/cliui"
1513
"github.com/coder/coder/v2/codersdk"

cli/templateversions.go

-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import (
88
"github.com/google/uuid"
99
"golang.org/x/xerrors"
1010

11-
"github.com/coder/pretty"
12-
1311
"github.com/coder/coder/v2/cli/clibase"
1412
"github.com/coder/coder/v2/cli/cliui"
1513
"github.com/coder/coder/v2/codersdk"

coderd/apidoc/docs.go

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/autobuild/lifecycle_executor.go

+14-2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type Executor struct {
3232
db database.Store
3333
ps pubsub.Pubsub
3434
templateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore]
35+
accessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
3536
auditor *atomic.Pointer[audit.Auditor]
3637
log slog.Logger
3738
tick <-chan time.Time
@@ -46,7 +47,7 @@ type Stats struct {
4647
}
4748

4849
// New returns a new wsactions executor.
49-
func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, tss *atomic.Pointer[schedule.TemplateScheduleStore], auditor *atomic.Pointer[audit.Auditor], log slog.Logger, tick <-chan time.Time) *Executor {
50+
func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, tss *atomic.Pointer[schedule.TemplateScheduleStore], auditor *atomic.Pointer[audit.Auditor], acs *atomic.Pointer[dbauthz.AccessControlStore], log slog.Logger, tick <-chan time.Time) *Executor {
5051
le := &Executor{
5152
//nolint:gocritic // Autostart has a limited set of permissions.
5253
ctx: dbauthz.AsAutostart(ctx),
@@ -56,6 +57,7 @@ func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, tss *
5657
tick: tick,
5758
log: log.Named("autobuild"),
5859
auditor: auditor,
60+
accessControlStore: acs,
5961
}
6062
return le
6163
}
@@ -159,6 +161,12 @@ func (e *Executor) runOnce(t time.Time) Stats {
159161
return nil
160162
}
161163

164+
template, err := tx.GetTemplateByID(e.ctx, ws.TemplateID)
165+
if err != nil {
166+
log.Warn(e.ctx, "get template by id", slog.Error(err))
167+
}
168+
accessControl := (*(e.accessControlStore.Load())).GetTemplateAccessControl(template)
169+
162170
latestJob, err := tx.GetProvisionerJobByID(e.ctx, latestBuild.JobID)
163171
if err != nil {
164172
log.Warn(e.ctx, "get last provisioner job for workspace %q: %w", slog.Error(err))
@@ -179,7 +187,7 @@ func (e *Executor) runOnce(t time.Time) Stats {
179187
Reason(reason)
180188
log.Debug(e.ctx, "auto building workspace", slog.F("transition", nextTransition))
181189
if nextTransition == database.WorkspaceTransitionStart &&
182-
ws.AutomaticUpdates == database.AutomaticUpdatesAlways {
190+
useActiveVersion(accessControl, ws) {
183191
log.Debug(e.ctx, "autostarting with active version")
184192
builder = builder.ActiveVersion()
185193
}
@@ -470,3 +478,7 @@ func auditBuild(ctx context.Context, log slog.Logger, auditor audit.Auditor, par
470478
AdditionalFields: raw,
471479
})
472480
}
481+
482+
func useActiveVersion(opts dbauthz.TemplateAccessControl, ws database.Workspace) bool {
483+
return opts.RequireActiveVersion || ws.AutomaticUpdates == database.AutomaticUpdatesAlways
484+
}

coderd/autobuild/lifecycle_executor_test.go

+50
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,56 @@ func TestExecutorAutostopTemplateDisabled(t *testing.T) {
783783
assert.Len(t, stats.Transitions, 0)
784784
}
785785

786+
// Test that an AGPL AccessControlStore properly disables
787+
// functionality.
788+
func TestExecutorRequireActiveVersion(t *testing.T) {
789+
t.Parallel()
790+
791+
var (
792+
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
793+
ticker = make(chan time.Time)
794+
statCh = make(chan autobuild.Stats)
795+
796+
ownerClient = coderdtest.New(t, &coderdtest.Options{
797+
AutobuildTicker: ticker,
798+
IncludeProvisionerDaemon: true,
799+
AutobuildStats: statCh,
800+
TemplateScheduleStore: schedule.NewAGPLTemplateScheduleStore(),
801+
})
802+
)
803+
owner := coderdtest.CreateFirstUser(t, ownerClient)
804+
805+
// Create an active and inactive template version. We'll
806+
// build a regular member's workspace using a non-active
807+
// template version and assert that the field is not abided
808+
// since there is no enterprise license.
809+
activeVersion := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, nil)
810+
template := coderdtest.CreateTemplate(t, ownerClient, owner.OrganizationID, activeVersion.ID, func(ctr *codersdk.CreateTemplateRequest) {
811+
ctr.RequireActiveVersion = true
812+
ctr.VersionID = activeVersion.ID
813+
})
814+
inactiveVersion := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) {
815+
ctvr.TemplateID = template.ID
816+
})
817+
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, activeVersion.ID)
818+
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
819+
ws := coderdtest.CreateWorkspace(t, memberClient, owner.OrganizationID, uuid.Nil, func(cwr *codersdk.CreateWorkspaceRequest) {
820+
cwr.TemplateVersionID = inactiveVersion.ID
821+
cwr.AutostartSchedule = ptr.Ref(sched.String())
822+
})
823+
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, ownerClient, ws.LatestBuild.ID)
824+
ws = coderdtest.MustTransitionWorkspace(t, memberClient, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop, func(req *codersdk.CreateWorkspaceBuildRequest) {
825+
req.TemplateVersionID = inactiveVersion.ID
826+
})
827+
require.Equal(t, inactiveVersion.ID, ws.LatestBuild.TemplateVersionID)
828+
ticker <- sched.Next(ws.LatestBuild.CreatedAt)
829+
stats := <-statCh
830+
require.Len(t, stats.Transitions, 1)
831+
832+
ws = coderdtest.MustWorkspace(t, memberClient, ws.ID)
833+
require.Equal(t, inactiveVersion.ID, ws.LatestBuild.TemplateVersionID)
834+
}
835+
786836
// TestExecutorFailedWorkspace test AGPL functionality which mainly
787837
// ensures that autostop actions as a result of a failed workspace
788838
// build do not trigger.

coderd/coderd.go

+14
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ type Options struct {
131131
SetUserSiteRoles func(ctx context.Context, logger slog.Logger, tx database.Store, userID uuid.UUID, roles []string) error
132132
TemplateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore]
133133
UserQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore]
134+
AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
134135
// AppSecurityKey is the crypto key used to sign and encrypt tokens related to
135136
// workspace applications. It consists of both a signing and encryption key.
136137
AppSecurityKey workspaceapps.SecurityKey
@@ -208,11 +209,20 @@ func New(options *Options) *API {
208209
if options.Authorizer == nil {
209210
options.Authorizer = rbac.NewCachingAuthorizer(options.PrometheusRegistry)
210211
}
212+
213+
if options.AccessControlStore == nil {
214+
options.AccessControlStore = &atomic.Pointer[dbauthz.AccessControlStore]{}
215+
var tacs dbauthz.AccessControlStore = dbauthz.AGPLTemplateAccessControlStore{}
216+
options.AccessControlStore.Store(&tacs)
217+
}
218+
211219
options.Database = dbauthz.New(
212220
options.Database,
213221
options.Authorizer,
214222
options.Logger.Named("authz_querier"),
223+
options.AccessControlStore,
215224
)
225+
216226
experiments := ReadExperiments(
217227
options.Logger, options.DeploymentValues.Experiments.Value(),
218228
)
@@ -369,6 +379,7 @@ func New(options *Options) *API {
369379
Auditor: atomic.Pointer[audit.Auditor]{},
370380
TemplateScheduleStore: options.TemplateScheduleStore,
371381
UserQuietHoursScheduleStore: options.UserQuietHoursScheduleStore,
382+
AccessControlStore: options.AccessControlStore,
372383
Experiments: experiments,
373384
healthCheckGroup: &singleflight.Group[string, *healthcheck.Report]{},
374385
Acquirer: provisionerdserver.NewAcquirer(
@@ -1008,6 +1019,9 @@ type API struct {
10081019
UserQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore]
10091020
// DERPMapper mutates the DERPMap to include workspace proxies.
10101021
DERPMapper atomic.Pointer[func(derpMap *tailcfg.DERPMap) *tailcfg.DERPMap]
1022+
// AccessControlStore is a pointer to an atomic pointer since it is
1023+
// passed to dbauthz.
1024+
AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
10111025

10121026
HTTPAuth *HTTPAuthorizer
10131027

coderd/coderdtest/coderdtest.go

+15-4
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,6 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
218218

219219
if options.Database == nil {
220220
options.Database, options.Pubsub = dbtestutil.NewDB(t)
221-
options.Database = dbauthz.New(options.Database, options.Authorizer, options.Logger.Leveled(slog.LevelDebug))
222221
}
223222

224223
// Some routes expect a deployment ID, so just make sure one exists.
@@ -260,6 +259,10 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
260259
t.Cleanup(closeBatcher)
261260
}
262261

262+
accessControlStore := &atomic.Pointer[dbauthz.AccessControlStore]{}
263+
var acs dbauthz.AccessControlStore = dbauthz.AGPLTemplateAccessControlStore{}
264+
accessControlStore.Store(&acs)
265+
263266
var templateScheduleStore atomic.Pointer[schedule.TemplateScheduleStore]
264267
if options.TemplateScheduleStore == nil {
265268
options.TemplateScheduleStore = schedule.NewAGPLTemplateScheduleStore()
@@ -279,6 +282,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
279282
options.Pubsub,
280283
&templateScheduleStore,
281284
&auditor,
285+
accessControlStore,
282286
*options.Logger,
283287
options.AutobuildTicker,
284288
).WithStatsChannel(options.AutobuildStats)
@@ -416,6 +420,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
416420
Authorizer: options.Authorizer,
417421
Telemetry: telemetry.NewNoop(),
418422
TemplateScheduleStore: &templateScheduleStore,
423+
AccessControlStore: accessControlStore,
419424
TLSCertificates: options.TLSCertificates,
420425
TrialGenerator: options.TrialGenerator,
421426
TailnetCoordinator: options.Coordinator,
@@ -915,7 +920,7 @@ func CreateWorkspace(t testing.TB, client *codersdk.Client, organization uuid.UU
915920
}
916921

917922
// TransitionWorkspace is a convenience method for transitioning a workspace from one state to another.
918-
func MustTransitionWorkspace(t testing.TB, client *codersdk.Client, workspaceID uuid.UUID, from, to database.WorkspaceTransition) codersdk.Workspace {
923+
func MustTransitionWorkspace(t testing.TB, client *codersdk.Client, workspaceID uuid.UUID, from, to database.WorkspaceTransition, muts ...func(req *codersdk.CreateWorkspaceBuildRequest)) codersdk.Workspace {
919924
t.Helper()
920925
ctx := context.Background()
921926
workspace, err := client.Workspace(ctx, workspaceID)
@@ -925,10 +930,16 @@ func MustTransitionWorkspace(t testing.TB, client *codersdk.Client, workspaceID
925930
template, err := client.Template(ctx, workspace.TemplateID)
926931
require.NoError(t, err, "fetch workspace template")
927932

928-
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
933+
req := codersdk.CreateWorkspaceBuildRequest{
929934
TemplateVersionID: template.ActiveVersionID,
930935
Transition: codersdk.WorkspaceTransition(to),
931-
})
936+
}
937+
938+
for _, mut := range muts {
939+
mut(&req)
940+
}
941+
942+
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, req)
932943
require.NoError(t, err, "unexpected error transitioning workspace to %s", to)
933944

934945
_ = AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package dbauthz
2+
3+
import (
4+
"context"
5+
6+
"github.com/google/uuid"
7+
8+
"github.com/coder/coder/v2/coderd/database"
9+
)
10+
11+
// AccessControlStore fetches access control-related configuration
12+
// that is used when determining whether an actor is authorized
13+
// to interact with an RBAC object.
14+
type AccessControlStore interface {
15+
GetTemplateAccessControl(t database.Template) TemplateAccessControl
16+
SetTemplateAccessControl(ctx context.Context, store database.Store, id uuid.UUID, opts TemplateAccessControl) error
17+
}
18+
19+
type TemplateAccessControl struct {
20+
RequireActiveVersion bool
21+
}
22+
23+
// AGPLTemplateAccessControlStore always returns the defaults for access control
24+
// settings.
25+
type AGPLTemplateAccessControlStore struct{}
26+
27+
var _ AccessControlStore = AGPLTemplateAccessControlStore{}
28+
29+
func (AGPLTemplateAccessControlStore) GetTemplateAccessControl(database.Template) TemplateAccessControl {
30+
return TemplateAccessControl{
31+
RequireActiveVersion: false,
32+
}
33+
}
34+
35+
func (AGPLTemplateAccessControlStore) SetTemplateAccessControl(context.Context, database.Store, uuid.UUID, TemplateAccessControl) error {
36+
return nil
37+
}

0 commit comments

Comments
 (0)