adding workspace_build resource (#4636)

* adding workspace_build resource

* added migration

* added migration for audit_actions

* fix keyword

* got rid oof diffs for workspace builds

* adding workspace name to string

* renamed migrations

* fixed lint

* pass throough AdditionalFields and fix tests

* no need to pass through each handler

* cleaned up migrations
This commit is contained in:
Kira Pilot 2022-10-25 09:27:50 -04:00 committed by GitHub
parent 3e08bb4842
commit 145faf4400
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 126 additions and 31 deletions

View File

@ -219,12 +219,26 @@ func convertAuditLog(dblog database.GetAuditLogsOffsetRow) codersdk.AuditLog {
}
}
type WorkspaceResourceInfo struct {
WorkspaceName string
}
func auditLogDescription(alog database.GetAuditLogsOffsetRow) string {
str := fmt.Sprintf("{user} %s %s",
codersdk.AuditAction(alog.Action).FriendlyString(),
codersdk.ResourceType(alog.ResourceType).FriendlyString(),
)
// Strings for build updates follow the below format:
// "{user} started workspace build for workspace {target}"
// where target is a workspace instead of the workspace build
if alog.ResourceType == database.ResourceTypeWorkspaceBuild {
workspaceBytes := []byte(alog.AdditionalFields)
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
// make too much sense to display.
if alog.ResourceType != database.ResourceTypeGitSshKey {
@ -288,6 +302,8 @@ func resourceTypeFromString(resourceTypeString string) string {
return resourceTypeString
case codersdk.ResourceTypeWorkspace:
return resourceTypeString
case codersdk.ResourceTypeWorkspaceBuild:
return resourceTypeString
case codersdk.ResourceTypeGitSSHKey:
return resourceTypeString
case codersdk.ResourceTypeAPIKey:

View File

@ -15,6 +15,7 @@ type Auditable interface {
database.TemplateVersion |
database.User |
database.Workspace |
database.WorkspaceBuild |
database.GitSSHKey |
database.Group
}

View File

@ -20,8 +20,9 @@ type RequestParams struct {
Audit Auditor
Log slog.Logger
Request *http.Request
Action database.AuditAction
Request *http.Request
Action database.AuditAction
AdditionalFields json.RawMessage
}
type Request[T Auditable] struct {
@ -43,6 +44,9 @@ func ResourceTarget[T Auditable](tgt T) string {
return typed.Username
case database.Workspace:
return typed.Name
case database.WorkspaceBuild:
// this isn't used
return string(typed.BuildNumber)
case database.GitSSHKey:
return typed.PublicKey
case database.Group:
@ -64,6 +68,8 @@ func ResourceID[T Auditable](tgt T) uuid.UUID {
return typed.ID
case database.Workspace:
return typed.ID
case database.WorkspaceBuild:
return typed.ID
case database.GitSSHKey:
return typed.UserID
case database.Group:
@ -85,6 +91,8 @@ func ResourceType[T Auditable](tgt T) database.ResourceType {
return database.ResourceTypeUser
case database.Workspace:
return database.ResourceTypeWorkspace
case database.WorkspaceBuild:
return database.ResourceTypeWorkspaceBuild
case database.GitSSHKey:
return database.ResourceTypeGitSshKey
case database.Group:
@ -129,6 +137,10 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request
}
}
if p.AdditionalFields == nil {
p.AdditionalFields = json.RawMessage("{}")
}
ip := parseIP(p.Request.RemoteAddr)
err := p.Audit.Export(ctx, database.AuditLog{
ID: uuid.New(),
@ -143,7 +155,7 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request
Diff: diffRaw,
StatusCode: int32(sw.Status),
RequestID: httpmw.RequestID(p.Request),
AdditionalFields: json.RawMessage("{}"),
AdditionalFields: p.AdditionalFields,
})
if err != nil {
p.Log.Error(logCtx, "export audit log", slog.Error(err))

View File

@ -14,7 +14,9 @@ CREATE TYPE app_sharing_level AS ENUM (
CREATE TYPE audit_action AS ENUM (
'create',
'write',
'delete'
'delete',
'start',
'stop'
);
CREATE TYPE build_reason AS ENUM (
@ -88,7 +90,8 @@ CREATE TYPE resource_type AS ENUM (
'workspace',
'git_ssh_key',
'api_key',
'group'
'group',
'workspace_build'
);
CREATE TYPE user_status AS ENUM (

View File

@ -0,0 +1,2 @@
-- It's not possible to drop enum values from enum types, so the UP has "IF NOT
-- EXISTS".

View File

@ -0,0 +1,4 @@
ALTER TYPE audit_action ADD VALUE IF NOT EXISTS 'start';
ALTER TYPE audit_action ADD VALUE IF NOT EXISTS 'stop';
ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'workspace_build';

View File

@ -60,6 +60,8 @@ const (
AuditActionCreate AuditAction = "create"
AuditActionWrite AuditAction = "write"
AuditActionDelete AuditAction = "delete"
AuditActionStart AuditAction = "start"
AuditActionStop AuditAction = "stop"
)
func (e *AuditAction) Scan(src interface{}) error {
@ -302,6 +304,7 @@ const (
ResourceTypeGitSshKey ResourceType = "git_ssh_key"
ResourceTypeApiKey ResourceType = "api_key"
ResourceTypeGroup ResourceType = "group"
ResourceTypeWorkspaceBuild ResourceType = "workspace_build"
)
func (e *ResourceType) Scan(src interface{}) error {

View File

@ -278,28 +278,59 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
return
}
// we only want to create audit logs for delete builds right now
auditor := api.Auditor.Load()
// if user deletes a workspace, audit the workspace
if action == rbac.ActionDelete {
var (
auditor = api.Auditor.Load()
aReq, commitAudit = audit.InitRequest[database.Workspace](rw, &audit.RequestParams{
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionDelete,
})
)
aReq, commitAudit := audit.InitRequest[database.Workspace](rw, &audit.RequestParams{
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionDelete,
})
defer commitAudit()
aReq.Old = workspace
}
latestBuild, latestBuildErr := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
// if a user starts/stops a workspace, audit the workspace build
if action == rbac.ActionUpdate {
var auditAction database.AuditAction
if createBuild.Transition == codersdk.WorkspaceTransitionStart {
auditAction = database.AuditActionStart
} else if createBuild.Transition == codersdk.WorkspaceTransitionStop {
auditAction = database.AuditActionStop
} else {
auditAction = database.AuditActionWrite
}
// We pass the workspace name to the Auditor so that it
// can form a friendly string for the user.
workspaceResourceInfo := map[string]string{
"workspaceName": workspace.Name,
}
wriBytes, _ := json.Marshal(workspaceResourceInfo)
aReq, commitAudit := audit.InitRequest[database.WorkspaceBuild](rw, &audit.RequestParams{
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: auditAction,
AdditionalFields: wriBytes,
})
defer commitAudit()
aReq.Old = latestBuild
}
if createBuild.TemplateVersionID == uuid.Nil {
latestBuild, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
if err != nil {
if latestBuildErr != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching the latest workspace build.",
Detail: err.Error(),
Detail: latestBuildErr.Error(),
})
return
}

View File

@ -579,6 +579,6 @@ func TestWorkspaceBuildStatus(t *testing.T) {
require.EqualValues(t, codersdk.WorkspaceStatusDeleted, workspace.LatestBuild.Status)
// assert an audit log has been created for deletion
require.Len(t, auditor.AuditLogs, 5)
assert.Equal(t, database.AuditActionDelete, auditor.AuditLogs[4].Action)
require.Len(t, auditor.AuditLogs, 7)
assert.Equal(t, database.AuditActionDelete, auditor.AuditLogs[6].Action)
}

View File

@ -19,6 +19,7 @@ const (
ResourceTypeTemplateVersion ResourceType = "template_version"
ResourceTypeUser ResourceType = "user"
ResourceTypeWorkspace ResourceType = "workspace"
ResourceTypeWorkspaceBuild ResourceType = "workspace_build"
ResourceTypeGitSSHKey ResourceType = "git_ssh_key"
ResourceTypeAPIKey ResourceType = "api_key"
ResourceTypeGroup ResourceType = "group"
@ -36,6 +37,8 @@ func (r ResourceType) FriendlyString() string {
return "user"
case ResourceTypeWorkspace:
return "workspace"
case ResourceTypeWorkspaceBuild:
return "workspace build"
case ResourceTypeGitSSHKey:
return "git ssh key"
case ResourceTypeAPIKey:
@ -53,6 +56,8 @@ const (
AuditActionCreate AuditAction = "create"
AuditActionWrite AuditAction = "write"
AuditActionDelete AuditAction = "delete"
AuditActionStart AuditAction = "start"
AuditActionStop AuditAction = "stop"
)
func (a AuditAction) FriendlyString() string {
@ -63,6 +68,10 @@ func (a AuditAction) FriendlyString() string {
return "updated"
case AuditActionDelete:
return "deleted"
case AuditActionStart:
return "started"
case AuditActionStop:
return "stopped"
default:
return "unknown"
}

View File

@ -103,6 +103,21 @@ var AuditableResources = auditMap(map[any]map[string]Action{
"ttl": ActionTrack,
"last_used_at": ActionIgnore,
},
// We don't show any diff for the WorkspaceBuild resource
&database.WorkspaceBuild{}: {
"id": ActionIgnore,
"created_at": ActionIgnore,
"updated_at": ActionIgnore,
"workspace_id": ActionIgnore,
"template_version_id": ActionIgnore,
"build_number": ActionIgnore,
"transition": ActionIgnore,
"initiator_id": ActionIgnore,
"provisioner_state": ActionIgnore,
"job_id": ActionIgnore,
"deadline": ActionIgnore,
"reason": ActionIgnore,
},
&database.Group{}: {
"id": ActionTrack,
"name": ActionTrack,

View File

@ -915,7 +915,7 @@ export interface WorkspacesRequest extends Pagination {
export type APIKeyScope = "all" | "application_connect"
// From codersdk/audit.go
export type AuditAction = "create" | "delete" | "write"
export type AuditAction = "create" | "delete" | "start" | "stop" | "write"
// From codersdk/workspacebuilds.go
export type BuildReason = "autostart" | "autostop" | "initiator"
@ -975,6 +975,7 @@ export type ResourceType =
| "template_version"
| "user"
| "workspace"
| "workspace_build"
// From codersdk/sse.go
export type ServerSentEventType = "data" | "error" | "ping"

View File

@ -130,13 +130,11 @@ export const AuditLogRow: React.FC<AuditLogRowProps> = ({
</Stack>
</Stack>
<div
className={
shouldDisplayDiff ? undefined : styles.disabledDropdownIcon
}
>
{isDiffOpen ? <CloseDropdown /> : <OpenDropdown />}
</div>
{shouldDisplayDiff ? (
<div> {isDiffOpen ? <CloseDropdown /> : <OpenDropdown />}</div>
) : (
<div className={styles.columnWithoutDiff}></div>
)}
</Stack>
{shouldDisplayDiff && (
@ -190,8 +188,8 @@ const useStyles = makeStyles((theme) => ({
color: theme.palette.text.secondary,
whiteSpace: "nowrap",
},
disabledDropdownIcon: {
opacity: 0.5,
// offset the absence of the arrow icon on diff-less logs
columnWithoutDiff: {
marginLeft: "24px",
},
}))