From ef807e41ce2efe7a1cb7099e77f33305cce69bdb Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Tue, 15 Jul 2025 16:08:42 +1000
Subject: [PATCH] chore: mark workspace apps and workspace agents as unaudited
(#18761)
The main goal of this PR is to remove Workspace Apps and Workspace Agents from the auto-generated audit log documentation, that incorrectly claims they are audited resources (no longer true with the addition of the connection log).
Though I believe we haven't touched any codepaths for returning audit logs, this PR also adds a test that ensures we continue to return *existing* connection, disconnect and open events correctly from the audit log API.
---
coderd/audit/diff.go | 4 +-
coderd/audit/request.go | 16 -----
coderd/audit_test.go | 110 ++++++++++++++++++++++++++++++
coderd/database/dbgen/dbgen.go | 2 +-
docs/admin/security/audit-logs.md | 10 ++-
enterprise/audit/table.go | 59 ----------------
6 files changed, 116 insertions(+), 85 deletions(-)
diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go
index 56ac9f88cc..b8139bb63b 100644
--- a/coderd/audit/diff.go
+++ b/coderd/audit/diff.go
@@ -31,9 +31,7 @@ type Auditable interface {
database.NotificationTemplate |
idpsync.OrganizationSyncSettings |
idpsync.GroupSyncSettings |
- idpsync.RoleSyncSettings |
- database.WorkspaceAgent |
- database.WorkspaceApp
+ idpsync.RoleSyncSettings
}
// Map is a map of changed fields in an audited resource. It maps field names to
diff --git a/coderd/audit/request.go b/coderd/audit/request.go
index ae6a57e6c2..a973bdb915 100644
--- a/coderd/audit/request.go
+++ b/coderd/audit/request.go
@@ -131,10 +131,6 @@ func ResourceTarget[T Auditable](tgt T) string {
return "Organization Group Sync"
case idpsync.RoleSyncSettings:
return "Organization Role Sync"
- case database.WorkspaceAgent:
- return typed.Name
- case database.WorkspaceApp:
- return typed.Slug
default:
panic(fmt.Sprintf("unknown resource %T for ResourceTarget", tgt))
}
@@ -197,10 +193,6 @@ func ResourceID[T Auditable](tgt T) uuid.UUID {
return noID // Org field on audit log has org id
case idpsync.RoleSyncSettings:
return noID // Org field on audit log has org id
- case database.WorkspaceAgent:
- return typed.ID
- case database.WorkspaceApp:
- return typed.ID
default:
panic(fmt.Sprintf("unknown resource %T for ResourceID", tgt))
}
@@ -254,10 +246,6 @@ func ResourceType[T Auditable](tgt T) database.ResourceType {
return database.ResourceTypeIdpSyncSettingsRole
case idpsync.GroupSyncSettings:
return database.ResourceTypeIdpSyncSettingsGroup
- case database.WorkspaceAgent:
- return database.ResourceTypeWorkspaceAgent
- case database.WorkspaceApp:
- return database.ResourceTypeWorkspaceApp
default:
panic(fmt.Sprintf("unknown resource %T for ResourceType", typed))
}
@@ -314,10 +302,6 @@ func ResourceRequiresOrgID[T Auditable]() bool {
return true
case idpsync.RoleSyncSettings:
return true
- case database.WorkspaceAgent:
- return true
- case database.WorkspaceApp:
- return true
default:
panic(fmt.Sprintf("unknown resource %T for ResourceRequiresOrgID", tgt))
}
diff --git a/coderd/audit_test.go b/coderd/audit_test.go
index e6fa985038..13dbc9ccd8 100644
--- a/coderd/audit_test.go
+++ b/coderd/audit_test.go
@@ -15,6 +15,7 @@ import (
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisioner/echo"
@@ -531,3 +532,112 @@ func completeWithAgentAndApp() *echo.Responses {
},
}
}
+
+// TestDeprecatedConnEvents tests the deprecated connection and disconnection
+// events in the audit logs. These events are no longer created, but need to be
+// returned by the API.
+func TestDeprecatedConnEvents(t *testing.T) {
+ t.Parallel()
+ var (
+ ctx = context.Background()
+ client, _, api = coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+ user = coderdtest.CreateFirstUser(t, client)
+ version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, completeWithAgentAndApp())
+ template = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+ )
+
+ coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
+ workspace := coderdtest.CreateWorkspace(t, client, template.ID)
+ workspace.LatestBuild = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
+
+ type additionalFields struct {
+ audit.AdditionalFields
+ ConnectionType string `json:"connection_type"`
+ }
+
+ sshFields := additionalFields{
+ AdditionalFields: audit.AdditionalFields{
+ WorkspaceName: workspace.Name,
+ BuildNumber: "999",
+ BuildReason: "initiator",
+ WorkspaceOwner: workspace.OwnerName,
+ WorkspaceID: workspace.ID,
+ },
+ ConnectionType: "SSH",
+ }
+
+ sshFieldsBytes, err := json.Marshal(sshFields)
+ require.NoError(t, err)
+
+ appFields := audit.AdditionalFields{
+ WorkspaceName: workspace.Name,
+ // Deliberately empty
+ BuildNumber: "",
+ BuildReason: "",
+ WorkspaceOwner: workspace.OwnerName,
+ WorkspaceID: workspace.ID,
+ }
+
+ appFieldsBytes, err := json.Marshal(appFields)
+ require.NoError(t, err)
+
+ dbgen.AuditLog(t, api.Database, database.AuditLog{
+ OrganizationID: user.OrganizationID,
+ Action: database.AuditActionConnect,
+ ResourceType: database.ResourceTypeWorkspaceAgent,
+ ResourceID: workspace.LatestBuild.Resources[0].Agents[0].ID,
+ ResourceTarget: workspace.LatestBuild.Resources[0].Agents[0].Name,
+ Time: time.Date(2022, 8, 15, 14, 30, 45, 100, time.UTC), // 2022-8-15 14:30:45
+ AdditionalFields: sshFieldsBytes,
+ })
+
+ dbgen.AuditLog(t, api.Database, database.AuditLog{
+ OrganizationID: user.OrganizationID,
+ Action: database.AuditActionDisconnect,
+ ResourceType: database.ResourceTypeWorkspaceAgent,
+ ResourceID: workspace.LatestBuild.Resources[0].Agents[0].ID,
+ ResourceTarget: workspace.LatestBuild.Resources[0].Agents[0].Name,
+ Time: time.Date(2022, 8, 15, 14, 35, 0o0, 100, time.UTC), // 2022-8-15 14:35:00
+ AdditionalFields: sshFieldsBytes,
+ })
+
+ dbgen.AuditLog(t, api.Database, database.AuditLog{
+ OrganizationID: user.OrganizationID,
+ UserID: user.UserID,
+ Action: database.AuditActionOpen,
+ ResourceType: database.ResourceTypeWorkspaceApp,
+ ResourceID: workspace.LatestBuild.Resources[0].Agents[0].Apps[0].ID,
+ ResourceTarget: workspace.LatestBuild.Resources[0].Agents[0].Apps[0].Slug,
+ Time: time.Date(2022, 8, 15, 14, 30, 45, 100, time.UTC), // 2022-8-15 14:30:45
+ AdditionalFields: appFieldsBytes,
+ })
+
+ connLog, err := client.AuditLogs(ctx, codersdk.AuditLogsRequest{
+ SearchQuery: "action:connect",
+ })
+ require.NoError(t, err)
+ require.Len(t, connLog.AuditLogs, 1)
+ var sshOutFields additionalFields
+ err = json.Unmarshal(connLog.AuditLogs[0].AdditionalFields, &sshOutFields)
+ require.NoError(t, err)
+ require.Equal(t, sshFields, sshOutFields)
+
+ dcLog, err := client.AuditLogs(ctx, codersdk.AuditLogsRequest{
+ SearchQuery: "action:disconnect",
+ })
+ require.NoError(t, err)
+ require.Len(t, dcLog.AuditLogs, 1)
+ err = json.Unmarshal(dcLog.AuditLogs[0].AdditionalFields, &sshOutFields)
+ require.NoError(t, err)
+ require.Equal(t, sshFields, sshOutFields)
+
+ openLog, err := client.AuditLogs(ctx, codersdk.AuditLogsRequest{
+ SearchQuery: "action:open",
+ })
+ require.NoError(t, err)
+ require.Len(t, openLog.AuditLogs, 1)
+ var appOutFields audit.AdditionalFields
+ err = json.Unmarshal(openLog.AuditLogs[0].AdditionalFields, &appOutFields)
+ require.NoError(t, err)
+ require.Equal(t, appFields, appOutFields)
+}
diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go
index 9720050a43..d5693afe98 100644
--- a/coderd/database/dbgen/dbgen.go
+++ b/coderd/database/dbgen/dbgen.go
@@ -65,7 +65,7 @@ func AuditLog(t testing.TB, db database.Store, seed database.AuditLog) database.
Action: takeFirst(seed.Action, database.AuditActionCreate),
Diff: takeFirstSlice(seed.Diff, []byte("{}")),
StatusCode: takeFirst(seed.StatusCode, 200),
- AdditionalFields: takeFirstSlice(seed.Diff, []byte("{}")),
+ AdditionalFields: takeFirstSlice(seed.AdditionalFields, []byte("{}")),
RequestID: takeFirst(seed.RequestID, uuid.New()),
ResourceIcon: takeFirst(seed.ResourceIcon, ""),
})
diff --git a/docs/admin/security/audit-logs.md b/docs/admin/security/audit-logs.md
index af033d02df..4d66260fb2 100644
--- a/docs/admin/security/audit-logs.md
+++ b/docs/admin/security/audit-logs.md
@@ -30,8 +30,6 @@ We track the following resources:
| Template
write, delete |
| Field | Tracked |
| | active_version_id | true |
| activity_bump | true |
| allow_user_autostart | true |
| allow_user_autostop | true |
| allow_user_cancel_workspace_jobs | true |
| autostart_block_days_of_week | true |
| autostop_requirement_days_of_week | true |
| autostop_requirement_weeks | true |
| created_at | false |
| created_by | true |
| created_by_avatar_url | false |
| created_by_name | false |
| created_by_username | false |
| default_ttl | true |
| deleted | false |
| deprecated | true |
| description | true |
| display_name | true |
| failure_ttl | true |
| group_acl | true |
| icon | true |
| id | true |
| max_port_sharing_level | true |
| name | true |
| organization_display_name | false |
| organization_icon | false |
| organization_id | false |
| organization_name | false |
| provisioner | true |
| require_active_version | true |
| time_til_dormant | true |
| time_til_dormant_autodelete | true |
| updated_at | false |
| use_classic_parameter_flow | true |
| user_acl | true |
|
| TemplateVersion
create, write | | Field | Tracked |
| | archived | true |
| created_at | false |
| created_by | true |
| created_by_avatar_url | false |
| created_by_name | false |
| created_by_username | false |
| external_auth_providers | false |
| has_ai_task | false |
| id | true |
| job_id | false |
| message | false |
| name | true |
| organization_id | false |
| readme | true |
| source_example_id | false |
| template_id | true |
| updated_at | false |
|
| User
create, write, delete | | Field | Tracked |
| | avatar_url | false |
| created_at | false |
| deleted | true |
| email | true |
| github_com_user_id | false |
| hashed_one_time_passcode | false |
| hashed_password | true |
| id | true |
| is_system | true |
| last_seen_at | false |
| login_type | true |
| name | true |
| one_time_passcode_expires_at | true |
| quiet_hours_schedule | true |
| rbac_roles | true |
| status | true |
| updated_at | false |
| username | true |
|
-| WorkspaceAgent
connect, disconnect | | Field | Tracked |
| | api_key_scope | false |
| api_version | false |
| architecture | false |
| auth_instance_id | false |
| auth_token | false |
| connection_timeout_seconds | false |
| created_at | false |
| deleted | false |
| directory | false |
| disconnected_at | false |
| display_apps | false |
| display_order | false |
| environment_variables | false |
| expanded_directory | false |
| first_connected_at | false |
| id | false |
| instance_metadata | false |
| last_connected_at | false |
| last_connected_replica_id | false |
| lifecycle_state | false |
| logs_length | false |
| logs_overflowed | false |
| motd_file | false |
| name | false |
| operating_system | false |
| parent_id | false |
| ready_at | false |
| resource_id | false |
| resource_metadata | false |
| started_at | false |
| subsystems | false |
| troubleshooting_url | false |
| updated_at | false |
| version | false |
|
-| WorkspaceApp
open, close | | Field | Tracked |
| | agent_id | false |
| command | false |
| created_at | false |
| display_group | false |
| display_name | false |
| display_order | false |
| external | false |
| health | false |
| healthcheck_interval | false |
| healthcheck_threshold | false |
| healthcheck_url | false |
| hidden | false |
| icon | false |
| id | false |
| open_in | false |
| sharing_level | false |
| slug | false |
| subdomain | false |
| url | false |
|
| WorkspaceBuild
start, stop | | Field | Tracked |
| | ai_task_sidebar_app_id | false |
| build_number | false |
| created_at | false |
| daily_cost | false |
| deadline | false |
| has_ai_task | false |
| id | false |
| initiator_by_avatar_url | false |
| initiator_by_name | false |
| initiator_by_username | false |
| initiator_id | false |
| job_id | false |
| max_deadline | false |
| provisioner_state | false |
| reason | false |
| template_version_id | true |
| template_version_preset_id | false |
| transition | false |
| updated_at | false |
| workspace_id | false |
|
| WorkspaceProxy
| | Field | Tracked |
| | created_at | true |
| deleted | false |
| derp_enabled | true |
| derp_only | true |
| display_name | true |
| icon | true |
| id | true |
| name | true |
| region_id | true |
| token_hashed_secret | true |
| updated_at | false |
| url | true |
| version | true |
| wildcard_hostname | true |
|
| WorkspaceTable
| | Field | Tracked |
| | automatic_updates | true |
| autostart_schedule | true |
| created_at | false |
| deleted | false |
| deleting_at | true |
| dormant_at | true |
| favorite | true |
| id | true |
| last_used_at | false |
| name | true |
| next_start_at | true |
| organization_id | false |
| owner_id | true |
| template_id | true |
| ttl | true |
| updated_at | false |
|
@@ -91,16 +89,16 @@ log entry:
"ts": "2023-06-13T03:45:37.294730279Z",
"level": "INFO",
"msg": "audit_log",
- "caller": "/home/runner/work/coder/coder/enterprise/audit/backends/slog.go:36",
- "func": "github.com/coder/coder/enterprise/audit/backends.slogBackend.Export",
+ "caller": "/home/coder/coder/enterprise/audit/backends/slog.go:38",
+ "func": "github.com/coder/coder/v2/enterprise/audit/backends.(*SlogExporter).ExportStruct",
"logger_names": ["coderd"],
"fields": {
"ID": "033a9ffa-b54d-4c10-8ec3-2aaf9e6d741a",
"Time": "2023-06-13T03:45:37.288506Z",
"UserID": "6c405053-27e3-484a-9ad7-bcb64e7bfde6",
"OrganizationID": "00000000-0000-0000-0000-000000000000",
- "Ip": "{IPNet:{IP:\u003cnil\u003e Mask:\u003cnil\u003e} Valid:false}",
- "UserAgent": "{String: Valid:false}",
+ "Ip": null,
+ "UserAgent": null,
"ResourceType": "workspace_build",
"ResourceID": "ca5647e0-ef50-4202-a246-717e04447380",
"ResourceTarget": "",
diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go
index 2a563946dc..6c1f907abf 100644
--- a/enterprise/audit/table.go
+++ b/enterprise/audit/table.go
@@ -27,8 +27,6 @@ var AuditActionMap = map[string][]codersdk.AuditAction{
"Group": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
"APIKey": {codersdk.AuditActionLogin, codersdk.AuditActionLogout, codersdk.AuditActionRegister, codersdk.AuditActionCreate, codersdk.AuditActionDelete},
"License": {codersdk.AuditActionCreate, codersdk.AuditActionDelete},
- "WorkspaceAgent": {codersdk.AuditActionConnect, codersdk.AuditActionDisconnect},
- "WorkspaceApp": {codersdk.AuditActionOpen, codersdk.AuditActionClose},
}
type Action string
@@ -343,63 +341,6 @@ var auditableResourcesTypes = map[any]map[string]Action{
"field": ActionTrack,
"mapping": ActionTrack,
},
- &database.WorkspaceAgent{}: {
- "id": ActionIgnore,
- "created_at": ActionIgnore,
- "updated_at": ActionIgnore,
- "name": ActionIgnore,
- "first_connected_at": ActionIgnore,
- "last_connected_at": ActionIgnore,
- "disconnected_at": ActionIgnore,
- "resource_id": ActionIgnore,
- "auth_token": ActionIgnore,
- "auth_instance_id": ActionIgnore,
- "architecture": ActionIgnore,
- "environment_variables": ActionIgnore,
- "operating_system": ActionIgnore,
- "instance_metadata": ActionIgnore,
- "resource_metadata": ActionIgnore,
- "directory": ActionIgnore,
- "version": ActionIgnore,
- "last_connected_replica_id": ActionIgnore,
- "connection_timeout_seconds": ActionIgnore,
- "troubleshooting_url": ActionIgnore,
- "motd_file": ActionIgnore,
- "lifecycle_state": ActionIgnore,
- "expanded_directory": ActionIgnore,
- "logs_length": ActionIgnore,
- "logs_overflowed": ActionIgnore,
- "started_at": ActionIgnore,
- "ready_at": ActionIgnore,
- "subsystems": ActionIgnore,
- "display_apps": ActionIgnore,
- "api_version": ActionIgnore,
- "display_order": ActionIgnore,
- "parent_id": ActionIgnore,
- "api_key_scope": ActionIgnore,
- "deleted": ActionIgnore,
- },
- &database.WorkspaceApp{}: {
- "id": ActionIgnore,
- "created_at": ActionIgnore,
- "agent_id": ActionIgnore,
- "display_name": ActionIgnore,
- "icon": ActionIgnore,
- "command": ActionIgnore,
- "url": ActionIgnore,
- "healthcheck_url": ActionIgnore,
- "healthcheck_interval": ActionIgnore,
- "healthcheck_threshold": ActionIgnore,
- "health": ActionIgnore,
- "subdomain": ActionIgnore,
- "sharing_level": ActionIgnore,
- "slug": ActionIgnore,
- "external": ActionIgnore,
- "display_group": ActionIgnore,
- "display_order": ActionIgnore,
- "hidden": ActionIgnore,
- "open_in": ActionIgnore,
- },
}
// auditMap converts a map of struct pointers to a map of struct names as