feat: add template/template version auditing (#3965)

This commit is contained in:
Colin Adler
2022-09-09 11:34:23 -05:00
committed by GitHub
parent d380c9494d
commit abb804f2de
8 changed files with 158 additions and 45 deletions

View File

@ -920,7 +920,7 @@ func (q *fakeQuerier) GetTemplateByOrganizationAndName(_ context.Context, arg da
return database.Template{}, sql.ErrNoRows
}
func (q *fakeQuerier) UpdateTemplateMetaByID(_ context.Context, arg database.UpdateTemplateMetaByIDParams) error {
func (q *fakeQuerier) UpdateTemplateMetaByID(_ context.Context, arg database.UpdateTemplateMetaByIDParams) (database.Template, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@ -935,10 +935,10 @@ func (q *fakeQuerier) UpdateTemplateMetaByID(_ context.Context, arg database.Upd
tpl.MaxTtl = arg.MaxTtl
tpl.MinAutostartInterval = arg.MinAutostartInterval
q.templates[idx] = tpl
return nil
return tpl, nil
}
return sql.ErrNoRows
return database.Template{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetTemplatesWithFilter(_ context.Context, arg database.GetTemplatesWithFilterParams) ([]database.Template, error) {

View File

@ -134,7 +134,7 @@ type querier interface {
UpdateProvisionerJobWithCompleteByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteByIDParams) error
UpdateTemplateActiveVersionByID(ctx context.Context, arg UpdateTemplateActiveVersionByIDParams) error
UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTemplateDeletedByIDParams) error
UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error
UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) (Template, error)
UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) error
UpdateTemplateVersionDescriptionByJobID(ctx context.Context, arg UpdateTemplateVersionDescriptionByJobIDParams) error
UpdateUserHashedPassword(ctx context.Context, arg UpdateUserHashedPasswordParams) error

View File

@ -2361,7 +2361,7 @@ func (q *sqlQuerier) UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTe
return err
}
const updateTemplateMetaByID = `-- name: UpdateTemplateMetaByID :exec
const updateTemplateMetaByID = `-- name: UpdateTemplateMetaByID :one
UPDATE
templates
SET
@ -2387,8 +2387,8 @@ type UpdateTemplateMetaByIDParams struct {
Icon string `db:"icon" json:"icon"`
}
func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error {
_, err := q.db.ExecContext(ctx, updateTemplateMetaByID,
func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) (Template, error) {
row := q.db.QueryRowContext(ctx, updateTemplateMetaByID,
arg.ID,
arg.UpdatedAt,
arg.Description,
@ -2397,7 +2397,23 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl
arg.Name,
arg.Icon,
)
return err
var i Template
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OrganizationID,
&i.Deleted,
&i.Name,
&i.Provisioner,
&i.ActiveVersionID,
&i.Description,
&i.MaxTtl,
&i.MinAutostartInterval,
&i.CreatedBy,
&i.Icon,
)
return i, err
}
const getTemplateVersionByID = `-- name: GetTemplateVersionByID :one

View File

@ -91,7 +91,7 @@ SET
WHERE
id = $1;
-- name: UpdateTemplateMetaByID :exec
-- name: UpdateTemplateMetaByID :one
UPDATE
templates
SET

View File

@ -16,6 +16,7 @@ import (
"github.com/moby/moby/pkg/namesgenerator"
"golang.org/x/xerrors"
"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
@ -82,7 +83,18 @@ func (api *API) template(rw http.ResponseWriter, r *http.Request) {
}
func (api *API) deleteTemplate(rw http.ResponseWriter, r *http.Request) {
template := httpmw.TemplateParam(r)
var (
template = httpmw.TemplateParam(r)
aReq, commitAudit = audit.InitRequest[database.Template](rw, &audit.RequestParams{
Features: api.FeaturesService,
Log: api.Logger,
Request: r,
Action: database.AuditActionDelete,
})
)
defer commitAudit()
aReq.Old = template
if !api.Authorize(r, rbac.ActionDelete, template) {
httpapi.ResourceNotFound(rw)
return
@ -91,10 +103,7 @@ func (api *API) deleteTemplate(rw http.ResponseWriter, r *http.Request) {
workspaces, err := api.Database.GetWorkspaces(r.Context(), database.GetWorkspacesParams{
TemplateIds: []uuid.UUID{template.ID},
})
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
if err != nil && !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspaces by template id.",
Detail: err.Error(),
@ -126,9 +135,26 @@ func (api *API) deleteTemplate(rw http.ResponseWriter, r *http.Request) {
// Create a new template in an organization.
func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Request) {
var createTemplate codersdk.CreateTemplateRequest
organization := httpmw.OrganizationParam(r)
apiKey := httpmw.APIKey(r)
var (
createTemplate codersdk.CreateTemplateRequest
organization = httpmw.OrganizationParam(r)
apiKey = httpmw.APIKey(r)
templateAudit, commitTemplateAudit = audit.InitRequest[database.Template](rw, &audit.RequestParams{
Features: api.FeaturesService,
Log: api.Logger,
Request: r,
Action: database.AuditActionCreate,
})
templateVersionAudit, commitTemplateVersionAudit = audit.InitRequest[database.TemplateVersion](rw, &audit.RequestParams{
Features: api.FeaturesService,
Log: api.Logger,
Request: r,
Action: database.AuditActionWrite,
})
)
defer commitTemplateAudit()
defer commitTemplateVersionAudit()
if !api.Authorize(r, rbac.ActionCreate, rbac.ResourceTemplate.InOrg(organization.ID)) {
httpapi.ResourceNotFound(rw)
return
@ -175,6 +201,8 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
})
return
}
templateVersionAudit.Old = templateVersion
importJob, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
@ -234,6 +262,8 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
return xerrors.Errorf("insert template: %s", err)
}
templateAudit.New = dbTemplate
err = db.UpdateTemplateVersionByID(r.Context(), database.UpdateTemplateVersionByIDParams{
ID: templateVersion.ID,
TemplateID: uuid.NullUUID{
@ -245,6 +275,12 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
if err != nil {
return xerrors.Errorf("insert template version: %s", err)
}
newTemplateVersion := templateVersion
newTemplateVersion.TemplateID = uuid.NullUUID{
UUID: dbTemplate.ID,
Valid: true,
}
templateVersionAudit.New = newTemplateVersion
for _, parameterValue := range createTemplate.ParameterValues {
_, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{
@ -397,7 +433,18 @@ func (api *API) templateByOrganizationAndName(rw http.ResponseWriter, r *http.Re
}
func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
template := httpmw.TemplateParam(r)
var (
template = httpmw.TemplateParam(r)
aReq, commitAudit = audit.InitRequest[database.Template](rw, &audit.RequestParams{
Features: api.FeaturesService,
Log: api.Logger,
Request: r,
Action: database.AuditActionWrite,
})
)
defer commitAudit()
aReq.Old = template
if !api.Authorize(r, rbac.ActionUpdate, template) {
httpapi.ResourceNotFound(rw)
return
@ -474,7 +521,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
minAutostartInterval = time.Duration(template.MinAutostartInterval)
}
if err := s.UpdateTemplateMetaByID(r.Context(), database.UpdateTemplateMetaByIDParams{
updated, err = s.UpdateTemplateMetaByID(r.Context(), database.UpdateTemplateMetaByIDParams{
ID: template.ID,
UpdatedAt: database.Now(),
Name: name,
@ -482,28 +529,24 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
Icon: icon,
MaxTtl: int64(maxTTL),
MinAutostartInterval: int64(minAutostartInterval),
}); err != nil {
return err
}
updated, err = s.GetTemplateByID(r.Context(), template.ID)
})
if err != nil {
return err
}
return nil
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error updating template metadata.",
Detail: err.Error(),
})
httpapi.InternalServerError(rw, err)
return
}
if updated.UpdatedAt.IsZero() {
aReq.New = template
httpapi.Write(rw, http.StatusNotModified, nil)
return
}
aReq.New = updated
createdByNameMap, err := getCreatedByNamesByTemplateIDs(r.Context(), api.Database, []database.Template{updated})
if err != nil {

View File

@ -13,7 +13,9 @@ import (
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/agent"
"github.com/coder/coder/coderd/audit"
"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"
@ -78,7 +80,8 @@ func TestPostTemplateByOrganization(t *testing.T) {
t.Parallel()
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
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)
@ -92,6 +95,11 @@ func TestPostTemplateByOrganization(t *testing.T) {
assert.Equal(t, expected.Name, got.Name)
assert.Equal(t, expected.Description, got.Description)
require.Len(t, auditor.AuditLogs, 3)
assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs[0].Action)
assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs[1].Action)
assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs[2].Action)
})
t.Run("AlreadyExists", func(t *testing.T) {
@ -291,7 +299,8 @@ func TestPatchTemplateMeta(t *testing.T) {
t.Run("Modified", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
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, func(ctr *codersdk.CreateTemplateRequest) {
@ -332,6 +341,9 @@ func TestPatchTemplateMeta(t *testing.T) {
assert.Equal(t, req.Icon, updated.Icon)
assert.Equal(t, req.MaxTTLMillis, updated.MaxTTLMillis)
assert.Equal(t, req.MinAutostartIntervalMillis, updated.MinAutostartIntervalMillis)
require.Len(t, auditor.AuditLogs, 4)
assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs[3].Action)
})
t.Run("NoMaxTTL", func(t *testing.T) {
@ -514,7 +526,8 @@ func TestDeleteTemplate(t *testing.T) {
t.Run("NoWorkspaces", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
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)
@ -524,6 +537,9 @@ func TestDeleteTemplate(t *testing.T) {
err := client.DeleteTemplate(ctx, template.ID)
require.NoError(t, err)
require.Len(t, auditor.AuditLogs, 4)
assert.Equal(t, database.AuditActionDelete, auditor.AuditLogs[3].Action)
})
t.Run("Workspaces", func(t *testing.T) {

View File

@ -13,6 +13,7 @@ import (
"github.com/moby/moby/pkg/namesgenerator"
"golang.org/x/xerrors"
"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
@ -556,7 +557,18 @@ func (api *API) templateVersionByName(rw http.ResponseWriter, r *http.Request) {
}
func (api *API) patchActiveTemplateVersion(rw http.ResponseWriter, r *http.Request) {
template := httpmw.TemplateParam(r)
var (
template = httpmw.TemplateParam(r)
aReq, commitAudit = audit.InitRequest[database.Template](rw, &audit.RequestParams{
Features: api.FeaturesService,
Log: api.Logger,
Request: r,
Action: database.AuditActionWrite,
})
)
defer commitAudit()
aReq.Old = template
if !api.Authorize(r, rbac.ActionUpdate, template) {
httpapi.ResourceNotFound(rw)
return
@ -581,7 +593,7 @@ func (api *API) patchActiveTemplateVersion(rw http.ResponseWriter, r *http.Reque
return
}
if version.TemplateID.UUID.String() != template.ID.String() {
httpapi.Write(rw, http.StatusUnauthorized, codersdk.Response{
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
Message: "The provided template version doesn't belong to the specified template.",
})
return
@ -605,6 +617,10 @@ func (api *API) patchActiveTemplateVersion(rw http.ResponseWriter, r *http.Reque
})
return
}
newTemplate := template
newTemplate.ActiveVersionID = req.ID
aReq.New = newTemplate
httpapi.Write(rw, http.StatusOK, codersdk.Response{
Message: "Updated the active template version!",
})
@ -612,13 +628,30 @@ func (api *API) patchActiveTemplateVersion(rw http.ResponseWriter, r *http.Reque
// Creates a new version of a template. An import job is queued to parse the storage method provided.
func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
organization := httpmw.OrganizationParam(r)
var req codersdk.CreateTemplateVersionRequest
var (
apiKey = httpmw.APIKey(r)
organization = httpmw.OrganizationParam(r)
aReq, commitAudit = audit.InitRequest[database.TemplateVersion](rw, &audit.RequestParams{
Features: api.FeaturesService,
Log: api.Logger,
Request: r,
Action: database.AuditActionCreate,
})
req codersdk.CreateTemplateVersionRequest
)
defer commitAudit()
if !httpapi.Read(rw, r, &req) {
return
}
// Making a new template version is the same permission as creating a new template.
if !api.Authorize(r, rbac.ActionCreate, rbac.ResourceTemplate.InOrg(organization.ID)) {
httpapi.ResourceNotFound(rw)
return
}
if req.TemplateID != uuid.Nil {
_, err := api.Database.GetTemplateByID(r.Context(), req.TemplateID)
if errors.Is(err, sql.ErrNoRows) {
@ -651,12 +684,6 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
return
}
// Making a new template version is the same permission as creating a new template.
if !api.Authorize(r, rbac.ActionCreate, rbac.ResourceTemplate.InOrg(organization.ID)) {
httpapi.ResourceNotFound(rw)
return
}
if !api.Authorize(r, rbac.ActionRead, file) {
httpapi.ResourceNotFound(rw)
return
@ -778,6 +805,7 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
})
return
}
aReq.New = templateVersion
createdByName, err := getUsernameByUserID(r.Context(), api.Database, templateVersion.CreatedBy)
if err != nil {

View File

@ -11,7 +11,9 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
@ -76,7 +78,8 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
t.Run("WithParameters", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
auditor := audit.NewMock()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor})
user := coderdtest.CreateFirstUser(t, client)
data, err := echo.Tar(&echo.Responses{
Parse: echo.ParseComplete,
@ -102,6 +105,9 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
}},
})
require.NoError(t, err)
require.Len(t, auditor.AuditLogs, 1)
assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs[0].Action)
})
}
@ -540,12 +546,13 @@ func TestPatchActiveTemplateVersion(t *testing.T) {
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
})
t.Run("Found", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
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)
@ -557,6 +564,9 @@ func TestPatchActiveTemplateVersion(t *testing.T) {
ID: version.ID,
})
require.NoError(t, err)
require.Len(t, auditor.AuditLogs, 4)
assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs[3].Action)
})
}