mirror of https://github.com/coder/coder.git
feat: add workspace auditing (#3966)
This commit is contained in:
parent
442df9e132
commit
29bac36816
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue