feat: add workspace auditing (#3966)

This commit is contained in:
Colin Adler 2022-09-10 11:07:45 -05:00 committed by GitHub
parent 442df9e132
commit 29bac36816
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 91 additions and 15 deletions

View File

@ -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)
})
}

View File

@ -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) {

View File

@ -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)
})
}

View File

@ -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