Skip to content

Commit 6cb618c

Browse files
fix(coderd): ensure lifecycle executor has sufficient task permissions
We recently made a change to the `wsbuilder` to handle task related logic. Our test coverage for the lifecycle executor didn't handle this scenario and so we missed the insufficient permissions. This PR adds `Update` and `Read` permissions for `Task`s in the lifecycle executor, as well as an autostart/autostop test tailored to task workspaces to verify the change.
1 parent dd28eef commit 6cb618c

File tree

2 files changed

+173
-0
lines changed

2 files changed

+173
-0
lines changed

coderd/autobuild/lifecycle_executor_test.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1764,3 +1764,175 @@ func TestExecutorAutostartSkipsWhenNoProvisionersAvailable(t *testing.T) {
17641764

17651765
assert.Len(t, stats.Transitions, 1, "should create builds when provisioners are available")
17661766
}
1767+
1768+
func TestExecutorTaskWorkspace(t *testing.T) {
1769+
t.Parallel()
1770+
1771+
createTaskTemplate := func(t *testing.T, client *codersdk.Client, orgID uuid.UUID, ctx context.Context, defaultTTL time.Duration) codersdk.Template {
1772+
t.Helper()
1773+
1774+
taskAppID := uuid.New()
1775+
version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{
1776+
Parse: echo.ParseComplete,
1777+
ProvisionPlan: []*proto.Response{
1778+
{
1779+
Type: &proto.Response_Plan{
1780+
Plan: &proto.PlanComplete{HasAiTasks: true},
1781+
},
1782+
},
1783+
},
1784+
ProvisionApply: []*proto.Response{
1785+
{
1786+
Type: &proto.Response_Apply{
1787+
Apply: &proto.ApplyComplete{
1788+
Resources: []*proto.Resource{
1789+
{
1790+
Agents: []*proto.Agent{
1791+
{
1792+
Id: uuid.NewString(),
1793+
Name: "dev",
1794+
Auth: &proto.Agent_Token{
1795+
Token: uuid.NewString(),
1796+
},
1797+
Apps: []*proto.App{
1798+
{
1799+
Id: taskAppID.String(),
1800+
Slug: "task-app",
1801+
},
1802+
},
1803+
},
1804+
},
1805+
},
1806+
},
1807+
AiTasks: []*proto.AITask{
1808+
{
1809+
AppId: taskAppID.String(),
1810+
},
1811+
},
1812+
},
1813+
},
1814+
},
1815+
},
1816+
})
1817+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
1818+
template := coderdtest.CreateTemplate(t, client, orgID, version.ID)
1819+
1820+
if defaultTTL > 0 {
1821+
_, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
1822+
DefaultTTLMillis: defaultTTL.Milliseconds(),
1823+
})
1824+
require.NoError(t, err)
1825+
}
1826+
1827+
return template
1828+
}
1829+
1830+
createTaskWorkspace := func(t *testing.T, client *codersdk.Client, template codersdk.Template, ctx context.Context, input string) codersdk.Workspace {
1831+
t.Helper()
1832+
1833+
exp := codersdk.NewExperimentalClient(client)
1834+
task, err := exp.CreateTask(ctx, "me", codersdk.CreateTaskRequest{
1835+
TemplateVersionID: template.ActiveVersionID,
1836+
Input: input,
1837+
})
1838+
require.NoError(t, err)
1839+
require.True(t, task.WorkspaceID.Valid, "task should have a workspace")
1840+
1841+
workspace, err := client.Workspace(ctx, task.WorkspaceID.UUID)
1842+
require.NoError(t, err)
1843+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
1844+
1845+
return workspace
1846+
}
1847+
1848+
t.Run("Autostart", func(t *testing.T) {
1849+
t.Parallel()
1850+
1851+
var (
1852+
ctx = testutil.Context(t, testutil.WaitShort)
1853+
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
1854+
tickCh = make(chan time.Time)
1855+
statsCh = make(chan autobuild.Stats)
1856+
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{
1857+
AutobuildTicker: tickCh,
1858+
IncludeProvisionerDaemon: true,
1859+
AutobuildStats: statsCh,
1860+
})
1861+
admin = coderdtest.CreateFirstUser(t, client)
1862+
)
1863+
1864+
// Given: A task workspace
1865+
template := createTaskTemplate(t, client, admin.OrganizationID, ctx, 0)
1866+
workspace := createTaskWorkspace(t, client, template, ctx, "test task for autostart")
1867+
1868+
// Given: The task workspace has an autostart schedule
1869+
err := client.UpdateWorkspaceAutostart(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
1870+
Schedule: ptr.Ref(sched.String()),
1871+
})
1872+
require.NoError(t, err)
1873+
1874+
// Given: That the workspace is in a stopped state.
1875+
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
1876+
1877+
p, err := coderdtest.GetProvisionerForTags(db, time.Now(), workspace.OrganizationID, map[string]string{})
1878+
require.NoError(t, err)
1879+
1880+
// When: the autobuild executor ticks after the scheduled time
1881+
go func() {
1882+
tickTime := sched.Next(workspace.LatestBuild.CreatedAt)
1883+
coderdtest.UpdateProvisionerLastSeenAt(t, db, p.ID, tickTime)
1884+
tickCh <- tickTime
1885+
close(tickCh)
1886+
}()
1887+
1888+
// Then: We expect to see a start transition
1889+
stats := <-statsCh
1890+
require.Len(t, stats.Transitions, 1, "lifecycle executor should transition the task workspace")
1891+
assert.Contains(t, stats.Transitions, workspace.ID, "task workspace should be in transitions")
1892+
assert.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[workspace.ID], "should autostart the workspace")
1893+
require.Empty(t, stats.Errors, "should have no errors when managing task workspaces")
1894+
})
1895+
1896+
t.Run("Autostop", func(t *testing.T) {
1897+
t.Parallel()
1898+
1899+
var (
1900+
ctx = testutil.Context(t, testutil.WaitShort)
1901+
tickCh = make(chan time.Time)
1902+
statsCh = make(chan autobuild.Stats)
1903+
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{
1904+
AutobuildTicker: tickCh,
1905+
IncludeProvisionerDaemon: true,
1906+
AutobuildStats: statsCh,
1907+
})
1908+
admin = coderdtest.CreateFirstUser(t, client)
1909+
)
1910+
1911+
// Given: A task workspace with an 8 hour deadline
1912+
template := createTaskTemplate(t, client, admin.OrganizationID, ctx, 8*time.Hour)
1913+
workspace := createTaskWorkspace(t, client, template, ctx, "test task for autostop")
1914+
1915+
// Given: The workspace is currently running
1916+
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
1917+
require.Equal(t, codersdk.WorkspaceTransitionStart, workspace.LatestBuild.Transition)
1918+
require.NotZero(t, workspace.LatestBuild.Deadline, "workspace should have a deadline for autostop")
1919+
1920+
p, err := coderdtest.GetProvisionerForTags(db, time.Now(), workspace.OrganizationID, map[string]string{})
1921+
require.NoError(t, err)
1922+
1923+
// When: the autobuild executor ticks after the deadline
1924+
go func() {
1925+
tickTime := workspace.LatestBuild.Deadline.Time.Add(time.Minute)
1926+
coderdtest.UpdateProvisionerLastSeenAt(t, db, p.ID, tickTime)
1927+
tickCh <- tickTime
1928+
close(tickCh)
1929+
}()
1930+
1931+
// Then: We expect to see a stop transition
1932+
stats := <-statsCh
1933+
require.Len(t, stats.Transitions, 1, "lifecycle executor should transition the task workspace")
1934+
assert.Contains(t, stats.Transitions, workspace.ID, "task workspace should be in transitions")
1935+
assert.Equal(t, database.WorkspaceTransitionStop, stats.Transitions[workspace.ID], "should autostop the workspace")
1936+
require.Empty(t, stats.Errors, "should have no errors when managing task workspaces")
1937+
})
1938+
}

coderd/database/dbauthz/dbauthz.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ var (
254254
rbac.ResourceFile.Type: {policy.ActionRead}, // Required to read terraform files
255255
rbac.ResourceNotificationMessage.Type: {policy.ActionCreate, policy.ActionRead},
256256
rbac.ResourceSystem.Type: {policy.WildcardSymbol},
257+
rbac.ResourceTask.Type: {policy.ActionRead, policy.ActionUpdate},
257258
rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate},
258259
rbac.ResourceUser.Type: {policy.ActionRead},
259260
rbac.ResourceWorkspace.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop},

0 commit comments

Comments
 (0)