Add build number to workspace_build audit logs (#5267)

* got links working

* added translations

* fixed translation

* added translation for unavailable ip

* added support for group, template, user links

* cleaned up string

* added deleted label

* querying for workspace id

* remove prints

* fix/write tests

* added build number

* checking for existence of additional fields

* adjust documentation

* PR feedback
This commit is contained in:
Kira Pilot
2022-12-06 13:33:21 -05:00
committed by GitHub
parent 6651c1632d
commit df389d429c
4 changed files with 47 additions and 31 deletions

View File

@ -160,6 +160,11 @@ func (api *API) convertAuditLogs(ctx context.Context, dblogs []database.GetAudit
return alogs return alogs
} }
type AdditionalFields struct {
WorkspaceName string
BuildNumber string
}
func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogsOffsetRow) codersdk.AuditLog { func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogsOffsetRow) codersdk.AuditLog {
ip, _ := netip.AddrFromSlice(dblog.Ip.IPNet.IP) ip, _ := netip.AddrFromSlice(dblog.Ip.IPNet.IP)
@ -185,12 +190,29 @@ func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogs
} }
} }
isDeleted := api.auditLogIsResourceDeleted(ctx, dblog) var (
var resourceLink string additionalFieldsBytes = []byte(dblog.AdditionalFields)
additionalFields 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",
}
dblog.AdditionalFields, err = json.Marshal(resourceInfo)
api.Logger.Error(ctx, "marshal additional fields", slog.Error(err))
}
var (
isDeleted = api.auditLogIsResourceDeleted(ctx, dblog)
resourceLink string
)
if isDeleted { if isDeleted {
resourceLink = "" resourceLink = ""
} else { } else {
resourceLink = api.auditLogResourceLink(ctx, dblog) resourceLink = auditLogResourceLink(dblog, additionalFields)
} }
return codersdk.AuditLog{ return codersdk.AuditLog{
@ -209,23 +231,28 @@ func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogs
StatusCode: dblog.StatusCode, StatusCode: dblog.StatusCode,
AdditionalFields: dblog.AdditionalFields, AdditionalFields: dblog.AdditionalFields,
User: user, User: user,
Description: auditLogDescription(dblog), Description: auditLogDescription(dblog, additionalFields),
ResourceLink: resourceLink, ResourceLink: resourceLink,
IsDeleted: isDeleted, IsDeleted: isDeleted,
} }
} }
func auditLogDescription(alog database.GetAuditLogsOffsetRow) string { func auditLogDescription(alog database.GetAuditLogsOffsetRow, additionalFields AdditionalFields) string {
str := fmt.Sprintf("{user} %s", str := fmt.Sprintf("{user} %s",
codersdk.AuditAction(alog.Action).FriendlyString(), codersdk.AuditAction(alog.Action).FriendlyString(),
) )
// Strings for starting/stopping workspace builds follow the below format: // Strings for starting/stopping workspace builds follow the below format:
// "{user} started build for workspace {target}" // "{user} started build #{build_number} for workspace {target}"
// where target is a workspace instead of a workspace build // where target is a workspace instead of a workspace build
// passed in on the FE via AuditLog.AdditionalFields rather than derived in request.go:35 // passed in on the FE via AuditLog.AdditionalFields rather than derived in request.go:35
if alog.ResourceType == database.ResourceTypeWorkspaceBuild && alog.Action != database.AuditActionDelete { if alog.ResourceType == database.ResourceTypeWorkspaceBuild && alog.Action != database.AuditActionDelete {
str += " build for" if len(additionalFields.BuildNumber) == 0 {
str += " build for"
} else {
str += fmt.Sprintf(" build #%s for",
additionalFields.BuildNumber)
}
} }
// We don't display the name (target) for git ssh keys. It's fairly long and doesn't // We don't display the name (target) for git ssh keys. It's fairly long and doesn't
@ -295,12 +322,7 @@ func (api *API) auditLogIsResourceDeleted(ctx context.Context, alog database.Get
} }
} }
type AdditionalFields struct { func auditLogResourceLink(alog database.GetAuditLogsOffsetRow, additionalFields AdditionalFields) string {
WorkspaceName string
BuildNumber string
}
func (api *API) auditLogResourceLink(ctx context.Context, alog database.GetAuditLogsOffsetRow) string {
switch alog.ResourceType { switch alog.ResourceType {
case database.ResourceTypeTemplate: case database.ResourceTypeTemplate:
return fmt.Sprintf("/templates/%s", return fmt.Sprintf("/templates/%s",
@ -312,11 +334,8 @@ func (api *API) auditLogResourceLink(ctx context.Context, alog database.GetAudit
return fmt.Sprintf("/@%s/%s", return fmt.Sprintf("/@%s/%s",
alog.UserUsername.String, alog.ResourceTarget) alog.UserUsername.String, alog.ResourceTarget)
case database.ResourceTypeWorkspaceBuild: case database.ResourceTypeWorkspaceBuild:
additionalFieldsBytes := []byte(alog.AdditionalFields) if len(additionalFields.WorkspaceName) == 0 || len(additionalFields.BuildNumber) == 0 {
var additionalFields AdditionalFields return ""
err := json.Unmarshal(additionalFieldsBytes, &additionalFields)
if err != nil {
api.Logger.Error(ctx, "unmarshal workspace name", slog.Error(err))
} }
return fmt.Sprintf("/@%s/%s/builds/%s", return fmt.Sprintf("/@%s/%s/builds/%s",
alog.UserUsername.String, additionalFields.WorkspaceName, additionalFields.BuildNumber) alog.UserUsername.String, additionalFields.WorkspaceName, additionalFields.BuildNumber)

View File

@ -531,23 +531,23 @@ func (server *Server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*p
auditor := server.Auditor.Load() auditor := server.Auditor.Load()
build, getBuildErr := server.Database.GetWorkspaceBuildByJobID(ctx, job.ID) build, getBuildErr := server.Database.GetWorkspaceBuildByJobID(ctx, job.ID)
if getBuildErr != nil { if getBuildErr != nil {
server.Logger.Error(ctx, "failed to create audit log - get build err", slog.Error(err)) server.Logger.Error(ctx, "audit log - get build", slog.Error(err))
} else { } else {
auditAction := auditActionFromTransition(build.Transition) auditAction := auditActionFromTransition(build.Transition)
workspace, getWorkspaceErr := server.Database.GetWorkspaceByID(ctx, build.WorkspaceID) workspace, getWorkspaceErr := server.Database.GetWorkspaceByID(ctx, build.WorkspaceID)
if getWorkspaceErr != nil { if getWorkspaceErr != nil {
server.Logger.Error(ctx, "failed to create audit log - get workspace err", slog.Error(err)) server.Logger.Error(ctx, "audit log - get workspace", slog.Error(err))
} else { } else {
// We pass the below information to the Auditor so that it // We pass the below information to the Auditor so that it
// can form a friendly string for the user to view in the UI. // can form a friendly string for the user to view in the UI.
workspaceResourceInfo := map[string]string{ buildResourceInfo := map[string]string{
"workspaceName": workspace.Name, "workspaceName": workspace.Name,
"buildNumber": strconv.FormatInt(int64(build.BuildNumber), 10), "buildNumber": strconv.FormatInt(int64(build.BuildNumber), 10),
} }
wriBytes, err := json.Marshal(workspaceResourceInfo) wriBytes, err := json.Marshal(buildResourceInfo)
if err != nil { if err != nil {
server.Logger.Error(ctx, "could not marshal workspace name", slog.Error(err)) server.Logger.Error(ctx, "marshal workspace resource info for failed job", slog.Error(err))
} }
audit.BuildAudit(ctx, &audit.BuildAuditParams[database.WorkspaceBuild]{ audit.BuildAudit(ctx, &audit.BuildAuditParams[database.WorkspaceBuild]{
@ -756,14 +756,14 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete
// We pass the below information to the Auditor so that it // We pass the below information to the Auditor so that it
// can form a friendly string for the user to view in the UI. // can form a friendly string for the user to view in the UI.
workspaceResourceInfo := map[string]string{ buildResourceInfo := map[string]string{
"workspaceName": workspace.Name, "workspaceName": workspace.Name,
"buildNumber": strconv.FormatInt(int64(workspaceBuild.BuildNumber), 10), "buildNumber": strconv.FormatInt(int64(workspaceBuild.BuildNumber), 10),
} }
wriBytes, err := json.Marshal(workspaceResourceInfo) wriBytes, err := json.Marshal(buildResourceInfo)
if err != nil { if err != nil {
server.Logger.Error(ctx, "marshal resource info", slog.Error(err)) server.Logger.Error(ctx, "marshal resource info for successful job", slog.Error(err))
} }
audit.BuildAudit(ctx, &audit.BuildAuditParams[database.WorkspaceBuild]{ audit.BuildAudit(ctx, &audit.BuildAuditParams[database.WorkspaceBuild]{

View File

@ -11,7 +11,7 @@ We track **create, update and delete** events for the following resources:
- Template - Template
- TemplateVersion - TemplateVersion
- Workspace - Workspace
- Workspace start/stop - WorkspaceBuild
- User - User
- Group - Group

View File

@ -14,10 +14,7 @@ export const AuditLogDescription: FC<{ auditLog: AuditLog }> = ({
let target = auditLog.resource_target.trim() let target = auditLog.resource_target.trim()
// audit logs with a resource_type of workspace build use workspace name as a target // audit logs with a resource_type of workspace build use workspace name as a target
if ( if (auditLog.resource_type === "workspace_build") {
auditLog.resource_type === "workspace_build" &&
auditLog.additional_fields.workspaceName
) {
target = auditLog.additional_fields.workspaceName.trim() target = auditLog.additional_fields.workspaceName.trim()
} }