Skip to content

Add build number to workspace_build audit logs #5267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Dec 6, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
cleaned up string
  • Loading branch information
Kira-Pilot committed Nov 30, 2022
commit 709e42e7c11c6ddf980f37056d7c81c921db8be5
84 changes: 71 additions & 13 deletions coderd/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,32 +207,84 @@ func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogs

type AdditionalFields struct {
WorkspaceName string
WorkspaceID string
BuildNumber string
}

func (api *API) auditLogIsResourceDeleted(ctx context.Context, alog database.GetAuditLogsOffsetRow) bool {
switch alog.ResourceType {
case database.ResourceTypeTemplate:
template, err := api.Database.GetTemplateByID(ctx, alog.ResourceID)
if err != nil {
api.Logger.Error(ctx, "could not get template", slog.Error(err))
}
return template.Deleted
case database.ResourceTypeUser:
user, err := api.Database.GetUserByID(ctx, alog.ResourceID)
if err != nil {
api.Logger.Error(ctx, "could not get user", slog.Error(err))
}
return user.Deleted
case database.ResourceTypeWorkspace:
workspace, err := api.Database.GetWorkspaceByID(ctx, alog.ResourceID)
if err != nil {
api.Logger.Error(ctx, "could not get workspace", slog.Error(err))
}
return workspace.Deleted
case database.ResourceTypeWorkspaceBuild:
additionalFieldsBytes := []byte(alog.AdditionalFields)
var additionalFields AdditionalFields
err := json.Unmarshal(additionalFieldsBytes, &additionalFields)
if err != nil {
api.Logger.Error(ctx, "could not unmarshal workspace ID", slog.Error(err))
}
// if we don't have a WorkspaceID, we return true so as to hide the link in the UI
if len(additionalFields.WorkspaceID) < 1 {
return true
}
// We use workspace as a proxy for workspace build here
workspace, err := api.Database.GetWorkspaceByID(ctx, uuid.MustParse(additionalFields.WorkspaceID))
if err != nil {
api.Logger.Error(ctx, "could not get workspace", slog.Error(err))
}
return workspace.Deleted
default:
return false
}
}

func (api *API) auditLogResourceLink(ctx context.Context, alog database.GetAuditLogsOffsetRow) string {
switch alog.ResourceType {
case database.ResourceTypeTemplate:
if api.auditLogIsResourceDeleted(ctx, alog) {
return ""
}
return fmt.Sprintf("/templates/%s",
alog.ResourceTarget)
case database.ResourceTypeUser:
if api.auditLogIsResourceDeleted(ctx, alog) {
return ""
}
return fmt.Sprintf("/users?filter=%s",
alog.ResourceTarget)
case database.ResourceTypeWorkspace:
if api.auditLogIsResourceDeleted(ctx, alog) {
return ""
}
return fmt.Sprintf("/@%s/%s",
alog.UserUsername.String, alog.ResourceTarget)
case database.ResourceTypeWorkspaceBuild:
if api.auditLogIsResourceDeleted(ctx, alog) {
return ""
}
additionalFieldsBytes := []byte(alog.AdditionalFields)
var additionalFields AdditionalFields
err := json.Unmarshal(additionalFieldsBytes, &additionalFields)
if err != nil {
api.Logger.Error(ctx, "could not unmarshal workspace name for friendly string", slog.Error(err))
api.Logger.Error(ctx, "could not unmarshal workspace name", slog.Error(err))
}
return fmt.Sprintf("/@%s/%s/builds/%s",
alog.UserUsername.String, additionalFields.WorkspaceName, additionalFields.BuildNumber)
case database.ResourceTypeGroup:
return fmt.Sprintf("/groups/%s",
alog.ResourceID)
default:
return ""
}
Expand All @@ -243,21 +295,27 @@ func auditLogDescription(alog database.GetAuditLogsOffsetRow) string {
codersdk.AuditAction(alog.Action).FriendlyString(),
)

// Strings for workspace_builds follow the below format:
// "{user} started workspace build for {target}"
// where target is a workspace instead of the workspace build,
// Strings for starting/stopping workspace builds follow the below format:
// "{user} started build for workspace {target}"
// 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
if alog.ResourceType != database.ResourceTypeWorkspaceBuild {
str += fmt.Sprintf(" %s",
codersdk.ResourceType(alog.ResourceType).FriendlyString())
if alog.ResourceType == database.ResourceTypeWorkspaceBuild && alog.Action != database.AuditActionDelete {
str += " build for"
}

// We don't display the name 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
// make too much sense to display.
if alog.ResourceType != database.ResourceTypeGitSshKey {
str += " {target}"
if alog.ResourceType == database.ResourceTypeGitSshKey {
str += fmt.Sprintf(" the %s",
codersdk.ResourceType(alog.ResourceType).FriendlyString())
return str
}

str += fmt.Sprintf(" %s",
codersdk.ResourceType(alog.ResourceType).FriendlyString())

str += " {target}"

return str
}

Expand Down
12 changes: 8 additions & 4 deletions coderd/provisionerdserver/provisionerdserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -538,10 +538,12 @@ func (server *Server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*p
if getWorkspaceErr != nil {
server.Logger.Error(ctx, "failed to create audit log - get workspace err", slog.Error(err))
} else {
// We pass the workspace name to the Auditor so that it
// can form a friendly string for the user.
fmt.Println("Hello World! 1", workspace.ID.String())
// We pass the below information to the Auditor so that it
// can form a friendly string for the user to view in the UI.
workspaceResourceInfo := map[string]string{
"workspaceName": workspace.Name,
"workspaceId": workspace.ID.String(),
"buildNumber": strconv.FormatInt(int64(build.BuildNumber), 10),
}

Expand Down Expand Up @@ -753,11 +755,13 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete
if getWorkspaceError == nil {
auditor := server.Auditor.Load()
auditAction := auditActionFromTransition(workspaceBuild.Transition)
fmt.Println("Hello World! 2", workspace.ID.String())

// We pass the workspace name to the Auditor so that it
// can form a friendly string for the user.
// We pass the below information to the Auditor so that it
// can form a friendly string for the user to view in the UI.
workspaceResourceInfo := map[string]string{
"workspaceName": workspace.Name,
"workspaceId": workspace.ID.String(),
"buildNumber": strconv.FormatInt(int64(workspaceBuild.BuildNumber), 10),
}

Expand Down
4 changes: 3 additions & 1 deletion codersdk/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ func (r ResourceType) FriendlyString() string {
case ResourceTypeWorkspace:
return "workspace"
case ResourceTypeWorkspaceBuild:
return "workspace build"
// workspace builds have a unique friendly string
// see coderd/audit.go:298 for explanation
return "workspace"
case ResourceTypeGitSSHKey:
return "git ssh key"
case ResourceTypeAPIKey:
Expand Down
46 changes: 46 additions & 0 deletions site/src/components/AuditLogRow/AuditLogDescription.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { FC } from "react"
import { AuditLog } from "api/typesGenerated"
import { Link as RouterLink } from "react-router-dom"
import Link from "@material-ui/core/Link"

export const AuditLogDescription: FC<{ auditLog: AuditLog }> = ({
auditLog,
}): JSX.Element => {
let target = auditLog.resource_target.trim()

// audit logs with a resource_type of workspace build use workspace name as a target
if (
auditLog.resource_type === "workspace_build" &&
auditLog.additional_fields.workspaceName
) {
target = auditLog.additional_fields.workspaceName.trim()
}

// SSH key entries have no links
if (auditLog.resource_type === "git_ssh_key") {
return (
<span>
{auditLog.description
.replace("{user}", `${auditLog.user?.username.trim()}`)
.replace("{target}", `${target}`)}
</span>
)
}

const truncatedDescription = auditLog.description
.replace("{user}", `${auditLog.user?.username.trim()}`)
.replace("{target}", "")

return (
<span>
{truncatedDescription}
{auditLog.resource_link ? (
<Link component={RouterLink} to={auditLog.resource_link}>
<strong>{target}</strong>
</Link>
) : (
<strong>{target}</strong>
)}
</span>
)
}
42 changes: 2 additions & 40 deletions site/src/components/AuditLogRow/AuditLogRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,8 @@ import { PaletteIndex } from "theme/palettes"
import userAgentParser from "ua-parser-js"
import { combineClasses } from "util/combineClasses"
import { AuditLogDiff } from "./AuditLogDiff"
import { Link as RouterLink } from "react-router-dom"
import i18next from "i18next"
import Link from "@material-ui/core/Link"

const determineResourceLink = (auditLog: AuditLog): JSX.Element | undefined => {
const { t } = i18next
const linkTarget = auditLog.resource_target.trim()

if (auditLog.resource_type === "workspace_build") {
return <>{t("auditLog:table.logRow.buildTarget")}</>
} else if (auditLog.resource_type === "git_ssh_key") {
return
} else {
return <strong>{linkTarget}</strong>
}
}

export const readableActionMessage = (auditLog: AuditLog): string => {
return auditLog.description
.replace("{user}", `${auditLog.user?.username.trim()}`)
.replace("{target}", "")
}
import { AuditLogDescription } from "./AuditLogDescription"

const httpStatusColor = (httpStatus: number): PaletteIndex => {
if (httpStatus >= 300 && httpStatus < 500) {
Expand Down Expand Up @@ -125,25 +105,7 @@ export const AuditLogRow: React.FC<AuditLogRowProps> = ({
alignItems="baseline"
spacing={1}
>
<span>
{readableActionMessage(auditLog)}{" "}
{auditLog.resource_link ? (
<Link component={RouterLink} to={auditLog.resource_link}>
{determineResourceLink(auditLog)}
</Link>
) : (
<strong>{determineResourceLink(auditLog)}</strong>
)}
{auditLog.resource_type === "workspace_build" &&
auditLog.additional_fields.workspaceName && (
<>
{t("auditLog:table.logRow.buildFriendlyString")}
<strong>
{auditLog.additional_fields.workspaceName}
</strong>
</>
)}
</span>
<AuditLogDescription auditLog={auditLog} />
<span className={styles.auditLogTime}>
{new Date(auditLog.time).toLocaleTimeString()}
</span>
Expand Down
2 changes: 0 additions & 2 deletions site/src/i18n/en/auditLog.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
"emptyPage": "No audit logs available on this page",
"noLogs": "No audit logs available",
"logRow": {
"buildTarget": "build",
"buildFriendlyString": " for workspace ",
"ip": "IP: ",
"os": "OS: ",
"browser": "Browser: ",
Expand Down