mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
chore: add audit log tests (#4764)
* added test for stopping a workspace build * formatted sfriendly string; added tests * logging unmarshal error in auditLogDescription * prettier * got rid of extra workspace word * PR feedback * fixed mistake; wrote tests in penance * fix be
This commit is contained in:
@ -219,24 +219,18 @@ func convertAuditLog(dblog database.GetAuditLogsOffsetRow) codersdk.AuditLog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkspaceResourceInfo struct {
|
|
||||||
WorkspaceName string
|
|
||||||
}
|
|
||||||
|
|
||||||
func auditLogDescription(alog database.GetAuditLogsOffsetRow) string {
|
func auditLogDescription(alog database.GetAuditLogsOffsetRow) string {
|
||||||
str := fmt.Sprintf("{user} %s %s",
|
str := fmt.Sprintf("{user} %s %s",
|
||||||
codersdk.AuditAction(alog.Action).FriendlyString(),
|
codersdk.AuditAction(alog.Action).FriendlyString(),
|
||||||
codersdk.ResourceType(alog.ResourceType).FriendlyString(),
|
codersdk.ResourceType(alog.ResourceType).FriendlyString(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Strings for build updates follow the below format:
|
// Strings for workspace_builds follow the below format:
|
||||||
// "{user} started workspace build for workspace {target}"
|
// "{user} started workspace build for {target}"
|
||||||
// where target is a workspace instead of the workspace build
|
// where target is a workspace instead of the workspace build,
|
||||||
|
// passed in on the FE via AuditLog.AdditionalFields rather than derived in request.go:35
|
||||||
if alog.ResourceType == database.ResourceTypeWorkspaceBuild {
|
if alog.ResourceType == database.ResourceTypeWorkspaceBuild {
|
||||||
workspaceBytes := []byte(alog.AdditionalFields)
|
str += " for"
|
||||||
var workspaceResourceInfo WorkspaceResourceInfo
|
|
||||||
_ = json.Unmarshal(workspaceBytes, &workspaceResourceInfo)
|
|
||||||
str += " for workspace " + workspaceResourceInfo.WorkspaceName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't display the name for git ssh keys. It's fairly long and doesn't
|
// We don't display the name for git ssh keys. It's fairly long and doesn't
|
||||||
|
@ -46,7 +46,7 @@ func ResourceTarget[T Auditable](tgt T) string {
|
|||||||
return typed.Name
|
return typed.Name
|
||||||
case database.WorkspaceBuild:
|
case database.WorkspaceBuild:
|
||||||
// this isn't used
|
// this isn't used
|
||||||
return string(typed.BuildNumber)
|
return ""
|
||||||
case database.GitSSHKey:
|
case database.GitSSHKey:
|
||||||
return typed.PublicKey
|
return typed.PublicKey
|
||||||
case database.Group:
|
case database.Group:
|
||||||
|
@ -536,13 +536,20 @@ func TestWorkspaceBuildStatus(t *testing.T) {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
auditor := audit.NewMock()
|
auditor := audit.NewMock()
|
||||||
|
numLogs := len(auditor.AuditLogs)
|
||||||
client, closeDaemon, api := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor})
|
client, closeDaemon, api := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor})
|
||||||
user := coderdtest.CreateFirstUser(t, client)
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
|
numLogs++ // add an audit log for user
|
||||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||||
|
numLogs++ // add an audit log for template version
|
||||||
|
|
||||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||||
closeDaemon.Close()
|
closeDaemon.Close()
|
||||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
|
numLogs++ // add an audit log for template creation
|
||||||
|
|
||||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||||
|
numLogs++ // add an audit log for workspace creation
|
||||||
|
|
||||||
// initial returned state is "pending"
|
// initial returned state is "pending"
|
||||||
require.EqualValues(t, codersdk.WorkspaceStatusPending, workspace.LatestBuild.Status)
|
require.EqualValues(t, codersdk.WorkspaceStatusPending, workspace.LatestBuild.Status)
|
||||||
@ -561,11 +568,22 @@ func TestWorkspaceBuildStatus(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.EqualValues(t, codersdk.WorkspaceStatusStopped, workspace.LatestBuild.Status)
|
require.EqualValues(t, codersdk.WorkspaceStatusStopped, workspace.LatestBuild.Status)
|
||||||
|
|
||||||
|
// assert an audit log has been created for workspace stopping
|
||||||
|
numLogs++ // add an audit log for workspace_build stop
|
||||||
|
require.Len(t, auditor.AuditLogs, numLogs)
|
||||||
|
require.Equal(t, database.AuditActionStop, auditor.AuditLogs[numLogs-1].Action)
|
||||||
|
|
||||||
_ = closeDaemon.Close()
|
_ = closeDaemon.Close()
|
||||||
// after successful cancel is "canceled"
|
// after successful cancel is "canceled"
|
||||||
build = coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStart)
|
build = coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStart)
|
||||||
err = client.CancelWorkspaceBuild(ctx, build.ID)
|
err = client.CancelWorkspaceBuild(ctx, build.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
numLogs++ // add an audit log for workspace build start
|
||||||
|
// assert an audit log has been created workspace starting
|
||||||
|
require.Len(t, auditor.AuditLogs, numLogs)
|
||||||
|
require.Equal(t, database.AuditActionStart, auditor.AuditLogs[numLogs-1].Action)
|
||||||
|
|
||||||
workspace, err = client.Workspace(ctx, workspace.ID)
|
workspace, err = client.Workspace(ctx, workspace.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.EqualValues(t, codersdk.WorkspaceStatusCanceled, workspace.LatestBuild.Status)
|
require.EqualValues(t, codersdk.WorkspaceStatusCanceled, workspace.LatestBuild.Status)
|
||||||
@ -577,8 +595,9 @@ func TestWorkspaceBuildStatus(t *testing.T) {
|
|||||||
workspace, err = client.DeletedWorkspace(ctx, workspace.ID)
|
workspace, err = client.DeletedWorkspace(ctx, workspace.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.EqualValues(t, codersdk.WorkspaceStatusDeleted, workspace.LatestBuild.Status)
|
require.EqualValues(t, codersdk.WorkspaceStatusDeleted, workspace.LatestBuild.Status)
|
||||||
|
numLogs++ // add an audit log for workspace build deletion
|
||||||
|
|
||||||
// assert an audit log has been created for deletion
|
// assert an audit log has been created for deletion
|
||||||
require.Len(t, auditor.AuditLogs, 7)
|
require.Len(t, auditor.AuditLogs, numLogs)
|
||||||
assert.Equal(t, database.AuditActionDelete, auditor.AuditLogs[6].Action)
|
require.Equal(t, database.AuditActionDelete, auditor.AuditLogs[numLogs-1].Action)
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ We track **create, update and delete** events for the following resources:
|
|||||||
- Template
|
- Template
|
||||||
- TemplateVersion
|
- TemplateVersion
|
||||||
- Workspace
|
- Workspace
|
||||||
|
- Workspace start/stop
|
||||||
- User
|
- User
|
||||||
- Group
|
- Group
|
||||||
|
|
||||||
|
@ -688,6 +688,8 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) {
|
|||||||
return TypescriptType{ValueType: "string", Optional: true}, nil
|
return TypescriptType{ValueType: "string", Optional: true}, nil
|
||||||
case "github.com/google/uuid.UUID":
|
case "github.com/google/uuid.UUID":
|
||||||
return TypescriptType{ValueType: "string"}, nil
|
return TypescriptType{ValueType: "string"}, nil
|
||||||
|
case "encoding/json.RawMessage":
|
||||||
|
return TypescriptType{ValueType: "Record<string, string>"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then see if the type is defined elsewhere. If it is, we can just
|
// Then see if the type is defined elsewhere. If it is, we can just
|
||||||
|
@ -65,8 +65,7 @@ export interface AuditLog {
|
|||||||
readonly action: AuditAction
|
readonly action: AuditAction
|
||||||
readonly diff: AuditDiff
|
readonly diff: AuditDiff
|
||||||
readonly status_code: number
|
readonly status_code: number
|
||||||
// This is likely an enum in an external package ("encoding/json.RawMessage")
|
readonly additional_fields: Record<string, string>
|
||||||
readonly additional_fields: string
|
|
||||||
readonly description: string
|
readonly description: string
|
||||||
readonly user?: User
|
readonly user?: User
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,11 @@ import TableContainer from "@material-ui/core/TableContainer"
|
|||||||
import TableHead from "@material-ui/core/TableHead"
|
import TableHead from "@material-ui/core/TableHead"
|
||||||
import TableRow from "@material-ui/core/TableRow"
|
import TableRow from "@material-ui/core/TableRow"
|
||||||
import { ComponentMeta, Story } from "@storybook/react"
|
import { ComponentMeta, Story } from "@storybook/react"
|
||||||
import { MockAuditLog, MockAuditLog2 } from "testHelpers/entities"
|
import {
|
||||||
|
MockAuditLog,
|
||||||
|
MockAuditLog2,
|
||||||
|
MockAuditLogWithWorkspaceBuild,
|
||||||
|
} from "testHelpers/entities"
|
||||||
import { AuditLogRow, AuditLogRowProps } from "./AuditLogRow"
|
import { AuditLogRow, AuditLogRowProps } from "./AuditLogRow"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -38,3 +42,8 @@ WithDiff.args = {
|
|||||||
auditLog: MockAuditLog2,
|
auditLog: MockAuditLog2,
|
||||||
defaultIsDiffOpen: true,
|
defaultIsDiffOpen: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const WithWorkspaceBuild = Template.bind({})
|
||||||
|
WithWorkspaceBuild.args = {
|
||||||
|
auditLog: MockAuditLogWithWorkspaceBuild,
|
||||||
|
}
|
||||||
|
41
site/src/components/AuditLogRow/AuditLogRow.test.tsx
Normal file
41
site/src/components/AuditLogRow/AuditLogRow.test.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { readableActionMessage } from "./AuditLogRow"
|
||||||
|
import {
|
||||||
|
MockAuditLog,
|
||||||
|
MockAuditLogWithWorkspaceBuild,
|
||||||
|
} from "testHelpers/entities"
|
||||||
|
|
||||||
|
describe("readableActionMessage()", () => {
|
||||||
|
it("renders the correct string for a workspaceBuild audit log", async () => {
|
||||||
|
// When
|
||||||
|
const friendlyString = readableActionMessage(MockAuditLogWithWorkspaceBuild)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(friendlyString).toBe(
|
||||||
|
"<strong>TestUser</strong> stopped workspace build for <strong>test2</strong>",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it("renders the correct string for a workspaceBuild audit log with a duplicate word", async () => {
|
||||||
|
// When
|
||||||
|
const AuditLogWithRepeat = {
|
||||||
|
...MockAuditLogWithWorkspaceBuild,
|
||||||
|
additional_fields: {
|
||||||
|
workspaceName: "workspace",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const friendlyString = readableActionMessage(AuditLogWithRepeat)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(friendlyString).toBe(
|
||||||
|
"<strong>TestUser</strong> stopped workspace build for <strong>workspace</strong>",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it("renders the correct string for a workspace audit log", async () => {
|
||||||
|
// When
|
||||||
|
const friendlyString = readableActionMessage(MockAuditLog)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(friendlyString).toBe(
|
||||||
|
"<strong>TestUser</strong> updated workspace <strong>bruno-dev</strong>",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
@ -16,10 +16,17 @@ import userAgentParser from "ua-parser-js"
|
|||||||
import { combineClasses } from "util/combineClasses"
|
import { combineClasses } from "util/combineClasses"
|
||||||
import { AuditLogDiff } from "./AuditLogDiff"
|
import { AuditLogDiff } from "./AuditLogDiff"
|
||||||
|
|
||||||
const readableActionMessage = (auditLog: AuditLog) => {
|
export const readableActionMessage = (auditLog: AuditLog): string => {
|
||||||
|
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") {
|
||||||
|
target = auditLog.additional_fields.workspaceName.trim()
|
||||||
|
}
|
||||||
|
|
||||||
return auditLog.description
|
return auditLog.description
|
||||||
.replace("{user}", `<strong>${auditLog.user?.username.trim()}</strong>`)
|
.replace("{user}", `<strong>${auditLog.user?.username.trim()}</strong>`)
|
||||||
.replace("{target}", `<strong>${auditLog.resource_target.trim()}</strong>`)
|
.replace("{target}", `<strong>${target}</strong>`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const httpStatusColor = (httpStatus: number): PaletteIndex => {
|
const httpStatusColor = (httpStatus: number): PaletteIndex => {
|
||||||
|
@ -916,7 +916,7 @@ export const MockAuditLog: TypesGen.AuditLog = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
status_code: 200,
|
status_code: 200,
|
||||||
additional_fields: "",
|
additional_fields: {},
|
||||||
description: "{user} updated workspace {target}",
|
description: "{user} updated workspace {target}",
|
||||||
user: MockUser,
|
user: MockUser,
|
||||||
}
|
}
|
||||||
@ -949,6 +949,18 @@ export const MockAuditLog2: TypesGen.AuditLog = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const MockAuditLogWithWorkspaceBuild: TypesGen.AuditLog = {
|
||||||
|
...MockAuditLog,
|
||||||
|
id: "f90995bf-4a2b-4089-b597-e66e025e523e",
|
||||||
|
request_id: "61555889-2875-475c-8494-f7693dd5d75b",
|
||||||
|
action: "stop",
|
||||||
|
resource_type: "workspace_build",
|
||||||
|
description: "{user} stopped workspace build for {target}",
|
||||||
|
additional_fields: {
|
||||||
|
workspaceName: "test2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export const MockWorkspaceQuota: TypesGen.WorkspaceQuota = {
|
export const MockWorkspaceQuota: TypesGen.WorkspaceQuota = {
|
||||||
user_workspace_count: 0,
|
user_workspace_count: 0,
|
||||||
user_workspace_limit: 100,
|
user_workspace_limit: 100,
|
||||||
|
Reference in New Issue
Block a user