coder/enterprise/audit/table.go

297 lines
11 KiB
Go

package audit
import (
"fmt"
"os"
"reflect"
"runtime"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/codersdk"
)
// This mapping creates a relationship between an Auditable Resource
// and the Audit Actions we track for that resource.
// It is important to maintain this mapping when adding a new Auditable Resource to the
// AuditableResources map (below) as our documentation - generated in scripts/auditdocgen/main.go -
// depends upon it.
var AuditActionMap = map[string][]codersdk.AuditAction{
"GitSSHKey": {codersdk.AuditActionCreate},
"Template": {codersdk.AuditActionWrite, codersdk.AuditActionDelete},
"TemplateVersion": {codersdk.AuditActionCreate, codersdk.AuditActionWrite},
"User": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
"Workspace": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
"WorkspaceBuild": {codersdk.AuditActionStart, codersdk.AuditActionStop},
"Group": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
"APIKey": {codersdk.AuditActionLogin, codersdk.AuditActionLogout, codersdk.AuditActionRegister, codersdk.AuditActionCreate, codersdk.AuditActionDelete},
"License": {codersdk.AuditActionCreate, codersdk.AuditActionDelete},
}
type Action string
const (
// ActionIgnore ignores diffing for the field.
ActionIgnore = "ignore"
// ActionTrack includes the value in the diff if the value changed.
ActionTrack = "track"
// ActionSecret includes a zero value of the same type if the value changed.
// It lets you indicate that a value changed, but without leaking its
// contents.
ActionSecret = "secret"
)
// Table is a map of struct names to a map of field names that indicate that
// field's AuditType.
type Table map[string]map[string]Action
// AuditableResources contains a definitive list of all auditable resources and
// which fields are auditable. All resource types must be valid audit.Auditable
// types.
var AuditableResources = auditMap(auditableResourcesTypes)
var auditableResourcesTypes = map[any]map[string]Action{
&database.GitSSHKey{}: {
"user_id": ActionTrack,
"created_at": ActionIgnore, // Never changes, but is implicit and not helpful in a diff.
"updated_at": ActionIgnore, // Changes, but is implicit and not helpful in a diff.
"private_key": ActionSecret, // We don't want to expose private keys in diffs.
"public_key": ActionTrack, // Public keys are ok to expose in a diff.
},
&database.Template{}: {
"id": ActionTrack,
"created_at": ActionIgnore, // Never changes, but is implicit and not helpful in a diff.
"updated_at": ActionIgnore, // Changes, but is implicit and not helpful in a diff.
"organization_id": ActionIgnore, /// Never changes.
"deleted": ActionIgnore, // Changes, but is implicit when a delete event is fired.
"name": ActionTrack,
"display_name": ActionTrack,
"provisioner": ActionTrack,
"active_version_id": ActionTrack,
"description": ActionTrack,
"icon": ActionTrack,
"default_ttl": ActionTrack,
"max_ttl": ActionTrack,
"use_max_ttl": ActionTrack,
"autostart_block_days_of_week": ActionTrack,
"autostop_requirement_days_of_week": ActionTrack,
"autostop_requirement_weeks": ActionTrack,
"created_by": ActionTrack,
"created_by_username": ActionIgnore,
"created_by_avatar_url": ActionIgnore,
"group_acl": ActionTrack,
"user_acl": ActionTrack,
"allow_user_autostart": ActionTrack,
"allow_user_autostop": ActionTrack,
"allow_user_cancel_workspace_jobs": ActionTrack,
"failure_ttl": ActionTrack,
"time_til_dormant": ActionTrack,
"time_til_dormant_autodelete": ActionTrack,
"require_active_version": ActionTrack,
"deprecated": ActionTrack,
"max_port_sharing_level": ActionTrack,
"activity_bump": ActionTrack,
},
&database.TemplateVersion{}: {
"id": ActionTrack,
"template_id": ActionTrack,
"organization_id": ActionIgnore, // Never changes.
"created_at": ActionIgnore, // Never changes, but is implicit and not helpful in a diff.
"updated_at": ActionIgnore, // Changes, but is implicit and not helpful in a diff.
"name": ActionTrack,
"message": ActionIgnore, // Never changes after creation.
"readme": ActionTrack,
"job_id": ActionIgnore, // Not helpful in a diff because jobs aren't tracked in audit logs.
"created_by": ActionTrack,
"external_auth_providers": ActionIgnore, // Not helpful because this can only change when new versions are added.
"created_by_avatar_url": ActionIgnore,
"created_by_username": ActionIgnore,
"archived": ActionTrack,
},
&database.User{}: {
"id": ActionTrack,
"email": ActionTrack,
"username": ActionTrack,
"hashed_password": ActionSecret, // Do not expose a users hashed password.
"created_at": ActionIgnore, // Never changes.
"updated_at": ActionIgnore, // Changes, but is implicit and not helpful in a diff.
"status": ActionTrack,
"rbac_roles": ActionTrack,
"login_type": ActionTrack,
"avatar_url": ActionIgnore,
"last_seen_at": ActionIgnore,
"deleted": ActionTrack,
"quiet_hours_schedule": ActionTrack,
"theme_preference": ActionIgnore,
"name": ActionTrack,
},
&database.Workspace{}: {
"id": ActionTrack,
"created_at": ActionIgnore, // Never changes.
"updated_at": ActionIgnore, // Changes, but is implicit and not helpful in a diff.
"owner_id": ActionTrack,
"organization_id": ActionIgnore, // Never changes.
"template_id": ActionTrack,
"deleted": ActionIgnore, // Changes, but is implicit when a delete event is fired.
"name": ActionTrack,
"autostart_schedule": ActionTrack,
"ttl": ActionTrack,
"last_used_at": ActionIgnore,
"dormant_at": ActionTrack,
"deleting_at": ActionTrack,
"automatic_updates": ActionTrack,
"favorite": ActionTrack,
},
&database.WorkspaceBuild{}: {
"id": ActionIgnore,
"created_at": ActionIgnore,
"updated_at": ActionIgnore,
"workspace_id": ActionIgnore,
"template_version_id": ActionTrack,
"build_number": ActionIgnore,
"transition": ActionIgnore,
"initiator_id": ActionIgnore,
"provisioner_state": ActionIgnore,
"job_id": ActionIgnore,
"deadline": ActionIgnore,
"reason": ActionIgnore,
"daily_cost": ActionIgnore,
"max_deadline": ActionIgnore,
"initiator_by_avatar_url": ActionIgnore,
"initiator_by_username": ActionIgnore,
},
&database.AuditableGroup{}: {
"id": ActionTrack,
"name": ActionTrack,
"display_name": ActionTrack,
"organization_id": ActionIgnore, // Never changes.
"avatar_url": ActionTrack,
"quota_allowance": ActionTrack,
"members": ActionTrack,
"source": ActionIgnore,
},
&database.APIKey{}: {
"id": ActionIgnore,
"hashed_secret": ActionIgnore,
"user_id": ActionTrack,
"last_used": ActionTrack,
"expires_at": ActionTrack,
"created_at": ActionTrack,
"updated_at": ActionIgnore,
"login_type": ActionIgnore,
"lifetime_seconds": ActionIgnore,
"ip_address": ActionIgnore,
"scope": ActionIgnore,
"token_name": ActionIgnore,
},
&database.AuditOAuthConvertState{}: {
"created_at": ActionTrack,
"expires_at": ActionTrack,
"from_login_type": ActionTrack,
"to_login_type": ActionTrack,
"user_id": ActionTrack,
},
&database.HealthSettings{}: {
"id": ActionIgnore,
"dismissed_healthchecks": ActionTrack,
},
// TODO: track an ID here when the below ticket is completed:
// https://github.com/coder/coder/pull/6012
&database.License{}: {
"id": ActionIgnore,
"uploaded_at": ActionTrack,
"jwt": ActionIgnore,
"exp": ActionTrack,
"uuid": ActionTrack,
},
&database.WorkspaceProxy{}: {
"id": ActionTrack,
"name": ActionTrack,
"display_name": ActionTrack,
"icon": ActionTrack,
"url": ActionTrack,
"wildcard_hostname": ActionTrack,
"created_at": ActionTrack,
"updated_at": ActionIgnore,
"deleted": ActionIgnore,
"token_hashed_secret": ActionSecret,
"derp_enabled": ActionTrack,
"derp_only": ActionTrack,
"region_id": ActionTrack,
"version": ActionTrack,
},
}
// auditMap converts a map of struct pointers to a map of struct names as
// strings. It's a convenience wrapper so that structs can be passed in by value
// instead of manually typing struct names as strings.
func auditMap(m map[any]map[string]Action) Table {
out := make(Table, len(m))
for k, v := range m {
tableKey, tableValue := entry(k, v)
out[tableKey] = tableValue
}
return out
}
// entry is a helper function that checks the json tags to make sure all fields
// are tracked. And no excess fields are tracked.
func entry(v any, f map[string]Action) (string, map[string]Action) {
vt := reflect.TypeOf(v)
for vt.Kind() == reflect.Ptr {
vt = vt.Elem()
}
// This should never happen because audit.Audible only allows structs in
// its union.
if vt.Kind() != reflect.Struct {
panic(fmt.Sprintf("audit table entry value must be a struct, got %T", v))
}
name := structName(vt)
// Use the flattenStructFields to recurse anonymously embedded structs
vv := reflect.ValueOf(v)
diffs, err := flattenStructFields(vv, vv)
if err != nil {
panic(fmt.Sprintf("audit table entry type %T failed to flatten", v))
}
fcpy := make(map[string]Action, len(f))
for k, v := range f {
fcpy[k] = v
}
for _, d := range diffs {
jsonTag := d.FieldType.Tag.Get("json")
if jsonTag == "-" {
// This field is explicitly ignored.
continue
}
if _, ok := fcpy[jsonTag]; !ok {
_, _ = fmt.Fprintf(os.Stderr, "ERROR: Audit table entry missing action for field %q in type %q\nPlease update the auditable resource types in: %s\n", d.FieldType.Name, name, self())
//nolint:revive
os.Exit(1)
}
delete(fcpy, jsonTag)
}
// If there are any fields left in fcpy, they are extra fields that don't
// exist in the struct. Don't track them.
if len(fcpy) > 0 {
panic(fmt.Sprintf("audit table entry has extra actions for type %q: %v", name, fcpy))
}
return structName(vt), f
}
func (t Action) String() string {
return string(t)
}
func self() string {
//nolint:dogsled
_, file, _, _ := runtime.Caller(1)
return file
}