mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
feat: add template setting to require active template version (#10277)
This commit is contained in:
8
coderd/apidoc/docs.go
generated
8
coderd/apidoc/docs.go
generated
@ -7816,6 +7816,10 @@ const docTemplate = `{
|
||||
"description": "Name is the name of the template.",
|
||||
"type": "string"
|
||||
},
|
||||
"require_active_version": {
|
||||
"description": "RequireActiveVersion mandates that workspaces are built with the active\ntemplate version.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"template_version_id": {
|
||||
"description": "VersionID is an in-progress or completed job to use as an initial version\nof the template.\n\nThis is required on creation to enable a user-flow of validating a\ntemplate works. There is no reason the data-model cannot support empty\ntemplates, but it doesn't make sense for users.",
|
||||
"type": "string",
|
||||
@ -9994,6 +9998,10 @@ const docTemplate = `{
|
||||
"terraform"
|
||||
]
|
||||
},
|
||||
"require_active_version": {
|
||||
"description": "RequireActiveVersion mandates that workspaces are built with the active\ntemplate version.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"time_til_dormant_autodelete_ms": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
8
coderd/apidoc/swagger.json
generated
8
coderd/apidoc/swagger.json
generated
@ -6969,6 +6969,10 @@
|
||||
"description": "Name is the name of the template.",
|
||||
"type": "string"
|
||||
},
|
||||
"require_active_version": {
|
||||
"description": "RequireActiveVersion mandates that workspaces are built with the active\ntemplate version.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"template_version_id": {
|
||||
"description": "VersionID is an in-progress or completed job to use as an initial version\nof the template.\n\nThis is required on creation to enable a user-flow of validating a\ntemplate works. There is no reason the data-model cannot support empty\ntemplates, but it doesn't make sense for users.",
|
||||
"type": "string",
|
||||
@ -9028,6 +9032,10 @@
|
||||
"type": "string",
|
||||
"enum": ["terraform"]
|
||||
},
|
||||
"require_active_version": {
|
||||
"description": "RequireActiveVersion mandates that workspaces are built with the active\ntemplate version.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"time_til_dormant_autodelete_ms": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
@ -32,6 +32,7 @@ type Executor struct {
|
||||
db database.Store
|
||||
ps pubsub.Pubsub
|
||||
templateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore]
|
||||
accessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
|
||||
auditor *atomic.Pointer[audit.Auditor]
|
||||
log slog.Logger
|
||||
tick <-chan time.Time
|
||||
@ -46,7 +47,7 @@ type Stats struct {
|
||||
}
|
||||
|
||||
// New returns a new wsactions executor.
|
||||
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 {
|
||||
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 {
|
||||
le := &Executor{
|
||||
//nolint:gocritic // Autostart has a limited set of permissions.
|
||||
ctx: dbauthz.AsAutostart(ctx),
|
||||
@ -56,6 +57,7 @@ func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, tss *
|
||||
tick: tick,
|
||||
log: log.Named("autobuild"),
|
||||
auditor: auditor,
|
||||
accessControlStore: acs,
|
||||
}
|
||||
return le
|
||||
}
|
||||
@ -159,6 +161,12 @@ func (e *Executor) runOnce(t time.Time) Stats {
|
||||
return nil
|
||||
}
|
||||
|
||||
template, err := tx.GetTemplateByID(e.ctx, ws.TemplateID)
|
||||
if err != nil {
|
||||
log.Warn(e.ctx, "get template by id", slog.Error(err))
|
||||
}
|
||||
accessControl := (*(e.accessControlStore.Load())).GetTemplateAccessControl(template)
|
||||
|
||||
latestJob, err := tx.GetProvisionerJobByID(e.ctx, latestBuild.JobID)
|
||||
if err != nil {
|
||||
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 {
|
||||
Reason(reason)
|
||||
log.Debug(e.ctx, "auto building workspace", slog.F("transition", nextTransition))
|
||||
if nextTransition == database.WorkspaceTransitionStart &&
|
||||
ws.AutomaticUpdates == database.AutomaticUpdatesAlways {
|
||||
useActiveVersion(accessControl, ws) {
|
||||
log.Debug(e.ctx, "autostarting with active version")
|
||||
builder = builder.ActiveVersion()
|
||||
}
|
||||
@ -470,3 +478,7 @@ func auditBuild(ctx context.Context, log slog.Logger, auditor audit.Auditor, par
|
||||
AdditionalFields: raw,
|
||||
})
|
||||
}
|
||||
|
||||
func useActiveVersion(opts dbauthz.TemplateAccessControl, ws database.Workspace) bool {
|
||||
return opts.RequireActiveVersion || ws.AutomaticUpdates == database.AutomaticUpdatesAlways
|
||||
}
|
||||
|
@ -783,6 +783,56 @@ func TestExecutorAutostopTemplateDisabled(t *testing.T) {
|
||||
assert.Len(t, stats.Transitions, 0)
|
||||
}
|
||||
|
||||
// Test that an AGPL AccessControlStore properly disables
|
||||
// functionality.
|
||||
func TestExecutorRequireActiveVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
||||
ticker = make(chan time.Time)
|
||||
statCh = make(chan autobuild.Stats)
|
||||
|
||||
ownerClient = coderdtest.New(t, &coderdtest.Options{
|
||||
AutobuildTicker: ticker,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statCh,
|
||||
TemplateScheduleStore: schedule.NewAGPLTemplateScheduleStore(),
|
||||
})
|
||||
)
|
||||
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||||
|
||||
// Create an active and inactive template version. We'll
|
||||
// build a regular member's workspace using a non-active
|
||||
// template version and assert that the field is not abided
|
||||
// since there is no enterprise license.
|
||||
activeVersion := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, ownerClient, owner.OrganizationID, activeVersion.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.RequireActiveVersion = true
|
||||
ctr.VersionID = activeVersion.ID
|
||||
})
|
||||
inactiveVersion := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) {
|
||||
ctvr.TemplateID = template.ID
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, activeVersion.ID)
|
||||
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||||
ws := coderdtest.CreateWorkspace(t, memberClient, owner.OrganizationID, uuid.Nil, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||
cwr.TemplateVersionID = inactiveVersion.ID
|
||||
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||||
})
|
||||
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, ownerClient, ws.LatestBuild.ID)
|
||||
ws = coderdtest.MustTransitionWorkspace(t, memberClient, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop, func(req *codersdk.CreateWorkspaceBuildRequest) {
|
||||
req.TemplateVersionID = inactiveVersion.ID
|
||||
})
|
||||
require.Equal(t, inactiveVersion.ID, ws.LatestBuild.TemplateVersionID)
|
||||
ticker <- sched.Next(ws.LatestBuild.CreatedAt)
|
||||
stats := <-statCh
|
||||
require.Len(t, stats.Transitions, 1)
|
||||
|
||||
ws = coderdtest.MustWorkspace(t, memberClient, ws.ID)
|
||||
require.Equal(t, inactiveVersion.ID, ws.LatestBuild.TemplateVersionID)
|
||||
}
|
||||
|
||||
// TestExecutorFailedWorkspace test AGPL functionality which mainly
|
||||
// ensures that autostop actions as a result of a failed workspace
|
||||
// build do not trigger.
|
||||
|
@ -131,6 +131,7 @@ type Options struct {
|
||||
SetUserSiteRoles func(ctx context.Context, logger slog.Logger, tx database.Store, userID uuid.UUID, roles []string) error
|
||||
TemplateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore]
|
||||
UserQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore]
|
||||
AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
|
||||
// AppSecurityKey is the crypto key used to sign and encrypt tokens related to
|
||||
// workspace applications. It consists of both a signing and encryption key.
|
||||
AppSecurityKey workspaceapps.SecurityKey
|
||||
@ -208,11 +209,20 @@ func New(options *Options) *API {
|
||||
if options.Authorizer == nil {
|
||||
options.Authorizer = rbac.NewCachingAuthorizer(options.PrometheusRegistry)
|
||||
}
|
||||
|
||||
if options.AccessControlStore == nil {
|
||||
options.AccessControlStore = &atomic.Pointer[dbauthz.AccessControlStore]{}
|
||||
var tacs dbauthz.AccessControlStore = dbauthz.AGPLTemplateAccessControlStore{}
|
||||
options.AccessControlStore.Store(&tacs)
|
||||
}
|
||||
|
||||
options.Database = dbauthz.New(
|
||||
options.Database,
|
||||
options.Authorizer,
|
||||
options.Logger.Named("authz_querier"),
|
||||
options.AccessControlStore,
|
||||
)
|
||||
|
||||
experiments := ReadExperiments(
|
||||
options.Logger, options.DeploymentValues.Experiments.Value(),
|
||||
)
|
||||
@ -369,6 +379,7 @@ func New(options *Options) *API {
|
||||
Auditor: atomic.Pointer[audit.Auditor]{},
|
||||
TemplateScheduleStore: options.TemplateScheduleStore,
|
||||
UserQuietHoursScheduleStore: options.UserQuietHoursScheduleStore,
|
||||
AccessControlStore: options.AccessControlStore,
|
||||
Experiments: experiments,
|
||||
healthCheckGroup: &singleflight.Group[string, *healthcheck.Report]{},
|
||||
Acquirer: provisionerdserver.NewAcquirer(
|
||||
@ -1008,6 +1019,9 @@ type API struct {
|
||||
UserQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore]
|
||||
// DERPMapper mutates the DERPMap to include workspace proxies.
|
||||
DERPMapper atomic.Pointer[func(derpMap *tailcfg.DERPMap) *tailcfg.DERPMap]
|
||||
// AccessControlStore is a pointer to an atomic pointer since it is
|
||||
// passed to dbauthz.
|
||||
AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
|
||||
|
||||
HTTPAuth *HTTPAuthorizer
|
||||
|
||||
|
@ -218,7 +218,6 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
|
||||
|
||||
if options.Database == nil {
|
||||
options.Database, options.Pubsub = dbtestutil.NewDB(t)
|
||||
options.Database = dbauthz.New(options.Database, options.Authorizer, options.Logger.Leveled(slog.LevelDebug))
|
||||
}
|
||||
|
||||
// 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
|
||||
t.Cleanup(closeBatcher)
|
||||
}
|
||||
|
||||
accessControlStore := &atomic.Pointer[dbauthz.AccessControlStore]{}
|
||||
var acs dbauthz.AccessControlStore = dbauthz.AGPLTemplateAccessControlStore{}
|
||||
accessControlStore.Store(&acs)
|
||||
|
||||
var templateScheduleStore atomic.Pointer[schedule.TemplateScheduleStore]
|
||||
if options.TemplateScheduleStore == nil {
|
||||
options.TemplateScheduleStore = schedule.NewAGPLTemplateScheduleStore()
|
||||
@ -279,6 +282,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
|
||||
options.Pubsub,
|
||||
&templateScheduleStore,
|
||||
&auditor,
|
||||
accessControlStore,
|
||||
*options.Logger,
|
||||
options.AutobuildTicker,
|
||||
).WithStatsChannel(options.AutobuildStats)
|
||||
@ -416,6 +420,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
|
||||
Authorizer: options.Authorizer,
|
||||
Telemetry: telemetry.NewNoop(),
|
||||
TemplateScheduleStore: &templateScheduleStore,
|
||||
AccessControlStore: accessControlStore,
|
||||
TLSCertificates: options.TLSCertificates,
|
||||
TrialGenerator: options.TrialGenerator,
|
||||
TailnetCoordinator: options.Coordinator,
|
||||
@ -915,7 +920,7 @@ func CreateWorkspace(t testing.TB, client *codersdk.Client, organization uuid.UU
|
||||
}
|
||||
|
||||
// TransitionWorkspace is a convenience method for transitioning a workspace from one state to another.
|
||||
func MustTransitionWorkspace(t testing.TB, client *codersdk.Client, workspaceID uuid.UUID, from, to database.WorkspaceTransition) codersdk.Workspace {
|
||||
func MustTransitionWorkspace(t testing.TB, client *codersdk.Client, workspaceID uuid.UUID, from, to database.WorkspaceTransition, muts ...func(req *codersdk.CreateWorkspaceBuildRequest)) codersdk.Workspace {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
workspace, err := client.Workspace(ctx, workspaceID)
|
||||
@ -925,10 +930,16 @@ func MustTransitionWorkspace(t testing.TB, client *codersdk.Client, workspaceID
|
||||
template, err := client.Template(ctx, workspace.TemplateID)
|
||||
require.NoError(t, err, "fetch workspace template")
|
||||
|
||||
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
req := codersdk.CreateWorkspaceBuildRequest{
|
||||
TemplateVersionID: template.ActiveVersionID,
|
||||
Transition: codersdk.WorkspaceTransition(to),
|
||||
})
|
||||
}
|
||||
|
||||
for _, mut := range muts {
|
||||
mut(&req)
|
||||
}
|
||||
|
||||
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, req)
|
||||
require.NoError(t, err, "unexpected error transitioning workspace to %s", to)
|
||||
|
||||
_ = AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
|
||||
|
37
coderd/database/dbauthz/accesscontrol.go
Normal file
37
coderd/database/dbauthz/accesscontrol.go
Normal file
@ -0,0 +1,37 @@
|
||||
package dbauthz
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
)
|
||||
|
||||
// AccessControlStore fetches access control-related configuration
|
||||
// that is used when determining whether an actor is authorized
|
||||
// to interact with an RBAC object.
|
||||
type AccessControlStore interface {
|
||||
GetTemplateAccessControl(t database.Template) TemplateAccessControl
|
||||
SetTemplateAccessControl(ctx context.Context, store database.Store, id uuid.UUID, opts TemplateAccessControl) error
|
||||
}
|
||||
|
||||
type TemplateAccessControl struct {
|
||||
RequireActiveVersion bool
|
||||
}
|
||||
|
||||
// AGPLTemplateAccessControlStore always returns the defaults for access control
|
||||
// settings.
|
||||
type AGPLTemplateAccessControlStore struct{}
|
||||
|
||||
var _ AccessControlStore = AGPLTemplateAccessControlStore{}
|
||||
|
||||
func (AGPLTemplateAccessControlStore) GetTemplateAccessControl(database.Template) TemplateAccessControl {
|
||||
return TemplateAccessControl{
|
||||
RequireActiveVersion: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (AGPLTemplateAccessControlStore) SetTemplateAccessControl(context.Context, database.Store, uuid.UUID, TemplateAccessControl) error {
|
||||
return nil
|
||||
}
|
@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@ -101,9 +102,10 @@ type querier struct {
|
||||
db database.Store
|
||||
auth rbac.Authorizer
|
||||
log slog.Logger
|
||||
acs *atomic.Pointer[AccessControlStore]
|
||||
}
|
||||
|
||||
func New(db database.Store, authorizer rbac.Authorizer, logger slog.Logger) database.Store {
|
||||
func New(db database.Store, authorizer rbac.Authorizer, logger slog.Logger, acs *atomic.Pointer[AccessControlStore]) database.Store {
|
||||
// If the underlying db store is already a querier, return it.
|
||||
// Do not double wrap.
|
||||
if slices.Contains(db.Wrappers(), wrapname) {
|
||||
@ -113,6 +115,7 @@ func New(db database.Store, authorizer rbac.Authorizer, logger slog.Logger) data
|
||||
db: db,
|
||||
auth: authorizer,
|
||||
log: logger,
|
||||
acs: acs,
|
||||
}
|
||||
}
|
||||
|
||||
@ -507,7 +510,7 @@ func (q *querier) Ping(ctx context.Context) (time.Duration, error) {
|
||||
func (q *querier) InTx(function func(querier database.Store) error, txOpts *sql.TxOptions) error {
|
||||
return q.db.InTx(func(tx database.Store) error {
|
||||
// Wrap the transaction store in a querier.
|
||||
wrapped := New(tx, q.auth, q.log)
|
||||
wrapped := New(tx, q.auth, q.log, q.acs)
|
||||
return function(wrapped)
|
||||
}, txOpts)
|
||||
}
|
||||
@ -2200,7 +2203,7 @@ func (q *querier) InsertWorkspaceAppStats(ctx context.Context, arg database.Inse
|
||||
func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertWorkspaceBuildParams) error {
|
||||
w, err := q.db.GetWorkspaceByID(ctx, arg.WorkspaceID)
|
||||
if err != nil {
|
||||
return err
|
||||
return xerrors.Errorf("get workspace by id: %w", err)
|
||||
}
|
||||
|
||||
var action rbac.Action = rbac.ActionUpdate
|
||||
@ -2209,7 +2212,28 @@ func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertW
|
||||
}
|
||||
|
||||
if err = q.authorizeContext(ctx, action, w.WorkspaceBuildRBAC(arg.Transition)); err != nil {
|
||||
return err
|
||||
return xerrors.Errorf("authorize context: %w", err)
|
||||
}
|
||||
|
||||
// If we're starting a workspace we need to check the template.
|
||||
if arg.Transition == database.WorkspaceTransitionStart {
|
||||
t, err := q.db.GetTemplateByID(ctx, w.TemplateID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get template by id: %w", err)
|
||||
}
|
||||
|
||||
accessControl := (*q.acs.Load()).GetTemplateAccessControl(t)
|
||||
|
||||
// If the template requires the active version we need to check if
|
||||
// the user is a template admin. If they aren't and are attempting
|
||||
// to use a non-active version then we must fail the request.
|
||||
if accessControl.RequireActiveVersion {
|
||||
if arg.TemplateVersionID != t.ActiveVersionID {
|
||||
if err = q.authorizeContext(ctx, rbac.ActionUpdate, t); err != nil {
|
||||
return xerrors.Errorf("cannot use non-active version: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return q.db.InsertWorkspaceBuild(ctx, arg)
|
||||
@ -2442,6 +2466,13 @@ func (q *querier) UpdateTemplateACLByID(ctx context.Context, arg database.Update
|
||||
return fetchAndExec(q.log, q.auth, rbac.ActionCreate, fetch, q.db.UpdateTemplateACLByID)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateTemplateAccessControlByID(ctx context.Context, arg database.UpdateTemplateAccessControlByIDParams) error {
|
||||
fetch := func(ctx context.Context, arg database.UpdateTemplateAccessControlByIDParams) (database.Template, error) {
|
||||
return q.db.GetTemplateByID(ctx, arg.ID)
|
||||
}
|
||||
return update(q.log, q.auth, fetch, q.db.UpdateTemplateAccessControlByID)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateTemplateActiveVersionByID(ctx context.Context, arg database.UpdateTemplateActiveVersionByIDParams) error {
|
||||
fetch := func(ctx context.Context, arg database.UpdateTemplateActiveVersionByIDParams) (database.Template, error) {
|
||||
return q.db.GetTemplateByID(ctx, arg.ID)
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/util/slice"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestAsNoActor(t *testing.T) {
|
||||
@ -61,7 +62,7 @@ func TestAsNoActor(t *testing.T) {
|
||||
func TestPing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
q := dbauthz.New(dbfake.New(), &coderdtest.RecordingAuthorizer{}, slog.Make())
|
||||
q := dbauthz.New(dbfake.New(), &coderdtest.RecordingAuthorizer{}, slog.Make(), accessControlStorePointer())
|
||||
_, err := q.Ping(context.Background())
|
||||
require.NoError(t, err, "must not error")
|
||||
}
|
||||
@ -73,7 +74,7 @@ func TestInTX(t *testing.T) {
|
||||
db := dbfake.New()
|
||||
q := dbauthz.New(db, &coderdtest.RecordingAuthorizer{
|
||||
Wrapped: &coderdtest.FakeAuthorizer{AlwaysReturn: xerrors.New("custom error")},
|
||||
}, slog.Make())
|
||||
}, slog.Make(), accessControlStorePointer())
|
||||
actor := rbac.Subject{
|
||||
ID: uuid.NewString(),
|
||||
Roles: rbac.RoleNames{rbac.RoleOwner()},
|
||||
@ -109,8 +110,8 @@ func TestNew(t *testing.T) {
|
||||
|
||||
// Double wrap should not cause an actual double wrap. So only 1 rbac call
|
||||
// should be made.
|
||||
az := dbauthz.New(db, rec, slog.Make())
|
||||
az = dbauthz.New(az, rec, slog.Make())
|
||||
az := dbauthz.New(db, rec, slog.Make(), accessControlStorePointer())
|
||||
az = dbauthz.New(az, rec, slog.Make(), accessControlStorePointer())
|
||||
|
||||
w, err := az.GetWorkspaceByID(ctx, exp.ID)
|
||||
require.NoError(t, err, "must not error")
|
||||
@ -127,7 +128,7 @@ func TestDBAuthzRecursive(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := dbauthz.New(dbfake.New(), &coderdtest.RecordingAuthorizer{
|
||||
Wrapped: &coderdtest.FakeAuthorizer{AlwaysReturn: nil},
|
||||
}, slog.Make())
|
||||
}, slog.Make(), accessControlStorePointer())
|
||||
actor := rbac.Subject{
|
||||
ID: uuid.NewString(),
|
||||
Roles: rbac.RoleNames{rbac.RoleOwner()},
|
||||
@ -1213,13 +1214,67 @@ func (s *MethodTestSuite) TestWorkspace() {
|
||||
}).Asserts(rbac.ResourceWorkspace.WithOwner(u.ID.String()).InOrg(o.ID), rbac.ActionCreate)
|
||||
}))
|
||||
s.Run("Start/InsertWorkspaceBuild", s.Subtest(func(db database.Store, check *expects) {
|
||||
w := dbgen.Workspace(s.T(), db, database.Workspace{})
|
||||
t := dbgen.Template(s.T(), db, database.Template{})
|
||||
w := dbgen.Workspace(s.T(), db, database.Workspace{
|
||||
TemplateID: t.ID,
|
||||
})
|
||||
check.Args(database.InsertWorkspaceBuildParams{
|
||||
WorkspaceID: w.ID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
Reason: database.BuildReasonInitiator,
|
||||
}).Asserts(w.WorkspaceBuildRBAC(database.WorkspaceTransitionStart), rbac.ActionUpdate)
|
||||
}))
|
||||
s.Run("Start/RequireActiveVersion/VersionMismatch/InsertWorkspaceBuild", s.Subtest(func(db database.Store, check *expects) {
|
||||
t := dbgen.Template(s.T(), db, database.Template{})
|
||||
ctx := testutil.Context(s.T(), testutil.WaitShort)
|
||||
err := db.UpdateTemplateAccessControlByID(ctx, database.UpdateTemplateAccessControlByIDParams{
|
||||
ID: t.ID,
|
||||
RequireActiveVersion: true,
|
||||
})
|
||||
require.NoError(s.T(), err)
|
||||
v := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
|
||||
TemplateID: uuid.NullUUID{UUID: t.ID},
|
||||
})
|
||||
w := dbgen.Workspace(s.T(), db, database.Workspace{
|
||||
TemplateID: t.ID,
|
||||
})
|
||||
check.Args(database.InsertWorkspaceBuildParams{
|
||||
WorkspaceID: w.ID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
Reason: database.BuildReasonInitiator,
|
||||
TemplateVersionID: v.ID,
|
||||
}).Asserts(
|
||||
w.WorkspaceBuildRBAC(database.WorkspaceTransitionStart), rbac.ActionUpdate,
|
||||
t, rbac.ActionUpdate,
|
||||
)
|
||||
}))
|
||||
s.Run("Start/RequireActiveVersion/VersionsMatch/InsertWorkspaceBuild", s.Subtest(func(db database.Store, check *expects) {
|
||||
v := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{})
|
||||
t := dbgen.Template(s.T(), db, database.Template{
|
||||
ActiveVersionID: v.ID,
|
||||
})
|
||||
|
||||
ctx := testutil.Context(s.T(), testutil.WaitShort)
|
||||
err := db.UpdateTemplateAccessControlByID(ctx, database.UpdateTemplateAccessControlByIDParams{
|
||||
ID: t.ID,
|
||||
RequireActiveVersion: true,
|
||||
})
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
w := dbgen.Workspace(s.T(), db, database.Workspace{
|
||||
TemplateID: t.ID,
|
||||
})
|
||||
// Assert that we do not check for template update permissions
|
||||
// if versions match.
|
||||
check.Args(database.InsertWorkspaceBuildParams{
|
||||
WorkspaceID: w.ID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
Reason: database.BuildReasonInitiator,
|
||||
TemplateVersionID: v.ID,
|
||||
}).Asserts(
|
||||
w.WorkspaceBuildRBAC(database.WorkspaceTransitionStart), rbac.ActionUpdate,
|
||||
)
|
||||
}))
|
||||
s.Run("Delete/InsertWorkspaceBuild", s.Subtest(func(db database.Store, check *expects) {
|
||||
w := dbgen.Workspace(s.T(), db, database.Workspace{})
|
||||
check.Args(database.InsertWorkspaceBuildParams{
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
@ -59,7 +60,7 @@ func (s *MethodTestSuite) SetupSuite() {
|
||||
mockStore := dbmock.NewMockStore(ctrl)
|
||||
// We intentionally set no expectations apart from this.
|
||||
mockStore.EXPECT().Wrappers().Return([]string{}).AnyTimes()
|
||||
az := dbauthz.New(mockStore, nil, slog.Make())
|
||||
az := dbauthz.New(mockStore, nil, slog.Make(), accessControlStorePointer())
|
||||
// Take the underlying type of the interface.
|
||||
azt := reflect.TypeOf(az).Elem()
|
||||
s.methodAccounting = make(map[string]int)
|
||||
@ -110,7 +111,7 @@ func (s *MethodTestSuite) Subtest(testCaseF func(db database.Store, check *expec
|
||||
rec := &coderdtest.RecordingAuthorizer{
|
||||
Wrapped: fakeAuthorizer,
|
||||
}
|
||||
az := dbauthz.New(db, rec, slog.Make())
|
||||
az := dbauthz.New(db, rec, slog.Make(), accessControlStorePointer())
|
||||
actor := rbac.Subject{
|
||||
ID: uuid.NewString(),
|
||||
Roles: rbac.RoleNames{rbac.RoleOwner()},
|
||||
@ -398,3 +399,22 @@ func (emptyPreparedAuthorized) Authorize(_ context.Context, _ rbac.Object) error
|
||||
func (emptyPreparedAuthorized) CompileToSQL(_ context.Context, _ regosql.ConvertConfig) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func accessControlStorePointer() *atomic.Pointer[dbauthz.AccessControlStore] {
|
||||
acs := &atomic.Pointer[dbauthz.AccessControlStore]{}
|
||||
var tacs dbauthz.AccessControlStore = fakeAccessControlStore{}
|
||||
acs.Store(&tacs)
|
||||
return acs
|
||||
}
|
||||
|
||||
type fakeAccessControlStore struct{}
|
||||
|
||||
func (fakeAccessControlStore) GetTemplateAccessControl(t database.Template) dbauthz.TemplateAccessControl {
|
||||
return dbauthz.TemplateAccessControl{
|
||||
RequireActiveVersion: t.RequireActiveVersion,
|
||||
}
|
||||
}
|
||||
|
||||
func (fakeAccessControlStore) SetTemplateAccessControl(context.Context, database.Store, uuid.UUID, dbauthz.TemplateAccessControl) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
@ -5643,6 +5643,25 @@ func (q *FakeQuerier) UpdateTemplateACLByID(_ context.Context, arg database.Upda
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateTemplateAccessControlByID(_ context.Context, arg database.UpdateTemplateAccessControlByIDParams) error {
|
||||
if err := validateDatabaseType(arg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for idx, tpl := range q.templates {
|
||||
if tpl.ID != arg.ID {
|
||||
continue
|
||||
}
|
||||
q.templates[idx].RequireActiveVersion = arg.RequireActiveVersion
|
||||
return nil
|
||||
}
|
||||
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateTemplateActiveVersionByID(_ context.Context, arg database.UpdateTemplateActiveVersionByIDParams) error {
|
||||
if err := validateDatabaseType(arg); err != nil {
|
||||
return err
|
||||
|
@ -1523,6 +1523,13 @@ func (m metricsStore) UpdateTemplateACLByID(ctx context.Context, arg database.Up
|
||||
return err
|
||||
}
|
||||
|
||||
func (m metricsStore) UpdateTemplateAccessControlByID(ctx context.Context, arg database.UpdateTemplateAccessControlByIDParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpdateTemplateAccessControlByID(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateTemplateAccessControlByID").Observe(time.Since(start).Seconds())
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m metricsStore) UpdateTemplateActiveVersionByID(ctx context.Context, arg database.UpdateTemplateActiveVersionByIDParams) error {
|
||||
start := time.Now()
|
||||
err := m.s.UpdateTemplateActiveVersionByID(ctx, arg)
|
||||
|
@ -3213,6 +3213,20 @@ func (mr *MockStoreMockRecorder) UpdateTemplateACLByID(arg0, arg1 interface{}) *
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateACLByID", reflect.TypeOf((*MockStore)(nil).UpdateTemplateACLByID), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateTemplateAccessControlByID mocks base method.
|
||||
func (m *MockStore) UpdateTemplateAccessControlByID(arg0 context.Context, arg1 database.UpdateTemplateAccessControlByIDParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateTemplateAccessControlByID", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateTemplateAccessControlByID indicates an expected call of UpdateTemplateAccessControlByID.
|
||||
func (mr *MockStoreMockRecorder) UpdateTemplateAccessControlByID(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateAccessControlByID", reflect.TypeOf((*MockStore)(nil).UpdateTemplateAccessControlByID), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateTemplateActiveVersionByID mocks base method.
|
||||
func (m *MockStore) UpdateTemplateActiveVersionByID(arg0 context.Context, arg1 database.UpdateTemplateActiveVersionByIDParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
4
coderd/database/dump.sql
generated
4
coderd/database/dump.sql
generated
@ -755,7 +755,8 @@ CREATE TABLE templates (
|
||||
time_til_dormant_autodelete bigint DEFAULT 0 NOT NULL,
|
||||
autostop_requirement_days_of_week smallint DEFAULT 0 NOT NULL,
|
||||
autostop_requirement_weeks bigint DEFAULT 0 NOT NULL,
|
||||
autostart_block_days_of_week smallint DEFAULT 0 NOT NULL
|
||||
autostart_block_days_of_week smallint DEFAULT 0 NOT NULL,
|
||||
require_active_version boolean DEFAULT false NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN templates.default_ttl IS 'The default duration for autostop for workspaces created from this template.';
|
||||
@ -800,6 +801,7 @@ CREATE VIEW template_with_users AS
|
||||
templates.autostop_requirement_days_of_week,
|
||||
templates.autostop_requirement_weeks,
|
||||
templates.autostart_block_days_of_week,
|
||||
templates.require_active_version,
|
||||
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
|
||||
COALESCE(visible_users.username, ''::text) AS created_by_username
|
||||
FROM (public.templates
|
||||
|
@ -0,0 +1,25 @@
|
||||
BEGIN;
|
||||
|
||||
-- Update the template_with_users view;
|
||||
DROP VIEW template_with_users;
|
||||
|
||||
ALTER TABLE templates DROP COLUMN require_active_version;
|
||||
|
||||
-- If you need to update this view, put 'DROP VIEW template_with_users;' before this.
|
||||
CREATE VIEW
|
||||
template_with_users
|
||||
AS
|
||||
SELECT
|
||||
templates.*,
|
||||
coalesce(visible_users.avatar_url, '') AS created_by_avatar_url,
|
||||
coalesce(visible_users.username, '') AS created_by_username
|
||||
FROM
|
||||
templates
|
||||
LEFT JOIN
|
||||
visible_users
|
||||
ON
|
||||
templates.created_by = visible_users.id;
|
||||
|
||||
COMMENT ON VIEW template_with_users IS 'Joins in the username + avatar url of the created by user.';
|
||||
|
||||
COMMIT;
|
@ -0,0 +1,23 @@
|
||||
BEGIN;
|
||||
|
||||
DROP VIEW template_with_users;
|
||||
|
||||
ALTER TABLE templates ADD COLUMN require_active_version boolean NOT NULL DEFAULT 'f';
|
||||
|
||||
CREATE VIEW
|
||||
template_with_users
|
||||
AS
|
||||
SELECT
|
||||
templates.*,
|
||||
coalesce(visible_users.avatar_url, '') AS created_by_avatar_url,
|
||||
coalesce(visible_users.username, '') AS created_by_username
|
||||
FROM
|
||||
templates
|
||||
LEFT JOIN
|
||||
visible_users
|
||||
ON
|
||||
templates.created_by = visible_users.id;
|
||||
|
||||
COMMENT ON VIEW template_with_users IS 'Joins in the username + avatar url of the created by user.';
|
||||
|
||||
COMMIT;
|
@ -179,7 +179,7 @@ func (w Workspace) ApplicationConnectRBAC() rbac.Object {
|
||||
}
|
||||
|
||||
func (w Workspace) WorkspaceBuildRBAC(transition WorkspaceTransition) rbac.Object {
|
||||
// If a workspace is locked it cannot be built.
|
||||
// If a workspace is dormant it cannot be built.
|
||||
// However we need to allow stopping a workspace by a caller once a workspace
|
||||
// is locked (e.g. for autobuild). Additionally, if a user wants to delete
|
||||
// a locked workspace, they shouldn't have to have it unlocked first.
|
||||
|
@ -86,6 +86,7 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate
|
||||
&i.AutostopRequirementDaysOfWeek,
|
||||
&i.AutostopRequirementWeeks,
|
||||
&i.AutostartBlockDaysOfWeek,
|
||||
&i.RequireActiveVersion,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
); err != nil {
|
||||
|
@ -1892,6 +1892,7 @@ type Template struct {
|
||||
AutostopRequirementDaysOfWeek int16 `db:"autostop_requirement_days_of_week" json:"autostop_requirement_days_of_week"`
|
||||
AutostopRequirementWeeks int64 `db:"autostop_requirement_weeks" json:"autostop_requirement_weeks"`
|
||||
AutostartBlockDaysOfWeek int16 `db:"autostart_block_days_of_week" json:"autostart_block_days_of_week"`
|
||||
RequireActiveVersion bool `db:"require_active_version" json:"require_active_version"`
|
||||
CreatedByAvatarURL sql.NullString `db:"created_by_avatar_url" json:"created_by_avatar_url"`
|
||||
CreatedByUsername string `db:"created_by_username" json:"created_by_username"`
|
||||
}
|
||||
@ -1930,6 +1931,7 @@ type TemplateTable struct {
|
||||
AutostopRequirementWeeks int64 `db:"autostop_requirement_weeks" json:"autostop_requirement_weeks"`
|
||||
// A bitmap of days of week that autostart of a workspace is not allowed. Default allows all days. This is intended as a cost savings measure to prevent auto start on weekends (for example).
|
||||
AutostartBlockDaysOfWeek int16 `db:"autostart_block_days_of_week" json:"autostart_block_days_of_week"`
|
||||
RequireActiveVersion bool `db:"require_active_version" json:"require_active_version"`
|
||||
}
|
||||
|
||||
// Joins in the username + avatar url of the created by user.
|
||||
|
@ -301,6 +301,7 @@ type sqlcQuerier interface {
|
||||
UpdateProvisionerJobWithCompleteByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteByIDParams) error
|
||||
UpdateReplica(ctx context.Context, arg UpdateReplicaParams) (Replica, error)
|
||||
UpdateTemplateACLByID(ctx context.Context, arg UpdateTemplateACLByIDParams) error
|
||||
UpdateTemplateAccessControlByID(ctx context.Context, arg UpdateTemplateAccessControlByIDParams) error
|
||||
UpdateTemplateActiveVersionByID(ctx context.Context, arg UpdateTemplateActiveVersionByIDParams) error
|
||||
UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTemplateDeletedByIDParams) error
|
||||
UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error
|
||||
|
@ -4726,7 +4726,7 @@ func (q *sqlQuerier) GetTemplateAverageBuildTime(ctx context.Context, arg GetTem
|
||||
|
||||
const getTemplateByID = `-- name: GetTemplateByID :one
|
||||
SELECT
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, created_by_avatar_url, created_by_username
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, created_by_avatar_url, created_by_username
|
||||
FROM
|
||||
template_with_users
|
||||
WHERE
|
||||
@ -4764,6 +4764,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
|
||||
&i.AutostopRequirementDaysOfWeek,
|
||||
&i.AutostopRequirementWeeks,
|
||||
&i.AutostartBlockDaysOfWeek,
|
||||
&i.RequireActiveVersion,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
)
|
||||
@ -4772,7 +4773,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
|
||||
|
||||
const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one
|
||||
SELECT
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, created_by_avatar_url, created_by_username
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, created_by_avatar_url, created_by_username
|
||||
FROM
|
||||
template_with_users AS templates
|
||||
WHERE
|
||||
@ -4818,6 +4819,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
|
||||
&i.AutostopRequirementDaysOfWeek,
|
||||
&i.AutostopRequirementWeeks,
|
||||
&i.AutostartBlockDaysOfWeek,
|
||||
&i.RequireActiveVersion,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
)
|
||||
@ -4825,7 +4827,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
|
||||
}
|
||||
|
||||
const getTemplates = `-- name: GetTemplates :many
|
||||
SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, created_by_avatar_url, created_by_username FROM template_with_users AS templates
|
||||
SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, created_by_avatar_url, created_by_username FROM template_with_users AS templates
|
||||
ORDER BY (name, id) ASC
|
||||
`
|
||||
|
||||
@ -4864,6 +4866,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
|
||||
&i.AutostopRequirementDaysOfWeek,
|
||||
&i.AutostopRequirementWeeks,
|
||||
&i.AutostartBlockDaysOfWeek,
|
||||
&i.RequireActiveVersion,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
); err != nil {
|
||||
@ -4882,7 +4885,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
|
||||
|
||||
const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many
|
||||
SELECT
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, created_by_avatar_url, created_by_username
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, created_by_avatar_url, created_by_username
|
||||
FROM
|
||||
template_with_users AS templates
|
||||
WHERE
|
||||
@ -4958,6 +4961,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate
|
||||
&i.AutostopRequirementDaysOfWeek,
|
||||
&i.AutostopRequirementWeeks,
|
||||
&i.AutostartBlockDaysOfWeek,
|
||||
&i.RequireActiveVersion,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
); err != nil {
|
||||
@ -5054,6 +5058,25 @@ func (q *sqlQuerier) UpdateTemplateACLByID(ctx context.Context, arg UpdateTempla
|
||||
return err
|
||||
}
|
||||
|
||||
const updateTemplateAccessControlByID = `-- name: UpdateTemplateAccessControlByID :exec
|
||||
UPDATE
|
||||
templates
|
||||
SET
|
||||
require_active_version = $2
|
||||
WHERE
|
||||
id = $1
|
||||
`
|
||||
|
||||
type UpdateTemplateAccessControlByIDParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
RequireActiveVersion bool `db:"require_active_version" json:"require_active_version"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateTemplateAccessControlByID(ctx context.Context, arg UpdateTemplateAccessControlByIDParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateTemplateAccessControlByID, arg.ID, arg.RequireActiveVersion)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateTemplateActiveVersionByID = `-- name: UpdateTemplateActiveVersionByID :exec
|
||||
UPDATE
|
||||
templates
|
||||
|
@ -169,3 +169,12 @@ SELECT
|
||||
coalesce((PERCENTILE_DISC(0.95) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'delete')), -1)::FLOAT AS delete_95
|
||||
FROM build_times
|
||||
;
|
||||
|
||||
-- name: UpdateTemplateAccessControlByID :exec
|
||||
UPDATE
|
||||
templates
|
||||
SET
|
||||
require_active_version = $2
|
||||
WHERE
|
||||
id = $1
|
||||
;
|
||||
|
@ -347,6 +347,15 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
|
||||
return xerrors.Errorf("insert template: %s", err)
|
||||
}
|
||||
|
||||
if createTemplate.RequireActiveVersion {
|
||||
err = (*api.AccessControlStore.Load()).SetTemplateAccessControl(ctx, tx, id, dbauthz.TemplateAccessControl{
|
||||
RequireActiveVersion: createTemplate.RequireActiveVersion,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("set template access control: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
dbTemplate, err = tx.GetTemplateByID(ctx, id)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get template by id: %s", err)
|
||||
@ -614,7 +623,8 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
||||
req.AutostopRequirement.Weeks == scheduleOpts.AutostopRequirement.Weeks &&
|
||||
req.FailureTTLMillis == time.Duration(template.FailureTTL).Milliseconds() &&
|
||||
req.TimeTilDormantMillis == time.Duration(template.TimeTilDormant).Milliseconds() &&
|
||||
req.TimeTilDormantAutoDeleteMillis == time.Duration(template.TimeTilDormantAutoDelete).Milliseconds() {
|
||||
req.TimeTilDormantAutoDeleteMillis == time.Duration(template.TimeTilDormantAutoDelete).Milliseconds() &&
|
||||
req.RequireActiveVersion == template.RequireActiveVersion {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -638,6 +648,15 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
||||
return xerrors.Errorf("update template metadata: %w", err)
|
||||
}
|
||||
|
||||
if template.RequireActiveVersion != req.RequireActiveVersion {
|
||||
err = (*api.AccessControlStore.Load()).SetTemplateAccessControl(ctx, tx, template.ID, dbauthz.TemplateAccessControl{
|
||||
RequireActiveVersion: req.RequireActiveVersion,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("set template access control: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
updated, err = tx.GetTemplateByID(ctx, template.ID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("fetch updated template metadata: %w", err)
|
||||
@ -824,5 +843,6 @@ func (api *API) convertTemplate(
|
||||
AutostartRequirement: codersdk.TemplateAutostartRequirement{
|
||||
DaysOfWeek: codersdk.BitmapToWeekdays(template.AutostartAllowedDays()),
|
||||
},
|
||||
RequireActiveVersion: template.RequireActiveVersion,
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/lib/pq"
|
||||
"github.com/sqlc-dev/pqtype"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
@ -216,28 +215,18 @@ func (b *Builder) Build(
|
||||
// RepeatableRead isolation ensures that we get a consistent view of the database while
|
||||
// computing the new build. This simplifies the logic so that we do not need to worry if
|
||||
// later reads are consistent with earlier ones.
|
||||
for retries := 0; retries < 5; retries++ {
|
||||
var workspaceBuild *database.WorkspaceBuild
|
||||
var provisionerJob *database.ProvisionerJob
|
||||
err := store.InTx(func(store database.Store) error {
|
||||
b.store = store
|
||||
workspaceBuild, provisionerJob, err = b.buildTx(authFunc)
|
||||
return err
|
||||
}, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})
|
||||
var pqe *pq.Error
|
||||
if xerrors.As(err, &pqe) {
|
||||
if pqe.Code == "40001" {
|
||||
// serialization error, retry
|
||||
continue
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
// Other (hard) error
|
||||
return nil, nil, err
|
||||
}
|
||||
return workspaceBuild, provisionerJob, nil
|
||||
var workspaceBuild *database.WorkspaceBuild
|
||||
var provisionerJob *database.ProvisionerJob
|
||||
err = database.ReadModifyUpdate(store, func(tx database.Store) error {
|
||||
var err error
|
||||
b.store = tx
|
||||
workspaceBuild, provisionerJob, err = b.buildTx(authFunc)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("build tx: %w", err)
|
||||
}
|
||||
return nil, nil, xerrors.Errorf("too many errors; last error: %w", err)
|
||||
return workspaceBuild, provisionerJob, nil
|
||||
}
|
||||
|
||||
// buildTx contains the business logic of computing a new build. Attributes of the new database objects are computed
|
||||
@ -360,7 +349,11 @@ func (b *Builder) buildTx(authFunc func(action rbac.Action, object rbac.Objecter
|
||||
MaxDeadline: time.Time{}, // set by provisioner upon completion
|
||||
})
|
||||
if err != nil {
|
||||
return BuildError{http.StatusInternalServerError, "insert workspace build", err}
|
||||
code := http.StatusInternalServerError
|
||||
if rbac.IsUnauthorizedError(err) {
|
||||
code = http.StatusUnauthorized
|
||||
}
|
||||
return BuildError{code, "insert workspace build", err}
|
||||
}
|
||||
|
||||
names, values, err := b.getParameters()
|
||||
|
Reference in New Issue
Block a user