From 29bac36816ca24664143083057b75b02772d2496 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Sat, 10 Sep 2022 11:07:45 -0500 Subject: [PATCH] feat: add workspace auditing (#3966) --- coderd/users_test.go | 3 +- coderd/workspaces.go | 78 ++++++++++++++++++++++++++++++++++----- coderd/workspaces_test.go | 21 +++++++++-- codersdk/workspaces.go | 4 +- 4 files changed, 91 insertions(+), 15 deletions(-) diff --git a/coderd/users_test.go b/coderd/users_test.go index 766dc2c9d0..b47c805bfe 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -391,7 +391,8 @@ func TestPostUsers(t *testing.T) { Password: "testing", }) require.NoError(t, err) - assert.Len(t, auditor.AuditLogs, 1) + + require.Len(t, auditor.AuditLogs, 1) assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs[0].Action) }) } diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 2c787407e6..c021f9a033 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -22,6 +22,7 @@ import ( "cdr.dev/slog" + "github.com/coder/coder/coderd/audit" "github.com/coder/coder/coderd/autobuild/schedule" "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" @@ -248,8 +249,18 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) // Create a new workspace for the currently authenticated user. func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Request) { - organization := httpmw.OrganizationParam(r) - apiKey := httpmw.APIKey(r) + var ( + organization = httpmw.OrganizationParam(r) + apiKey = httpmw.APIKey(r) + aReq, commitAudit = audit.InitRequest[database.Workspace](rw, &audit.RequestParams{ + Features: api.FeaturesService, + Log: api.Logger, + Request: r, + Action: database.AuditActionCreate, + }) + ) + defer commitAudit() + if !api.Authorize(r, rbac.ActionCreate, rbac.ResourceWorkspace.InOrg(organization.ID).WithOwner(apiKey.UserID.String())) { httpapi.ResourceNotFound(rw) @@ -325,7 +336,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req }) return } - if !errors.Is(err, sql.ErrNoRows) { + if err != nil && !errors.Is(err, sql.ErrNoRows) { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ Message: fmt.Sprintf("Internal error fetching workspace by name %q.", createWorkspace.Name), Detail: err.Error(), @@ -457,6 +468,8 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req }) return } + aReq.New = workspace + users, err := api.Database.GetUsersByIDs(r.Context(), []uuid.UUID{apiKey.UserID, workspaceBuild.InitiatorID}) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ @@ -476,7 +489,18 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req } func (api *API) patchWorkspace(rw http.ResponseWriter, r *http.Request) { - workspace := httpmw.WorkspaceParam(r) + var ( + workspace = httpmw.WorkspaceParam(r) + aReq, commitAudit = audit.InitRequest[database.Workspace](rw, &audit.RequestParams{ + Features: api.FeaturesService, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + }) + ) + defer commitAudit() + aReq.Old = workspace + if !api.Authorize(r, rbac.ActionUpdate, workspace) { httpapi.ResourceNotFound(rw) return @@ -488,10 +512,12 @@ func (api *API) patchWorkspace(rw http.ResponseWriter, r *http.Request) { } if req.Name == "" || req.Name == workspace.Name { + aReq.New = workspace // Nothing changed, optionally this could be an error. rw.WriteHeader(http.StatusNoContent) return } + // The reason we double check here is in case more fields can be // patched in the future, it's enough if one changes. name := workspace.Name @@ -499,7 +525,7 @@ func (api *API) patchWorkspace(rw http.ResponseWriter, r *http.Request) { name = req.Name } - _, err := api.Database.UpdateWorkspace(r.Context(), database.UpdateWorkspaceParams{ + newWorkspace, err := api.Database.UpdateWorkspace(r.Context(), database.UpdateWorkspaceParams{ ID: workspace.ID, Name: name, }) @@ -534,11 +560,23 @@ func (api *API) patchWorkspace(rw http.ResponseWriter, r *http.Request) { return } + aReq.New = newWorkspace rw.WriteHeader(http.StatusNoContent) } func (api *API) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) { - workspace := httpmw.WorkspaceParam(r) + var ( + workspace = httpmw.WorkspaceParam(r) + aReq, commitAudit = audit.InitRequest[database.Workspace](rw, &audit.RequestParams{ + Features: api.FeaturesService, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + }) + ) + defer commitAudit() + aReq.Old = workspace + if !api.Authorize(r, rbac.ActionUpdate, workspace) { httpapi.ResourceNotFound(rw) return @@ -578,10 +616,26 @@ func (api *API) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) { }) return } + + newWorkspace := workspace + newWorkspace.AutostartSchedule = dbSched + aReq.New = newWorkspace + + rw.WriteHeader(http.StatusNoContent) } func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) { - workspace := httpmw.WorkspaceParam(r) + var ( + workspace = httpmw.WorkspaceParam(r) + aReq, commitAudit = audit.InitRequest[database.Workspace](rw, &audit.RequestParams{ + Features: api.FeaturesService, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + }) + ) + defer commitAudit() + if !api.Authorize(r, rbac.ActionUpdate, workspace) { httpapi.ResourceNotFound(rw) return @@ -592,6 +646,8 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) { return } + var dbTTL sql.NullInt64 + err := api.Database.InTx(func(s database.Store) error { template, err := s.GetTemplateByID(r.Context(), workspace.TemplateID) if err != nil { @@ -601,7 +657,7 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) { return xerrors.Errorf("fetch workspace template: %w", err) } - dbTTL, err := validWorkspaceTTLMillis(req.TTLMillis, time.Duration(template.MaxTtl)) + dbTTL, err = validWorkspaceTTLMillis(req.TTLMillis, time.Duration(template.MaxTtl)) if err != nil { return codersdk.ValidationError{Field: "ttl_ms", Detail: err.Error()} } @@ -630,7 +686,11 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) { return } - httpapi.Write(rw, http.StatusOK, nil) + newWorkspace := workspace + newWorkspace.Ttl = dbTTL + aReq.New = newWorkspace + + rw.WriteHeader(http.StatusNoContent) } func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) { diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 976c2dbb37..bbaba4e379 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -9,10 +9,13 @@ import ( "time" "github.com/google/uuid" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/coder/coder/coderd/audit" "github.com/coder/coder/coderd/autobuild/schedule" "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/rbac" "github.com/coder/coder/coderd/util/ptr" "github.com/coder/coder/codersdk" @@ -214,12 +217,16 @@ func TestPostWorkspacesByOrganization(t *testing.T) { t.Run("Create", func(t *testing.T) { t.Parallel() - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + auditor := audit.NewMock() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) _ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + + require.Len(t, auditor.AuditLogs, 4) + assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs[3].Action) }) t.Run("TemplateNoTTL", func(t *testing.T) { @@ -949,7 +956,8 @@ func TestWorkspaceUpdateAutostart(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { t.Parallel() var ( - client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + auditor = audit.NewMock() + client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor}) user = coderdtest.CreateFirstUser(t, client) version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) @@ -994,6 +1002,9 @@ func TestWorkspaceUpdateAutostart(t *testing.T) { require.Equal(t, testCase.expectedNext, next, "unexpected next scheduled autostart time") interval := next.Sub(testCase.at) require.Equal(t, testCase.expectedInterval, interval, "unexpected interval") + + require.Len(t, auditor.AuditLogs, 5) + assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs[4].Action) }) } @@ -1086,7 +1097,8 @@ func TestWorkspaceUpdateTTL(t *testing.T) { mutators = append(mutators, testCase.modifyTemplate) } var ( - client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + auditor = audit.NewMock() + client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor}) user = coderdtest.CreateFirstUser(t, client) version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) @@ -1116,6 +1128,9 @@ func TestWorkspaceUpdateTTL(t *testing.T) { require.NoError(t, err, "fetch updated workspace") require.Equal(t, testCase.ttlMillis, updated.TTLMillis, "expected autostop ttl to equal requested") + + require.Len(t, auditor.AuditLogs, 5) + assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs[4].Action) }) } diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 2fdad3b5a5..53247b5b86 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -183,7 +183,7 @@ func (c *Client) UpdateWorkspaceAutostart(ctx context.Context, id uuid.UUID, req return xerrors.Errorf("update workspace autostart: %w", err) } defer res.Body.Close() - if res.StatusCode != http.StatusOK { + if res.StatusCode != http.StatusNoContent { return readBodyAsError(res) } return nil @@ -203,7 +203,7 @@ func (c *Client) UpdateWorkspaceTTL(ctx context.Context, id uuid.UUID, req Updat return xerrors.Errorf("update workspace time until shutdown: %w", err) } defer res.Body.Close() - if res.StatusCode != http.StatusOK { + if res.StatusCode != http.StatusNoContent { return readBodyAsError(res) } return nil