mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
170c33a475
Adds an optional dbcrypt wrapper around gitsshkeys.private_key. The column is encrypted on insert and update through enterprise/dbcrypt when external token encryption is configured, and decrypted on read. A new private_key_key_id column references dbcrypt_keys(active_key_digest) so revocation safety is enforced by the existing foreign key. Rows with a NULL key_id stay plaintext and remain readable. Existing plaintext rows can be backfilled by running `coder server dbcrypt rotate`. Generated with assistance from Coder Agents.
559 lines
23 KiB
Go
559 lines
23 KiB
Go
package audit
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/idpsync"
|
|
"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.AuditActionWrite, codersdk.AuditActionDelete},
|
|
"License": {codersdk.AuditActionCreate, codersdk.AuditActionDelete},
|
|
"Task": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
|
|
"AiSeatState": {codersdk.AuditActionCreate},
|
|
"AIProvider": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
|
|
"AIProviderKey": {codersdk.AuditActionCreate, codersdk.AuditActionDelete},
|
|
"AIGatewayKey": {codersdk.AuditActionCreate, codersdk.AuditActionDelete},
|
|
"AuditableGroupAiBudget": {codersdk.AuditActionWrite, codersdk.AuditActionDelete},
|
|
"Chat": {codersdk.AuditActionCreate, codersdk.AuditActionWrite}, // chats get 'archived' by users, not deleted.
|
|
"UserSecret": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
|
|
"UserSkill": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, 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.AuditableOrganizationMember{}: {
|
|
"username": ActionTrack,
|
|
"user_id": ActionTrack,
|
|
"organization_id": ActionIgnore, // Never changes.
|
|
"created_at": ActionTrack,
|
|
"updated_at": ActionTrack,
|
|
"roles": ActionTrack,
|
|
},
|
|
&database.CustomRole{}: {
|
|
"name": ActionTrack,
|
|
"display_name": ActionTrack,
|
|
"site_permissions": ActionTrack,
|
|
"org_permissions": ActionTrack,
|
|
"user_permissions": ActionTrack,
|
|
"member_permissions": ActionTrack,
|
|
"organization_id": ActionIgnore, // Never changes.
|
|
"is_system": ActionIgnore, // Never changes.
|
|
|
|
"id": ActionIgnore,
|
|
"created_at": ActionIgnore,
|
|
"updated_at": ActionIgnore,
|
|
},
|
|
&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.
|
|
"private_key_key_id": ActionIgnore, // Internal dbcrypt metadata, not useful in audit 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.
|
|
"organization_name": ActionIgnore, // Ignore these changes
|
|
"organization_display_name": ActionIgnore, // Ignore these changes
|
|
"organization_icon": ActionIgnore, // Ignore these 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,
|
|
"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_name": 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,
|
|
"use_classic_parameter_flow": ActionTrack,
|
|
"cors_behavior": ActionTrack,
|
|
"disable_module_cache": 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,
|
|
"created_by_name": ActionIgnore,
|
|
"archived": ActionTrack,
|
|
"source_example_id": ActionIgnore, // Never changes.
|
|
"has_ai_task": ActionIgnore, // Never changes.
|
|
"has_external_agent": ActionIgnore, // Never changes.
|
|
},
|
|
&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,
|
|
"name": ActionTrack,
|
|
"github_com_user_id": ActionIgnore,
|
|
"hashed_one_time_passcode": ActionIgnore,
|
|
"one_time_passcode_expires_at": ActionTrack,
|
|
"is_system": ActionTrack, // Should never change, but track it anyway.
|
|
"is_service_account": ActionTrack, // Should never change, but track it anyway.
|
|
"chat_spend_limit_micros": ActionTrack,
|
|
},
|
|
&database.WorkspaceTable{}: {
|
|
"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,
|
|
"next_start_at": ActionTrack,
|
|
"group_acl": ActionTrack,
|
|
"user_acl": 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,
|
|
"job_id": ActionIgnore,
|
|
"deadline": ActionIgnore,
|
|
"reason": ActionIgnore,
|
|
"daily_cost": ActionIgnore,
|
|
"max_deadline": ActionIgnore,
|
|
"initiator_by_avatar_url": ActionIgnore,
|
|
"initiator_by_username": ActionIgnore,
|
|
"initiator_by_name": ActionIgnore,
|
|
"template_version_preset_id": ActionIgnore, // Never changes.
|
|
"has_ai_task": ActionIgnore, // Never changes.
|
|
"has_external_agent": ActionIgnore, // Never changes.
|
|
},
|
|
&database.AuditableGroup{}: {
|
|
"id": ActionTrack,
|
|
"name": ActionTrack,
|
|
"display_name": ActionTrack,
|
|
"organization_id": ActionIgnore, // Never changes.
|
|
"avatar_url": ActionTrack,
|
|
"quota_allowance": ActionTrack,
|
|
"members": ActionTrack,
|
|
"source": ActionIgnore,
|
|
"chat_spend_limit_micros": ActionTrack,
|
|
},
|
|
&database.AuditableGroupAiBudget{}: {
|
|
"group_id": ActionIgnore, // Group name is already included in the title.
|
|
"spend_limit_micros": ActionIgnore,
|
|
"spend_limit": ActionTrack, // Track spend_limit, which is the human-readable version.
|
|
"group_name": ActionIgnore, // Group name is already included in the title.
|
|
"created_at": ActionIgnore, // Redundant with the audit log's own timestamp.
|
|
"updated_at": ActionIgnore, // Redundant with the audit log's own timestamp.
|
|
},
|
|
&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,
|
|
"scopes": ActionIgnore,
|
|
"allow_list": 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,
|
|
},
|
|
&database.NotificationsSettings{}: {
|
|
"id": ActionIgnore,
|
|
"notifier_paused": ActionTrack,
|
|
},
|
|
&database.PrebuildsSettings{}: {
|
|
"id": ActionIgnore,
|
|
"reconciliation_paused": 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,
|
|
},
|
|
&database.OAuth2ProviderApp{}: {
|
|
"id": ActionIgnore,
|
|
"created_at": ActionIgnore,
|
|
"updated_at": ActionIgnore,
|
|
"name": ActionTrack,
|
|
"icon": ActionTrack,
|
|
"callback_url": ActionTrack,
|
|
"redirect_uris": ActionTrack,
|
|
"client_type": ActionTrack,
|
|
"dynamically_registered": ActionTrack,
|
|
// RFC 7591 Dynamic Client Registration fields
|
|
"client_id_issued_at": ActionIgnore, // Timestamp, not security relevant
|
|
"client_secret_expires_at": ActionTrack, // Security relevant - expiration policy
|
|
"grant_types": ActionTrack, // Security relevant - authorization capabilities
|
|
"response_types": ActionTrack, // Security relevant - response flow types
|
|
"token_endpoint_auth_method": ActionTrack, // Security relevant - auth method
|
|
"scope": ActionTrack, // Security relevant - permissions scope
|
|
"contacts": ActionTrack, // Contact info for responsible parties
|
|
"client_uri": ActionTrack, // Client identification info
|
|
"logo_uri": ActionTrack, // Client branding
|
|
"tos_uri": ActionTrack, // Legal compliance
|
|
"policy_uri": ActionTrack, // Legal compliance
|
|
"jwks_uri": ActionTrack, // Security relevant - key location
|
|
"jwks": ActionSecret, // Security sensitive - actual keys
|
|
"software_id": ActionTrack, // Client software identification
|
|
"software_version": ActionTrack, // Client software version
|
|
// RFC 7592 Management fields - sensitive data
|
|
"registration_access_token": ActionSecret, // Secret token for client management
|
|
"registration_client_uri": ActionTrack, // Management endpoint URI
|
|
},
|
|
&database.OAuth2ProviderAppSecret{}: {
|
|
"id": ActionIgnore,
|
|
"created_at": ActionIgnore,
|
|
"last_used_at": ActionIgnore,
|
|
"hashed_secret": ActionIgnore,
|
|
"display_secret": ActionIgnore,
|
|
"app_id": ActionIgnore,
|
|
"secret_prefix": ActionIgnore,
|
|
},
|
|
&database.Organization{}: {
|
|
"id": ActionIgnore,
|
|
"name": ActionTrack,
|
|
"description": ActionTrack,
|
|
"deleted": ActionTrack,
|
|
"created_at": ActionIgnore,
|
|
"updated_at": ActionTrack,
|
|
"is_default": ActionTrack,
|
|
"display_name": ActionTrack,
|
|
"icon": ActionTrack,
|
|
"shareable_workspace_owners": ActionTrack,
|
|
},
|
|
&database.NotificationTemplate{}: {
|
|
"id": ActionIgnore,
|
|
"name": ActionTrack,
|
|
"title_template": ActionTrack,
|
|
"body_template": ActionTrack,
|
|
"actions": ActionTrack,
|
|
"group": ActionTrack,
|
|
"method": ActionTrack,
|
|
"kind": ActionTrack,
|
|
"enabled_by_default": ActionTrack,
|
|
},
|
|
&idpsync.OrganizationSyncSettings{}: {
|
|
"field": ActionTrack,
|
|
"mapping": ActionTrack,
|
|
"assign_default": ActionTrack,
|
|
},
|
|
&idpsync.GroupSyncSettings{}: {
|
|
"field": ActionTrack,
|
|
"mapping": ActionTrack,
|
|
"regex_filter": ActionTrack,
|
|
"auto_create_missing_groups": ActionTrack,
|
|
// Configured in env vars
|
|
"legacy_group_name_mapping": ActionIgnore,
|
|
},
|
|
&idpsync.RoleSyncSettings{}: {
|
|
"field": ActionTrack,
|
|
"mapping": ActionTrack,
|
|
},
|
|
&database.AiSeatState{}: {
|
|
"user_id": ActionTrack,
|
|
"first_used_at": ActionTrack,
|
|
"last_event_type": ActionTrack,
|
|
"last_event_description": ActionTrack,
|
|
|
|
// Since the audit log only fires on the first event, these fields will always
|
|
// match "first_used_at".
|
|
"last_used_at": ActionIgnore,
|
|
"updated_at": ActionIgnore,
|
|
},
|
|
&database.AIProvider{}: {
|
|
"id": ActionTrack,
|
|
"type": ActionTrack,
|
|
"name": ActionTrack,
|
|
"display_name": ActionTrack,
|
|
"enabled": ActionTrack,
|
|
"deleted": ActionTrack,
|
|
"base_url": ActionTrack,
|
|
"settings": ActionSecret, // Encrypted JSON blob may contain provider secrets (e.g. Bedrock access key + secret).
|
|
"settings_key_id": ActionIgnore, // dbcrypt key reference, derivable.
|
|
"created_at": ActionIgnore, // Implicit; not useful in a diff.
|
|
"updated_at": ActionIgnore, // Changes; not useful in a diff.
|
|
},
|
|
&database.AIProviderKey{}: {
|
|
"id": ActionTrack,
|
|
"provider_id": ActionTrack,
|
|
"api_key": ActionTrack, // Callers must pre-mask before auditing; the audit pipeline never sees plaintext.
|
|
"api_key_key_id": ActionIgnore, // dbcrypt key reference, derivable.
|
|
"created_at": ActionIgnore, // Implicit; not useful in a diff.
|
|
"updated_at": ActionIgnore, // Changes; not useful in a diff.
|
|
},
|
|
&database.AIGatewayKey{}: {
|
|
"id": ActionTrack,
|
|
"name": ActionTrack,
|
|
"secret_prefix": ActionTrack,
|
|
"hashed_secret": ActionSecret, // Bearer token hash, never expose.
|
|
"created_at": ActionIgnore, // Implicit; not useful in a diff.
|
|
"last_used_at": ActionIgnore, // Bumped on every use.
|
|
},
|
|
&database.TaskTable{}: {
|
|
"id": ActionTrack,
|
|
"organization_id": ActionIgnore, // Never changes.
|
|
"owner_id": ActionTrack,
|
|
"name": ActionTrack,
|
|
"display_name": ActionTrack,
|
|
"workspace_id": ActionTrack,
|
|
"template_version_id": ActionTrack,
|
|
"template_parameters": ActionTrack,
|
|
"prompt": ActionTrack,
|
|
"created_at": ActionIgnore, // Never changes.
|
|
"deleted_at": ActionIgnore, // Changes, but is implicit when a delete event is fired.
|
|
},
|
|
&database.Chat{}: {
|
|
"id": ActionTrack,
|
|
"owner_id": ActionTrack,
|
|
"owner_username": ActionIgnore,
|
|
"owner_name": ActionIgnore,
|
|
"organization_id": ActionIgnore, // Never changes after creation.
|
|
"workspace_id": ActionTrack,
|
|
"build_id": ActionIgnore, // Internal lifecycle.
|
|
"agent_id": ActionIgnore, // Internal lifecycle.
|
|
"title": ActionSecret, // May contain sensitive content.
|
|
"status": ActionIgnore, // Churns every message.
|
|
"worker_id": ActionIgnore, // Internal.
|
|
"started_at": ActionIgnore,
|
|
"heartbeat_at": ActionIgnore, // Internal.
|
|
"created_at": ActionIgnore, // Never changes.
|
|
"updated_at": ActionIgnore, // Bumped on every mutation.
|
|
"parent_chat_id": ActionIgnore, // Immutable after creation.
|
|
"root_chat_id": ActionIgnore, // Immutable after creation.
|
|
"last_model_config_id": ActionIgnore, // Churns every message.
|
|
"archived": ActionTrack,
|
|
"last_error": ActionIgnore, // Internal.
|
|
"last_turn_summary": ActionIgnore, // Internal cached display text.
|
|
"mode": ActionTrack,
|
|
"mcp_server_ids": ActionTrack,
|
|
"labels": ActionTrack,
|
|
"user_acl": ActionTrack,
|
|
"group_acl": ActionTrack,
|
|
"pin_order": ActionTrack,
|
|
"last_read_message_id": ActionIgnore, // User-scoped read cursor.
|
|
"last_injected_context": ActionIgnore, // Internal lifecycle.
|
|
"dynamic_tools": ActionIgnore, // Internal lifecycle.
|
|
"plan_mode": ActionIgnore, // Can flip back and forth during a session.
|
|
"client_type": ActionIgnore, // Set at creation.
|
|
},
|
|
&database.UserSkill{}: {
|
|
"id": ActionTrack,
|
|
"user_id": ActionTrack,
|
|
"name": ActionTrack,
|
|
"description": ActionTrack,
|
|
"content": ActionTrack,
|
|
"created_at": ActionIgnore,
|
|
"updated_at": ActionIgnore,
|
|
},
|
|
&database.UserSecret{}: {
|
|
"id": ActionTrack,
|
|
"user_id": ActionTrack,
|
|
"name": ActionTrack,
|
|
"description": ActionTrack,
|
|
"env_name": ActionTrack,
|
|
"file_path": ActionTrack,
|
|
|
|
"value": ActionSecret,
|
|
|
|
"value_key_id": ActionIgnore,
|
|
"created_at": ActionIgnore,
|
|
"updated_at": ActionIgnore,
|
|
},
|
|
}
|
|
|
|
// 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
|
|
}
|
|
jsonTag = strings.TrimSuffix(jsonTag, ",omitempty")
|
|
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
|
|
}
|