mirror of
https://github.com/coder/coder.git
synced 2025-07-08 11:39:50 +00:00
fix: audit log broken build links (#5895)
* pushing for guidance * added test * PR feedback * fixed tests * Update coderd/audit.go Co-authored-by: Steven Masley <Emyrk@users.noreply.github.com> * runnig make gen --------- Co-authored-by: Steven Masley <Emyrk@users.noreply.github.com>
This commit is contained in:
6
coderd/apidoc/docs.go
generated
6
coderd/apidoc/docs.go
generated
@ -5734,6 +5734,12 @@ const docTemplate = `{
|
||||
}
|
||||
]
|
||||
},
|
||||
"additional_fields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"build_reason": {
|
||||
"enum": [
|
||||
"autostart",
|
||||
|
6
coderd/apidoc/swagger.json
generated
6
coderd/apidoc/swagger.json
generated
@ -5084,6 +5084,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"additional_fields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"build_reason": {
|
||||
"enum": ["autostart", "autostop", "initiator"],
|
||||
"allOf": [
|
||||
|
@ -17,6 +17,7 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/coderd/audit"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
@ -147,6 +148,9 @@ func (api *API) generateFakeAuditLog(rw http.ResponseWriter, r *http.Request) {
|
||||
if params.Time.IsZero() {
|
||||
params.Time = time.Now()
|
||||
}
|
||||
if len(params.AdditionalFields) == 0 {
|
||||
params.AdditionalFields = json.RawMessage("{}")
|
||||
}
|
||||
|
||||
_, err = api.Database.InsertAuditLog(ctx, database.InsertAuditLogParams{
|
||||
ID: uuid.New(),
|
||||
@ -160,7 +164,7 @@ func (api *API) generateFakeAuditLog(rw http.ResponseWriter, r *http.Request) {
|
||||
Action: database.AuditAction(params.Action),
|
||||
Diff: diff,
|
||||
StatusCode: http.StatusOK,
|
||||
AdditionalFields: []byte("{}"),
|
||||
AdditionalFields: params.AdditionalFields,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
@ -180,12 +184,6 @@ func (api *API) convertAuditLogs(ctx context.Context, dblogs []database.GetAudit
|
||||
return alogs
|
||||
}
|
||||
|
||||
type AdditionalFields struct {
|
||||
WorkspaceName string `json:"workspace_name"`
|
||||
BuildNumber string `json:"build_number"`
|
||||
BuildReason database.BuildReason `json:"build_reason"`
|
||||
}
|
||||
|
||||
func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogsOffsetRow) codersdk.AuditLog {
|
||||
ip, _ := netip.AddrFromSlice(dblog.Ip.IPNet.IP)
|
||||
|
||||
@ -213,16 +211,18 @@ func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogs
|
||||
|
||||
var (
|
||||
additionalFieldsBytes = []byte(dblog.AdditionalFields)
|
||||
additionalFields AdditionalFields
|
||||
additionalFields audit.AdditionalFields
|
||||
err = json.Unmarshal(additionalFieldsBytes, &additionalFields)
|
||||
)
|
||||
if err != nil {
|
||||
api.Logger.Error(ctx, "unmarshal additional fields", slog.Error(err))
|
||||
resourceInfo := map[string]string{
|
||||
"workspaceName": "unknown",
|
||||
"buildNumber": "unknown",
|
||||
"buildReason": "unknown",
|
||||
resourceInfo := audit.AdditionalFields{
|
||||
WorkspaceName: "unknown",
|
||||
BuildNumber: "unknown",
|
||||
BuildReason: "unknown",
|
||||
WorkspaceOwner: "unknown",
|
||||
}
|
||||
|
||||
dblog.AdditionalFields, err = json.Marshal(resourceInfo)
|
||||
api.Logger.Error(ctx, "marshal additional fields", slog.Error(err))
|
||||
}
|
||||
@ -259,7 +259,7 @@ func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogs
|
||||
}
|
||||
}
|
||||
|
||||
func auditLogDescription(alog database.GetAuditLogsOffsetRow, additionalFields AdditionalFields) string {
|
||||
func auditLogDescription(alog database.GetAuditLogsOffsetRow, additionalFields audit.AdditionalFields) string {
|
||||
str := fmt.Sprintf("{user} %s",
|
||||
codersdk.AuditAction(alog.Action).Friendly(),
|
||||
)
|
||||
@ -344,14 +344,16 @@ func (api *API) auditLogIsResourceDeleted(ctx context.Context, alog database.Get
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) auditLogResourceLink(ctx context.Context, alog database.GetAuditLogsOffsetRow, additionalFields AdditionalFields) string {
|
||||
func (api *API) auditLogResourceLink(ctx context.Context, alog database.GetAuditLogsOffsetRow, additionalFields audit.AdditionalFields) string {
|
||||
switch alog.ResourceType {
|
||||
case database.ResourceTypeTemplate:
|
||||
return fmt.Sprintf("/templates/%s",
|
||||
alog.ResourceTarget)
|
||||
|
||||
case database.ResourceTypeUser:
|
||||
return fmt.Sprintf("/users?filter=%s",
|
||||
alog.ResourceTarget)
|
||||
|
||||
case database.ResourceTypeWorkspace:
|
||||
workspace, getWorkspaceErr := api.Database.GetWorkspaceByID(ctx, alog.ResourceID)
|
||||
if getWorkspaceErr != nil {
|
||||
@ -363,6 +365,7 @@ func (api *API) auditLogResourceLink(ctx context.Context, alog database.GetAudit
|
||||
}
|
||||
return fmt.Sprintf("/@%s/%s",
|
||||
workspaceOwner.Username, alog.ResourceTarget)
|
||||
|
||||
case database.ResourceTypeWorkspaceBuild:
|
||||
if len(additionalFields.WorkspaceName) == 0 || len(additionalFields.BuildNumber) == 0 {
|
||||
return ""
|
||||
@ -381,6 +384,7 @@ func (api *API) auditLogResourceLink(ctx context.Context, alog database.GetAudit
|
||||
}
|
||||
return fmt.Sprintf("/@%s/%s/builds/%s",
|
||||
workspaceOwner.Username, additionalFields.WorkspaceName, additionalFields.BuildNumber)
|
||||
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
@ -12,6 +12,13 @@ type Auditor interface {
|
||||
diff(old, new any) Map
|
||||
}
|
||||
|
||||
type AdditionalFields struct {
|
||||
WorkspaceName string `json:"workspace_name"`
|
||||
BuildNumber string `json:"build_number"`
|
||||
BuildReason database.BuildReason `json:"build_reason"`
|
||||
WorkspaceOwner string `json:"workspace_owner"`
|
||||
}
|
||||
|
||||
func NewNop() Auditor {
|
||||
return nop{}
|
||||
}
|
||||
|
@ -2,12 +2,17 @@ package coderd_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/audit"
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
@ -36,6 +41,49 @@ func TestAuditLogs(t *testing.T) {
|
||||
require.Equal(t, int64(1), alogs.Count)
|
||||
require.Len(t, alogs.AuditLogs, 1)
|
||||
})
|
||||
|
||||
t.Run("WorkspaceBuildAuditLink", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
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)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
buildResourceInfo := audit.AdditionalFields{
|
||||
WorkspaceName: workspace.Name,
|
||||
BuildNumber: strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10),
|
||||
BuildReason: database.BuildReason(string(workspace.LatestBuild.Reason)),
|
||||
}
|
||||
|
||||
wriBytes, err := json.Marshal(buildResourceInfo)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{
|
||||
Action: codersdk.AuditActionStop,
|
||||
ResourceType: codersdk.ResourceTypeWorkspaceBuild,
|
||||
ResourceID: workspace.LatestBuild.ID,
|
||||
AdditionalFields: wriBytes,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
auditLogs, err := client.AuditLogs(ctx, codersdk.AuditLogsRequest{
|
||||
Pagination: codersdk.Pagination{
|
||||
Limit: 1,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
buildNumberString := strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10)
|
||||
require.Equal(t, auditLogs.AuditLogs[0].ResourceLink, fmt.Sprintf("/@%s/%s/builds/%s",
|
||||
workspace.OwnerName, workspace.Name, buildNumberString))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuditLogsFilter(t *testing.T) {
|
||||
|
@ -553,12 +553,13 @@ func (server *Server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*p
|
||||
if prevBuildErr != nil {
|
||||
previousBuild = database.WorkspaceBuild{}
|
||||
}
|
||||
|
||||
// We pass the below information to the Auditor so that it
|
||||
// can form a friendly string for the user to view in the UI.
|
||||
buildResourceInfo := map[string]string{
|
||||
"workspaceName": workspace.Name,
|
||||
"buildNumber": strconv.FormatInt(int64(build.BuildNumber), 10),
|
||||
"buildReason": fmt.Sprintf("%v", build.Reason),
|
||||
buildResourceInfo := audit.AdditionalFields{
|
||||
WorkspaceName: workspace.Name,
|
||||
BuildNumber: strconv.FormatInt(int64(build.BuildNumber), 10),
|
||||
BuildReason: database.BuildReason(string(build.Reason)),
|
||||
}
|
||||
|
||||
wriBytes, err := json.Marshal(buildResourceInfo)
|
||||
@ -816,10 +817,10 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete
|
||||
|
||||
// We pass the below information to the Auditor so that it
|
||||
// can form a friendly string for the user to view in the UI.
|
||||
buildResourceInfo := map[string]string{
|
||||
"workspaceName": workspace.Name,
|
||||
"buildNumber": strconv.FormatInt(int64(workspaceBuild.BuildNumber), 10),
|
||||
"buildReason": fmt.Sprintf("%v", workspaceBuild.Reason),
|
||||
buildResourceInfo := audit.AdditionalFields{
|
||||
WorkspaceName: workspace.Name,
|
||||
BuildNumber: strconv.FormatInt(int64(workspaceBuild.BuildNumber), 10),
|
||||
BuildReason: database.BuildReason(string(workspaceBuild.Reason)),
|
||||
}
|
||||
|
||||
wriBytes, err := json.Marshal(buildResourceInfo)
|
||||
|
@ -279,19 +279,29 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request)
|
||||
// @Router /organizations/{organization}/members/{user}/workspaces [post]
|
||||
func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
organization = httpmw.OrganizationParam(r)
|
||||
apiKey = httpmw.APIKey(r)
|
||||
auditor = api.Auditor.Load()
|
||||
user = httpmw.UserParam(r)
|
||||
aReq, commitAudit = audit.InitRequest[database.Workspace](rw, &audit.RequestParams{
|
||||
Audit: *auditor,
|
||||
Log: api.Logger,
|
||||
Request: r,
|
||||
Action: database.AuditActionCreate,
|
||||
})
|
||||
ctx = r.Context()
|
||||
organization = httpmw.OrganizationParam(r)
|
||||
apiKey = httpmw.APIKey(r)
|
||||
auditor = api.Auditor.Load()
|
||||
user = httpmw.UserParam(r)
|
||||
workspaceResourceInfo = audit.AdditionalFields{
|
||||
WorkspaceOwner: user.Username,
|
||||
}
|
||||
)
|
||||
|
||||
wriBytes, err := json.Marshal(workspaceResourceInfo)
|
||||
if err != nil {
|
||||
api.Logger.Warn(ctx, "marshal workspace owner name")
|
||||
}
|
||||
|
||||
aReq, commitAudit := audit.InitRequest[database.Workspace](rw, &audit.RequestParams{
|
||||
Audit: *auditor,
|
||||
Log: api.Logger,
|
||||
Request: r,
|
||||
Action: database.AuditActionCreate,
|
||||
AdditionalFields: wriBytes,
|
||||
})
|
||||
|
||||
defer commitAudit()
|
||||
|
||||
if !api.Authorize(r, rbac.ActionCreate,
|
||||
|
Reference in New Issue
Block a user