package coderd_test import ( "bytes" "context" "net/http" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/cryptorand" "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" "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/testutil" ) func TestTemplates(t *testing.T) { t.Parallel() // TODO(@dean): remove legacy max_ttl tests t.Run("CreateUpdateWorkspaceMaxTTL", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ IncludeProvisionerDaemon: true, }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureAdvancedTemplateScheduling: 1, }, }, }) anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) exp := 24 * time.Hour.Milliseconds() template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.DefaultTTLMillis = &exp ctr.MaxTTLMillis = &exp }) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() // No TTL provided should use template default req := codersdk.CreateWorkspaceRequest{ TemplateID: template.ID, Name: "testing", } ws, err := anotherClient.CreateWorkspace(ctx, template.OrganizationID, codersdk.Me, req) require.NoError(t, err) require.NotNil(t, ws.TTLMillis) require.EqualValues(t, exp, *ws.TTLMillis) // Editing a workspace to have a higher TTL than the template's max // should error exp = exp + time.Minute.Milliseconds() err = anotherClient.UpdateWorkspaceTTL(ctx, ws.ID, codersdk.UpdateWorkspaceTTLRequest{ TTLMillis: &exp, }) require.Error(t, err) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) require.Len(t, apiErr.Validations, 1) require.Equal(t, apiErr.Validations[0].Field, "ttl_ms") require.Contains(t, apiErr.Validations[0].Detail, "time until shutdown must be less than or equal to the template's maximum TTL") // Creating workspace with TTL higher than max should error req.Name = "testing2" req.TTLMillis = &exp ws, err = anotherClient.CreateWorkspace(ctx, template.OrganizationID, codersdk.Me, req) require.Error(t, err) apiErr = nil require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) require.Len(t, apiErr.Validations, 1) require.Equal(t, apiErr.Validations[0].Field, "ttl_ms") require.Contains(t, apiErr.Validations[0].Detail, "time until shutdown must be less than or equal to the template's maximum TTL") }) t.Run("Deprecated", func(t *testing.T) { t.Parallel() owner, user := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ IncludeProvisionerDaemon: true, }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureAccessControl: 1, }, }, }) client, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.RoleTemplateAdmin()) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ DeprecationMessage: ptr.Ref("Stop using this template"), }) require.NoError(t, err) assert.Greater(t, updated.UpdatedAt, template.UpdatedAt) // AGPL cannot deprecate, expect no change assert.True(t, updated.Deprecated) assert.NotEmpty(t, updated.DeprecationMessage) _, err = client.CreateWorkspace(ctx, user.OrganizationID, codersdk.Me, codersdk.CreateWorkspaceRequest{ TemplateID: template.ID, Name: "foobar", }) require.ErrorContains(t, err, "deprecated") // Unset deprecated and try again updated, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{DeprecationMessage: ptr.Ref("")}) require.NoError(t, err) assert.False(t, updated.Deprecated) assert.Empty(t, updated.DeprecationMessage) _, err = client.CreateWorkspace(ctx, user.OrganizationID, codersdk.Me, codersdk.CreateWorkspaceRequest{ TemplateID: template.ID, Name: "foobar", }) require.NoError(t, err) }) t.Run("MaxPortShareLevel", func(t *testing.T) { t.Parallel() owner, user := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ IncludeProvisionerDaemon: true, }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureControlSharedPorts: 1, }, }, }) client, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.RoleTemplateAdmin()) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() // OK var level codersdk.WorkspaceAgentPortShareLevel = codersdk.WorkspaceAgentPortShareLevelPublic updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ MaxPortShareLevel: &level, }) require.NoError(t, err) assert.Equal(t, level, updated.MaxPortShareLevel) // Invalid level level = "invalid" _, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ MaxPortShareLevel: &level, }) require.ErrorContains(t, err, "invalid max port sharing level") }) t.Run("BlockDisablingAutoOffWithMaxTTL", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ IncludeProvisionerDaemon: true, }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureAdvancedTemplateScheduling: 1, }, }, }) anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) exp := 24 * time.Hour.Milliseconds() template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.MaxTTLMillis = &exp }) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() // No TTL provided should use template default req := codersdk.CreateWorkspaceRequest{ TemplateID: template.ID, Name: "testing", } ws, err := anotherClient.CreateWorkspace(ctx, template.OrganizationID, codersdk.Me, req) require.NoError(t, err) require.NotNil(t, ws.TTLMillis) require.EqualValues(t, exp, *ws.TTLMillis) // Editing a workspace to disable the TTL should do nothing err = anotherClient.UpdateWorkspaceTTL(ctx, ws.ID, codersdk.UpdateWorkspaceTTLRequest{ TTLMillis: nil, }) require.NoError(t, err) ws, err = anotherClient.Workspace(ctx, ws.ID) require.NoError(t, err) require.EqualValues(t, exp, *ws.TTLMillis) // Editing a workspace to have a TTL of 0 should do nothing zero := int64(0) err = anotherClient.UpdateWorkspaceTTL(ctx, ws.ID, codersdk.UpdateWorkspaceTTLRequest{ TTLMillis: &zero, }) require.NoError(t, err) ws, err = anotherClient.Workspace(ctx, ws.ID) require.NoError(t, err) require.EqualValues(t, exp, *ws.TTLMillis) }) t.Run("SetAutostartRequirement", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ IncludeProvisionerDaemon: true, }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureAdvancedTemplateScheduling: 1, }, }, }) anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) require.Equal(t, []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"}, template.AutostartRequirement.DaysOfWeek) ctx := testutil.Context(t, testutil.WaitLong) updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, DisplayName: template.DisplayName, Description: template.Description, Icon: template.Icon, AutostartRequirement: &codersdk.TemplateAutostartRequirement{ DaysOfWeek: []string{"monday", "saturday"}, }, }) require.NoError(t, err) require.Equal(t, []string{"monday", "saturday"}, updated.AutostartRequirement.DaysOfWeek) template, err = anotherClient.Template(ctx, template.ID) require.NoError(t, err) require.Equal(t, []string{"monday", "saturday"}, template.AutostartRequirement.DaysOfWeek) // Ensure a missing field is a noop updated, err = anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, DisplayName: template.DisplayName, Description: template.Description, Icon: template.Icon + "something", }) require.NoError(t, err) require.Equal(t, []string{"monday", "saturday"}, updated.AutostartRequirement.DaysOfWeek) template, err = anotherClient.Template(ctx, template.ID) require.NoError(t, err) require.Equal(t, []string{"monday", "saturday"}, template.AutostartRequirement.DaysOfWeek) require.Empty(t, template.DeprecationMessage) require.False(t, template.Deprecated) }) t.Run("SetInvalidAutostartRequirement", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ IncludeProvisionerDaemon: true, }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureAdvancedTemplateScheduling: 1, }, }, }) anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) require.Equal(t, []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"}, template.AutostartRequirement.DaysOfWeek) ctx := testutil.Context(t, testutil.WaitLong) _, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, DisplayName: template.DisplayName, Description: template.Description, Icon: template.Icon, AutostartRequirement: &codersdk.TemplateAutostartRequirement{ DaysOfWeek: []string{"foobar", "saturday"}, }, }) require.Error(t, err) require.Empty(t, template.DeprecationMessage) require.False(t, template.Deprecated) }) t.Run("SetAutostopRequirement", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ IncludeProvisionerDaemon: true, }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureAdvancedTemplateScheduling: 1, }, }, }) anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) require.Empty(t, 0, template.AutostopRequirement.DaysOfWeek) require.EqualValues(t, 1, template.AutostopRequirement.Weeks) ctx := context.Background() updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, DisplayName: template.DisplayName, Description: template.Description, Icon: template.Icon, AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, DefaultTTLMillis: time.Hour.Milliseconds(), AutostopRequirement: &codersdk.TemplateAutostopRequirement{ DaysOfWeek: []string{"monday", "saturday"}, Weeks: 3, }, }) require.NoError(t, err) require.Equal(t, []string{"monday", "saturday"}, updated.AutostopRequirement.DaysOfWeek) require.EqualValues(t, 3, updated.AutostopRequirement.Weeks) template, err = anotherClient.Template(ctx, template.ID) require.NoError(t, err) require.Equal(t, []string{"monday", "saturday"}, template.AutostopRequirement.DaysOfWeek) require.EqualValues(t, 3, template.AutostopRequirement.Weeks) require.Empty(t, template.DeprecationMessage) require.False(t, template.Deprecated) }) t.Run("CleanupTTLs", func(t *testing.T) { t.Run("OK", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitMedium) client, user := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ IncludeProvisionerDaemon: true, }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureAdvancedTemplateScheduling: 1, }, }, }) anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) require.EqualValues(t, 0, template.TimeTilDormantMillis) require.EqualValues(t, 0, template.FailureTTLMillis) require.EqualValues(t, 0, template.TimeTilDormantAutoDeleteMillis) var ( failureTTL = 1 * time.Minute inactivityTTL = 2 * time.Minute dormantTTL = 3 * time.Minute ) updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, DisplayName: template.DisplayName, Description: template.Description, Icon: template.Icon, AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, TimeTilDormantMillis: inactivityTTL.Milliseconds(), FailureTTLMillis: failureTTL.Milliseconds(), TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(), }) require.NoError(t, err) require.Equal(t, failureTTL.Milliseconds(), updated.FailureTTLMillis) require.Equal(t, inactivityTTL.Milliseconds(), updated.TimeTilDormantMillis) require.Equal(t, dormantTTL.Milliseconds(), updated.TimeTilDormantAutoDeleteMillis) // Validate fetching the template returns the same values as updating // the template. template, err = anotherClient.Template(ctx, template.ID) require.NoError(t, err) require.Equal(t, failureTTL.Milliseconds(), updated.FailureTTLMillis) require.Equal(t, inactivityTTL.Milliseconds(), updated.TimeTilDormantMillis) require.Equal(t, dormantTTL.Milliseconds(), updated.TimeTilDormantAutoDeleteMillis) }) t.Run("BadRequest", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitMedium) client, user := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ IncludeProvisionerDaemon: true, }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureAdvancedTemplateScheduling: 1, }, }, }) anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) type testcase struct { Name string TimeTilDormantMS int64 FailureTTLMS int64 DormantAutoDeleteMS int64 } cases := []testcase{ { Name: "NegativeValue", TimeTilDormantMS: -1, FailureTTLMS: -2, DormantAutoDeleteMS: -3, }, { Name: "ValueTooSmall", TimeTilDormantMS: 1, FailureTTLMS: 999, DormantAutoDeleteMS: 500, }, } for _, c := range cases { c := c // nolint: paralleltest // context is from parent t.Run t.Run(c.Name, func(t *testing.T) { _, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, DisplayName: template.DisplayName, Description: template.Description, Icon: template.Icon, AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, TimeTilDormantMillis: c.TimeTilDormantMS, FailureTTLMillis: c.FailureTTLMS, TimeTilDormantAutoDeleteMillis: c.DormantAutoDeleteMS, }) require.Error(t, err) cerr, ok := codersdk.AsError(err) require.True(t, ok) require.Len(t, cerr.Validations, 3) require.Equal(t, "Value must be at least one minute.", cerr.Validations[0].Detail) }) } }) }) t.Run("UpdateTimeTilDormantAutoDelete", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitMedium) client, user := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ IncludeProvisionerDaemon: true, }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureAdvancedTemplateScheduling: 1, }, }, }) anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) activeWS := coderdtest.CreateWorkspace(t, anotherClient, user.OrganizationID, template.ID) dormantWS := coderdtest.CreateWorkspace(t, anotherClient, user.OrganizationID, template.ID) require.Nil(t, activeWS.DeletingAt) require.Nil(t, dormantWS.DeletingAt) _ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, activeWS.LatestBuild.ID) _ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, dormantWS.LatestBuild.ID) err := anotherClient.UpdateWorkspaceDormancy(ctx, dormantWS.ID, codersdk.UpdateWorkspaceDormancy{ Dormant: true, }) require.NoError(t, err) dormantWS = coderdtest.MustWorkspace(t, client, dormantWS.ID) require.NotNil(t, dormantWS.DormantAt) // The deleting_at field should be nil since there is no template time_til_dormant_autodelete set. require.Nil(t, dormantWS.DeletingAt) dormantTTL := time.Minute updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(), }) require.NoError(t, err) require.Equal(t, dormantTTL.Milliseconds(), updated.TimeTilDormantAutoDeleteMillis) activeWS = coderdtest.MustWorkspace(t, client, activeWS.ID) require.Nil(t, activeWS.DormantAt) require.Nil(t, activeWS.DeletingAt) updatedDormantWorkspace := coderdtest.MustWorkspace(t, client, dormantWS.ID) require.NotNil(t, updatedDormantWorkspace.DormantAt) require.NotNil(t, updatedDormantWorkspace.DeletingAt) require.Equal(t, updatedDormantWorkspace.DormantAt.Add(dormantTTL), *updatedDormantWorkspace.DeletingAt) require.Equal(t, updatedDormantWorkspace.DormantAt, dormantWS.DormantAt) // Disable the time_til_dormant_auto_delete on the template, then we can assert that the workspaces // no longer have a deleting_at field. updated, err = anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ TimeTilDormantAutoDeleteMillis: 0, }) require.NoError(t, err) require.EqualValues(t, 0, updated.TimeTilDormantAutoDeleteMillis) // The active workspace should remain unchanged. activeWS = coderdtest.MustWorkspace(t, client, activeWS.ID) require.Nil(t, activeWS.DormantAt) require.Nil(t, activeWS.DeletingAt) // Fetch the dormant workspace. It should still be dormant, but it should no // longer be scheduled for deletion. dormantWS = coderdtest.MustWorkspace(t, client, dormantWS.ID) require.NotNil(t, dormantWS.DormantAt) require.Nil(t, dormantWS.DeletingAt) }) t.Run("UpdateDormantAt", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitMedium) client, user := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ IncludeProvisionerDaemon: true, }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureAdvancedTemplateScheduling: 1, }, }, }) anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) activeWS := coderdtest.CreateWorkspace(t, anotherClient, user.OrganizationID, template.ID) dormantWS := coderdtest.CreateWorkspace(t, anotherClient, user.OrganizationID, template.ID) require.Nil(t, activeWS.DeletingAt) require.Nil(t, dormantWS.DeletingAt) _ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, activeWS.LatestBuild.ID) _ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, dormantWS.LatestBuild.ID) err := anotherClient.UpdateWorkspaceDormancy(ctx, dormantWS.ID, codersdk.UpdateWorkspaceDormancy{ Dormant: true, }) require.NoError(t, err) dormantWS = coderdtest.MustWorkspace(t, client, dormantWS.ID) require.NotNil(t, dormantWS.DormantAt) // The deleting_at field should be nil since there is no template time_til_dormant_autodelete set. require.Nil(t, dormantWS.DeletingAt) dormantTTL := time.Minute //nolint:gocritic // non-template-admin cannot update template meta updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(), UpdateWorkspaceDormantAt: true, }) require.NoError(t, err) require.Equal(t, dormantTTL.Milliseconds(), updated.TimeTilDormantAutoDeleteMillis) activeWS = coderdtest.MustWorkspace(t, client, activeWS.ID) require.Nil(t, activeWS.DormantAt) require.Nil(t, activeWS.DeletingAt) updatedDormantWorkspace := coderdtest.MustWorkspace(t, client, dormantWS.ID) require.NotNil(t, updatedDormantWorkspace.DormantAt) require.NotNil(t, updatedDormantWorkspace.DeletingAt) // Validate that the workspace dormant_at value is updated. require.True(t, updatedDormantWorkspace.DormantAt.After(*dormantWS.DormantAt)) require.Equal(t, updatedDormantWorkspace.DormantAt.Add(dormantTTL), *updatedDormantWorkspace.DeletingAt) }) t.Run("UpdateLastUsedAt", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitMedium) client, user := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ IncludeProvisionerDaemon: true, }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureAdvancedTemplateScheduling: 1, }, }, }) anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) activeWorkspace := coderdtest.CreateWorkspace(t, anotherClient, user.OrganizationID, template.ID) dormantWorkspace := coderdtest.CreateWorkspace(t, anotherClient, user.OrganizationID, template.ID) require.Nil(t, activeWorkspace.DeletingAt) require.Nil(t, dormantWorkspace.DeletingAt) _ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, activeWorkspace.LatestBuild.ID) _ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, dormantWorkspace.LatestBuild.ID) err := anotherClient.UpdateWorkspaceDormancy(ctx, dormantWorkspace.ID, codersdk.UpdateWorkspaceDormancy{ Dormant: true, }) require.NoError(t, err) dormantWorkspace = coderdtest.MustWorkspace(t, client, dormantWorkspace.ID) require.NotNil(t, dormantWorkspace.DormantAt) // The deleting_at field should be nil since there is no template time_til_dormant_autodelete set. require.Nil(t, dormantWorkspace.DeletingAt) inactivityTTL := time.Minute updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ TimeTilDormantMillis: inactivityTTL.Milliseconds(), UpdateWorkspaceLastUsedAt: true, }) require.NoError(t, err) require.Equal(t, inactivityTTL.Milliseconds(), updated.TimeTilDormantMillis) updatedActiveWS := coderdtest.MustWorkspace(t, client, activeWorkspace.ID) require.Nil(t, updatedActiveWS.DormantAt) require.Nil(t, updatedActiveWS.DeletingAt) require.True(t, updatedActiveWS.LastUsedAt.After(activeWorkspace.LastUsedAt)) updatedDormantWS := coderdtest.MustWorkspace(t, client, dormantWorkspace.ID) require.NotNil(t, updatedDormantWS.DormantAt) require.Nil(t, updatedDormantWS.DeletingAt) // Validate that the workspace dormant_at value is updated. require.Equal(t, updatedDormantWS.DormantAt, dormantWorkspace.DormantAt) require.True(t, updatedDormantWS.LastUsedAt.After(dormantWorkspace.LastUsedAt)) }) t.Run("RequireActiveVersion", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ IncludeProvisionerDaemon: true, TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore()), }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureAccessControl: 1, }, }, }) anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.RequireActiveVersion = true }) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) require.True(t, template.RequireActiveVersion) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() // Update the field and assert it persists. updatedTemplate, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ RequireActiveVersion: false, }) require.NoError(t, err) require.False(t, updatedTemplate.RequireActiveVersion) // Flip it back to ensure we aren't hardcoding to a default value. updatedTemplate, err = anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ RequireActiveVersion: true, }) require.NoError(t, err) require.True(t, updatedTemplate.RequireActiveVersion) // Assert that fetching a template is no different from the response // when updating. template, err = anotherClient.Template(ctx, template.ID) require.NoError(t, err) require.Equal(t, updatedTemplate, template) require.Empty(t, template.DeprecationMessage) require.False(t, template.Deprecated) }) // Create a template, remove the group, see if an owner can // still fetch the template. t.Run("GetOnEveryoneRemove", func(t *testing.T) { t.Parallel() owner, first := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ IncludeProvisionerDaemon: true, TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore()), }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureAccessControl: 1, codersdk.FeatureTemplateRBAC: 1, }, }, }) client, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.RoleTemplateAdmin()) version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) ctx := testutil.Context(t, testutil.WaitMedium) err := client.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{ UserPerms: nil, GroupPerms: map[string]codersdk.TemplateRole{ // OrgID is the everyone ID first.OrganizationID.String(): codersdk.TemplateRoleDeleted, }, }) require.NoError(t, err) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() _, err = owner.Template(ctx, template.ID) require.NoError(t, err) }) } func TestTemplateACL(t *testing.T) { t.Parallel() t.Run("UserRoles", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, }, }}) anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) _, user2 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) _, user3 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) ctx := testutil.Context(t, testutil.WaitLong) err := anotherClient.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{ UserPerms: map[string]codersdk.TemplateRole{ user2.ID.String(): codersdk.TemplateRoleUse, user3.ID.String(): codersdk.TemplateRoleAdmin, }, }) require.NoError(t, err) acl, err := anotherClient.TemplateACL(ctx, template.ID) require.NoError(t, err) templateUser2 := codersdk.TemplateUser{ User: user2, Role: codersdk.TemplateRoleUse, } templateUser3 := codersdk.TemplateUser{ User: user3, Role: codersdk.TemplateRoleAdmin, } require.Len(t, acl.Users, 2) require.Contains(t, acl.Users, templateUser2) require.Contains(t, acl.Users, templateUser3) }) t.Run("everyoneGroup", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, }, }}) // Create a user to assert they aren't returned in the response. anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() acl, err := anotherClient.TemplateACL(ctx, template.ID) require.NoError(t, err) require.Len(t, acl.Groups, 1) require.Len(t, acl.Groups[0].Members, 2) require.Len(t, acl.Users, 0) }) t.Run("NoGroups", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, }, }}) client1, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() //nolint:gocritic // non-template-admin cannot update template acl acl, err := client.TemplateACL(ctx, template.ID) require.NoError(t, err) require.Len(t, acl.Groups, 1) require.Len(t, acl.Users, 0) // User should be able to read template due to allUsers group. _, err = client1.Template(ctx, template.ID) require.NoError(t, err) allUsers := acl.Groups[0] err = client.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{ GroupPerms: map[string]codersdk.TemplateRole{ allUsers.ID.String(): codersdk.TemplateRoleDeleted, }, }) require.NoError(t, err) //nolint:gocritic // non-template-admin cannot update template acl acl, err = client.TemplateACL(ctx, template.ID) require.NoError(t, err) require.Len(t, acl.Groups, 0) require.Len(t, acl.Users, 0) // User should not be able to read template due to allUsers group being deleted. _, err = client1.Template(ctx, template.ID) require.Error(t, err) cerr, ok := codersdk.AsError(err) require.True(t, ok) require.Equal(t, http.StatusNotFound, cerr.StatusCode()) }) t.Run("DisableEveryoneGroupAccess", func(t *testing.T) { t.Parallel() client, admin := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, }, }}) version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() //nolint:gocritic // non-template-admin cannot get template acl acl, err := client.TemplateACL(ctx, template.ID) require.NoError(t, err) require.Equal(t, 1, len(acl.Groups)) _, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, DisplayName: template.DisplayName, Description: template.Description, Icon: template.Icon, AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, DisableEveryoneGroupAccess: true, }) require.NoError(t, err) acl, err = client.TemplateACL(ctx, template.ID) require.NoError(t, err) require.Equal(t, 0, len(acl.Groups), acl.Groups) }) // Test that we do not return deleted users. t.Run("FilterDeletedUsers", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, }, }}) anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin()) _, user1 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) ctx := testutil.Context(t, testutil.WaitLong) err := anotherClient.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{ UserPerms: map[string]codersdk.TemplateRole{ user1.ID.String(): codersdk.TemplateRoleUse, }, }) require.NoError(t, err) acl, err := anotherClient.TemplateACL(ctx, template.ID) require.NoError(t, err) require.Contains(t, acl.Users, codersdk.TemplateUser{ User: user1, Role: codersdk.TemplateRoleUse, }) err = anotherClient.DeleteUser(ctx, user1.ID) require.NoError(t, err) acl, err = anotherClient.TemplateACL(ctx, template.ID) require.NoError(t, err) require.Len(t, acl.Users, 0, "deleted users should be filtered") }) // Test that we do not return suspended users. t.Run("FilterSuspendedUsers", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, }, }}) anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin()) _, user1 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) ctx := testutil.Context(t, testutil.WaitLong) err := anotherClient.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{ UserPerms: map[string]codersdk.TemplateRole{ user1.ID.String(): codersdk.TemplateRoleUse, }, }) require.NoError(t, err) acl, err := anotherClient.TemplateACL(ctx, template.ID) require.NoError(t, err) require.Contains(t, acl.Users, codersdk.TemplateUser{ User: user1, Role: codersdk.TemplateRoleUse, }) _, err = anotherClient.UpdateUserStatus(ctx, user1.ID.String(), codersdk.UserStatusSuspended) require.NoError(t, err) acl, err = anotherClient.TemplateACL(ctx, template.ID) require.NoError(t, err) require.Len(t, acl.Users, 0, "suspended users should be filtered") }) // Test that we do not return deleted groups. t.Run("FilterDeletedGroups", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, }, }}) anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin()) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) ctx := testutil.Context(t, testutil.WaitLong) group, err := anotherClient.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ Name: "test", }) require.NoError(t, err) err = anotherClient.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{ GroupPerms: map[string]codersdk.TemplateRole{ group.ID.String(): codersdk.TemplateRoleUse, }, }) require.NoError(t, err) acl, err := anotherClient.TemplateACL(ctx, template.ID) require.NoError(t, err) // Length should be 2 for test group and the implicit allUsers group. require.Len(t, acl.Groups, 2) require.Contains(t, acl.Groups, codersdk.TemplateGroup{ Group: group, Role: codersdk.TemplateRoleUse, }) err = anotherClient.DeleteGroup(ctx, group.ID) require.NoError(t, err) acl, err = anotherClient.TemplateACL(ctx, template.ID) require.NoError(t, err) // Length should be 1 for the allUsers group. require.Len(t, acl.Groups, 1) require.NotContains(t, acl.Groups, codersdk.TemplateGroup{ Group: group, Role: codersdk.TemplateRoleUse, }) }) t.Run("AdminCanPushVersions", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, }, }}) client1, user1 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) ctx := testutil.Context(t, testutil.WaitLong) //nolint:gocritic // test setup err := client.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{ UserPerms: map[string]codersdk.TemplateRole{ user1.ID.String(): codersdk.TemplateRoleUse, }, }) require.NoError(t, err) data, err := echo.Tar(nil) require.NoError(t, err) file, err := client1.Upload(context.Background(), codersdk.ContentTypeTar, bytes.NewReader(data)) require.NoError(t, err) _, err = client1.CreateTemplateVersion(ctx, user.OrganizationID, codersdk.CreateTemplateVersionRequest{ Name: "testme", TemplateID: template.ID, FileID: file.ID, StorageMethod: codersdk.ProvisionerStorageMethodFile, Provisioner: codersdk.ProvisionerTypeEcho, }) require.Error(t, err) //nolint:gocritic // test setup err = client.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{ UserPerms: map[string]codersdk.TemplateRole{ user1.ID.String(): codersdk.TemplateRoleAdmin, }, }) require.NoError(t, err) _, err = client1.CreateTemplateVersion(ctx, user.OrganizationID, codersdk.CreateTemplateVersionRequest{ Name: "testme", TemplateID: template.ID, FileID: file.ID, StorageMethod: codersdk.ProvisionerStorageMethodFile, Provisioner: codersdk.ProvisionerTypeEcho, }) require.NoError(t, err) }) } func TestUpdateTemplateACL(t *testing.T) { t.Parallel() t.Run("UserPerms", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, }, }}) anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) _, user2 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) _, user3 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() err := anotherClient.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{ UserPerms: map[string]codersdk.TemplateRole{ user2.ID.String(): codersdk.TemplateRoleUse, user3.ID.String(): codersdk.TemplateRoleAdmin, }, }) require.NoError(t, err) acl, err := anotherClient.TemplateACL(ctx, template.ID) require.NoError(t, err) templateUser2 := codersdk.TemplateUser{ User: user2, Role: codersdk.TemplateRoleUse, } templateUser3 := codersdk.TemplateUser{ User: user3, Role: codersdk.TemplateRoleAdmin, } require.Len(t, acl.Users, 2) require.Contains(t, acl.Users, templateUser2) require.Contains(t, acl.Users, templateUser3) }) t.Run("Audit", func(t *testing.T) { t.Parallel() auditor := audit.NewMock() client, user := coderdenttest.New(t, &coderdenttest.Options{ AuditLogging: true, Options: &coderdtest.Options{ IncludeProvisionerDaemon: true, Auditor: auditor, }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, codersdk.FeatureAuditLog: 1, }, }, }) anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) ctx := testutil.Context(t, testutil.WaitLong) numLogs := len(auditor.AuditLogs()) req := codersdk.UpdateTemplateACL{ GroupPerms: map[string]codersdk.TemplateRole{ user.OrganizationID.String(): codersdk.TemplateRoleDeleted, }, } err := anotherClient.UpdateTemplateACL(ctx, template.ID, req) require.NoError(t, err) numLogs++ require.Len(t, auditor.AuditLogs(), numLogs) require.True(t, auditor.Contains(t, database.AuditLog{ Action: database.AuditActionWrite, ResourceID: template.ID, })) }) t.Run("DeleteUser", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, }, }}) anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) _, user2 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) _, user3 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) req := codersdk.UpdateTemplateACL{ UserPerms: map[string]codersdk.TemplateRole{ user2.ID.String(): codersdk.TemplateRoleUse, user3.ID.String(): codersdk.TemplateRoleAdmin, }, } ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() err := anotherClient.UpdateTemplateACL(ctx, template.ID, req) require.NoError(t, err) acl, err := anotherClient.TemplateACL(ctx, template.ID) require.NoError(t, err) require.Contains(t, acl.Users, codersdk.TemplateUser{ User: user2, Role: codersdk.TemplateRoleUse, }) require.Contains(t, acl.Users, codersdk.TemplateUser{ User: user3, Role: codersdk.TemplateRoleAdmin, }) req = codersdk.UpdateTemplateACL{ UserPerms: map[string]codersdk.TemplateRole{ user2.ID.String(): codersdk.TemplateRoleAdmin, user3.ID.String(): codersdk.TemplateRoleDeleted, }, } err = anotherClient.UpdateTemplateACL(ctx, template.ID, req) require.NoError(t, err) acl, err = anotherClient.TemplateACL(ctx, template.ID) require.NoError(t, err) require.Contains(t, acl.Users, codersdk.TemplateUser{ User: user2, Role: codersdk.TemplateRoleAdmin, }) require.NotContains(t, acl.Users, codersdk.TemplateUser{ User: user3, Role: codersdk.TemplateRoleAdmin, }) }) t.Run("InvalidUUID", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, }, }}) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) req := codersdk.UpdateTemplateACL{ UserPerms: map[string]codersdk.TemplateRole{ "hi": "admin", }, } ctx := testutil.Context(t, testutil.WaitLong) //nolint:gocritic // we're testing invalid UUID so testing RBAC is not relevant here. err := client.UpdateTemplateACL(ctx, template.ID, req) require.Error(t, err) cerr, _ := codersdk.AsError(err) require.Equal(t, http.StatusBadRequest, cerr.StatusCode()) }) t.Run("InvalidUser", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, }, }}) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) req := codersdk.UpdateTemplateACL{ UserPerms: map[string]codersdk.TemplateRole{ uuid.NewString(): "admin", }, } ctx := testutil.Context(t, testutil.WaitLong) //nolint:gocritic // we're testing invalid user so testing RBAC is not relevant here. err := client.UpdateTemplateACL(ctx, template.ID, req) require.Error(t, err) cerr, _ := codersdk.AsError(err) require.Equal(t, http.StatusBadRequest, cerr.StatusCode()) }) t.Run("InvalidRole", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, }, }}) _, user2 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) req := codersdk.UpdateTemplateACL{ UserPerms: map[string]codersdk.TemplateRole{ user2.ID.String(): "updater", }, } ctx := testutil.Context(t, testutil.WaitLong) //nolint:gocritic // we're testing invalid role so testing RBAC is not relevant here. err := client.UpdateTemplateACL(ctx, template.ID, req) require.Error(t, err) cerr, _ := codersdk.AsError(err) require.Equal(t, http.StatusBadRequest, cerr.StatusCode()) }) t.Run("RegularUserCannotUpdatePerms", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, }, }}) client1, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) client2, user2 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) req := codersdk.UpdateTemplateACL{ UserPerms: map[string]codersdk.TemplateRole{ user2.ID.String(): codersdk.TemplateRoleUse, }, } ctx := testutil.Context(t, testutil.WaitLong) err := client1.UpdateTemplateACL(ctx, template.ID, req) require.NoError(t, err) req = codersdk.UpdateTemplateACL{ UserPerms: map[string]codersdk.TemplateRole{ user2.ID.String(): codersdk.TemplateRoleAdmin, }, } err = client2.UpdateTemplateACL(ctx, template.ID, req) require.Error(t, err) cerr, _ := codersdk.AsError(err) require.Equal(t, http.StatusInternalServerError, cerr.StatusCode()) }) t.Run("RegularUserWithAdminCanUpdate", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, }, }}) client1, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) client2, user2 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) _, user3 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) req := codersdk.UpdateTemplateACL{ UserPerms: map[string]codersdk.TemplateRole{ user2.ID.String(): codersdk.TemplateRoleAdmin, }, } ctx := testutil.Context(t, testutil.WaitLong) err := client1.UpdateTemplateACL(ctx, template.ID, req) require.NoError(t, err) // Should be able to see user 3 available, err := client2.TemplateACLAvailable(ctx, template.ID) require.NoError(t, err) userFound := false for _, avail := range available.Users { if avail.ID == user3.ID { userFound = true } } require.True(t, userFound, "user not found in acl available") req = codersdk.UpdateTemplateACL{ UserPerms: map[string]codersdk.TemplateRole{ user3.ID.String(): codersdk.TemplateRoleUse, }, } err = client2.UpdateTemplateACL(ctx, template.ID, req) require.NoError(t, err) acl, err := client2.TemplateACL(ctx, template.ID) require.NoError(t, err) found := false for _, u := range acl.Users { if u.ID == user3.ID { found = true } } require.True(t, found, "user not found in acl") }) t.Run("allUsersGroup", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, }, }}) anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() acl, err := anotherClient.TemplateACL(ctx, template.ID) require.NoError(t, err) require.Len(t, acl.Groups, 1) require.Len(t, acl.Users, 0) }) t.Run("CustomGroupHasAccess", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, }, }}) anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin()) client1, user1 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) ctx := testutil.Context(t, testutil.WaitLong) // Create a group to add to the template. group, err := anotherClient.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ Name: "test", }) require.NoError(t, err) // Check that the only current group is the allUsers group. acl, err := anotherClient.TemplateACL(ctx, template.ID) require.NoError(t, err) require.Len(t, acl.Groups, 1) // Update the template to only allow access to the 'test' group. err = anotherClient.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{ GroupPerms: map[string]codersdk.TemplateRole{ // The allUsers group shares the same ID as the organization. user.OrganizationID.String(): codersdk.TemplateRoleDeleted, group.ID.String(): codersdk.TemplateRoleUse, }, }) require.NoError(t, err) // Get the ACL list for the template and assert the test group is // present. acl, err = anotherClient.TemplateACL(ctx, template.ID) require.NoError(t, err) require.Len(t, acl.Groups, 1) require.Len(t, acl.Users, 0) require.Equal(t, group.ID, acl.Groups[0].ID) // Try to get the template as the regular user. This should // fail since we haven't been added to the template yet. _, err = client1.Template(ctx, template.ID) require.Error(t, err) cerr, ok := codersdk.AsError(err) require.True(t, ok) require.Equal(t, http.StatusNotFound, cerr.StatusCode()) // Patch the group to add the regular user. group, err = anotherClient.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{ AddUsers: []string{user1.ID.String()}, }) require.NoError(t, err) require.Len(t, group.Members, 1) require.Equal(t, user1.ID, group.Members[0].ID) // Fetching the template should succeed since our group has view access. _, err = client1.Template(ctx, template.ID) require.NoError(t, err) }) t.Run("NoAccess", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, }, }}) anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) client1, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() acl, err := anotherClient.TemplateACL(ctx, template.ID) require.NoError(t, err) require.Len(t, acl.Groups, 1) require.Len(t, acl.Users, 0) // User should be able to read template due to allUsers group. _, err = client1.Template(ctx, template.ID) require.NoError(t, err) allUsers := acl.Groups[0] err = anotherClient.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{ GroupPerms: map[string]codersdk.TemplateRole{ allUsers.ID.String(): codersdk.TemplateRoleDeleted, }, }) require.NoError(t, err) acl, err = anotherClient.TemplateACL(ctx, template.ID) require.NoError(t, err) require.Len(t, acl.Groups, 0) require.Len(t, acl.Users, 0) // User should not be able to read template due to allUsers group being deleted. _, err = client1.Template(ctx, template.ID) require.Error(t, err) cerr, ok := codersdk.AsError(err) require.True(t, ok) require.Equal(t, http.StatusNotFound, cerr.StatusCode()) }) } func TestReadFileWithTemplateUpdate(t *testing.T) { t.Parallel() t.Run("HasTemplateUpdate", func(t *testing.T) { t.Parallel() // Upload a file client, first := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, }, }}) ctx := testutil.Context(t, testutil.WaitLong) //nolint:gocritic // regular user cannot create file resp, err := client.Upload(ctx, codersdk.ContentTypeTar, bytes.NewReader(make([]byte, 1024))) require.NoError(t, err) // Make a new user member, memberData := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) // Try to download file, this should fail _, _, err = member.Download(ctx, resp.ID) require.Error(t, err, "no template yet") // Make a new template version with the file version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil, func(request *codersdk.CreateTemplateVersionRequest) { request.FileID = resp.ID }) template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) // Not in acl yet _, _, err = member.Download(ctx, resp.ID) require.Error(t, err, "not in acl yet") //nolint:gocritic // regular user cannot update template acl err = client.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{ UserPerms: map[string]codersdk.TemplateRole{ memberData.ID.String(): codersdk.TemplateRoleAdmin, }, }) require.NoError(t, err) _, _, err = member.Download(ctx, resp.ID) require.NoError(t, err) }) } // TestTemplateAccess tests the rego -> sql conversion. We need to implement // this test on at least 1 table type to ensure that the conversion is correct. // The rbac tests only assert against static SQL queries. // This is a full rbac test of many of the common role combinations. // //nolint:tparallel func TestTemplateAccess(t *testing.T) { t.Parallel() // TODO: This context is for all the subtests. Each subtest should have its // own context. ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong*3) t.Cleanup(cancel) ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, }, }}) type coderUser struct { *codersdk.Client User codersdk.User } type orgSetup struct { Admin coderUser MemberInGroup coderUser MemberNoGroup coderUser DefaultTemplate codersdk.Template AllRead codersdk.Template UserACL codersdk.Template GroupACL codersdk.Template Group codersdk.Group Org codersdk.Organization } // Create the following users // - owner: Site wide owner // - template-admin // - org-admin (org 1) // - org-admin (org 2) // - org-member (org 1) // - org-member (org 2) // Create the following templates in each org // - template 1, default acls // - template 2, all_user read // - template 3, user_acl read for member // - template 4, group_acl read for groupMember templateAdmin, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin()) makeTemplate := func(t *testing.T, client *codersdk.Client, orgID uuid.UUID, acl codersdk.UpdateTemplateACL) codersdk.Template { version := coderdtest.CreateTemplateVersion(t, client, orgID, nil) template := coderdtest.CreateTemplate(t, client, orgID, version.ID) err := client.UpdateTemplateACL(ctx, template.ID, acl) require.NoError(t, err, "failed to update template acl") return template } makeOrg := func(t *testing.T) orgSetup { // Make org orgName, err := cryptorand.String(5) require.NoError(t, err, "org name") // Make users newOrg, err := ownerClient.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{Name: orgName}) require.NoError(t, err, "failed to create org") adminCli, adminUsr := coderdtest.CreateAnotherUser(t, ownerClient, newOrg.ID, rbac.RoleOrgAdmin(newOrg.ID)) groupMemCli, groupMemUsr := coderdtest.CreateAnotherUser(t, ownerClient, newOrg.ID, rbac.RoleOrgMember(newOrg.ID)) memberCli, memberUsr := coderdtest.CreateAnotherUser(t, ownerClient, newOrg.ID, rbac.RoleOrgMember(newOrg.ID)) // Make group group, err := adminCli.CreateGroup(ctx, newOrg.ID, codersdk.CreateGroupRequest{ Name: "SingleUser", }) require.NoError(t, err, "failed to create group") group, err = adminCli.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{ AddUsers: []string{groupMemUsr.ID.String()}, }) require.NoError(t, err, "failed to add user to group") // Make templates return orgSetup{ Admin: coderUser{Client: adminCli, User: adminUsr}, MemberInGroup: coderUser{Client: groupMemCli, User: groupMemUsr}, MemberNoGroup: coderUser{Client: memberCli, User: memberUsr}, Org: newOrg, Group: group, DefaultTemplate: makeTemplate(t, adminCli, newOrg.ID, codersdk.UpdateTemplateACL{ GroupPerms: map[string]codersdk.TemplateRole{ newOrg.ID.String(): codersdk.TemplateRoleDeleted, }, }), AllRead: makeTemplate(t, adminCli, newOrg.ID, codersdk.UpdateTemplateACL{ GroupPerms: map[string]codersdk.TemplateRole{ newOrg.ID.String(): codersdk.TemplateRoleUse, }, }), UserACL: makeTemplate(t, adminCli, newOrg.ID, codersdk.UpdateTemplateACL{ GroupPerms: map[string]codersdk.TemplateRole{ newOrg.ID.String(): codersdk.TemplateRoleDeleted, }, UserPerms: map[string]codersdk.TemplateRole{ memberUsr.ID.String(): codersdk.TemplateRoleUse, }, }), GroupACL: makeTemplate(t, adminCli, newOrg.ID, codersdk.UpdateTemplateACL{ GroupPerms: map[string]codersdk.TemplateRole{ group.ID.String(): codersdk.TemplateRoleUse, newOrg.ID.String(): codersdk.TemplateRoleDeleted, }, }), } } // Make 2 organizations orgs := []orgSetup{ makeOrg(t), makeOrg(t), } testTemplateRead := func(t *testing.T, org orgSetup, usr *codersdk.Client, read []codersdk.Template) { found, err := usr.TemplatesByOrganization(ctx, org.Org.ID) if len(read) == 0 && err != nil { require.ErrorContains(t, err, "Resource not found") return } require.NoError(t, err, "failed to get templates") exp := make(map[uuid.UUID]codersdk.Template) for _, tmpl := range read { exp[tmpl.ID] = tmpl } for _, f := range found { if _, ok := exp[f.ID]; !ok { t.Errorf("found unexpected template %q", f.Name) } delete(exp, f.ID) } require.Len(t, exp, 0, "expected templates not found") } // nolint:paralleltest t.Run("OwnerReadAll", func(t *testing.T) { for _, o := range orgs { // Owners can read all templates in all orgs exp := []codersdk.Template{o.DefaultTemplate, o.AllRead, o.UserACL, o.GroupACL} testTemplateRead(t, o, ownerClient, exp) } }) // nolint:paralleltest t.Run("TemplateAdminReadAll", func(t *testing.T) { for _, o := range orgs { // Template Admins can read all templates in all orgs exp := []codersdk.Template{o.DefaultTemplate, o.AllRead, o.UserACL, o.GroupACL} testTemplateRead(t, o, templateAdmin, exp) } }) // nolint:paralleltest t.Run("OrgAdminReadAllTheirs", func(t *testing.T) { for i, o := range orgs { cli := o.Admin.Client // Only read their own org exp := []codersdk.Template{o.DefaultTemplate, o.AllRead, o.UserACL, o.GroupACL} testTemplateRead(t, o, cli, exp) other := orgs[(i+1)%len(orgs)] require.NotEqual(t, other.Org.ID, o.Org.ID, "this test needs at least 2 orgs") testTemplateRead(t, other, cli, []codersdk.Template{}) } }) // nolint:paralleltest t.Run("TestMemberNoGroup", func(t *testing.T) { for i, o := range orgs { cli := o.MemberNoGroup.Client // Only read their own org exp := []codersdk.Template{o.AllRead, o.UserACL} testTemplateRead(t, o, cli, exp) other := orgs[(i+1)%len(orgs)] require.NotEqual(t, other.Org.ID, o.Org.ID, "this test needs at least 2 orgs") testTemplateRead(t, other, cli, []codersdk.Template{}) } }) // nolint:paralleltest t.Run("TestMemberInGroup", func(t *testing.T) { for i, o := range orgs { cli := o.MemberInGroup.Client // Only read their own org exp := []codersdk.Template{o.AllRead, o.GroupACL} testTemplateRead(t, o, cli, exp) other := orgs[(i+1)%len(orgs)] require.NotEqual(t, other.Org.ID, o.Org.ID, "this test needs at least 2 orgs") testTemplateRead(t, other, cli, []codersdk.Template{}) } }) }