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.
This commit is contained in:
Ethan
2025-07-15 16:08:42 +10:00
committed by GitHub
parent 6b17aee425
commit ef807e41ce
6 changed files with 116 additions and 85 deletions

View File

@ -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

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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, ""),
})

View File

@ -30,8 +30,6 @@ We track the following resources:
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>active_version_id</td><td>true</td></tr><tr><td>activity_bump</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_name</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deprecated</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_port_sharing_level</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_display_name</td><td>false</td></tr><tr><td>organization_icon</td><td>false</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>organization_name</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>require_active_version</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>use_classic_parameter_flow</td><td>true</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_name</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>has_ai_task</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>source_example_id</td><td>false</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>github_com_user_id</td><td>false</td></tr><tr><td>hashed_one_time_passcode</td><td>false</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>is_system</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>one_time_passcode_expires_at</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
| WorkspaceAgent<br><i>connect, disconnect</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>api_key_scope</td><td>false</td></tr><tr><td>api_version</td><td>false</td></tr><tr><td>architecture</td><td>false</td></tr><tr><td>auth_instance_id</td><td>false</td></tr><tr><td>auth_token</td><td>false</td></tr><tr><td>connection_timeout_seconds</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>directory</td><td>false</td></tr><tr><td>disconnected_at</td><td>false</td></tr><tr><td>display_apps</td><td>false</td></tr><tr><td>display_order</td><td>false</td></tr><tr><td>environment_variables</td><td>false</td></tr><tr><td>expanded_directory</td><td>false</td></tr><tr><td>first_connected_at</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>instance_metadata</td><td>false</td></tr><tr><td>last_connected_at</td><td>false</td></tr><tr><td>last_connected_replica_id</td><td>false</td></tr><tr><td>lifecycle_state</td><td>false</td></tr><tr><td>logs_length</td><td>false</td></tr><tr><td>logs_overflowed</td><td>false</td></tr><tr><td>motd_file</td><td>false</td></tr><tr><td>name</td><td>false</td></tr><tr><td>operating_system</td><td>false</td></tr><tr><td>parent_id</td><td>false</td></tr><tr><td>ready_at</td><td>false</td></tr><tr><td>resource_id</td><td>false</td></tr><tr><td>resource_metadata</td><td>false</td></tr><tr><td>started_at</td><td>false</td></tr><tr><td>subsystems</td><td>false</td></tr><tr><td>troubleshooting_url</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>version</td><td>false</td></tr></tbody></table> |
| WorkspaceApp<br><i>open, close</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>agent_id</td><td>false</td></tr><tr><td>command</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>display_group</td><td>false</td></tr><tr><td>display_name</td><td>false</td></tr><tr><td>display_order</td><td>false</td></tr><tr><td>external</td><td>false</td></tr><tr><td>health</td><td>false</td></tr><tr><td>healthcheck_interval</td><td>false</td></tr><tr><td>healthcheck_threshold</td><td>false</td></tr><tr><td>healthcheck_url</td><td>false</td></tr><tr><td>hidden</td><td>false</td></tr><tr><td>icon</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>open_in</td><td>false</td></tr><tr><td>sharing_level</td><td>false</td></tr><tr><td>slug</td><td>false</td></tr><tr><td>subdomain</td><td>false</td></tr><tr><td>url</td><td>false</td></tr></tbody></table> |
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>ai_task_sidebar_app_id</td><td>false</td></tr><tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>has_ai_task</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_name</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>template_version_preset_id</td><td>false</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
| WorkspaceProxy<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>derp_enabled</td><td>true</td></tr><tr><td>derp_only</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>region_id</td><td>true</td></tr><tr><td>token_hashed_secret</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>url</td><td>true</td></tr><tr><td>version</td><td>true</td></tr><tr><td>wildcard_hostname</td><td>true</td></tr></tbody></table> |
| WorkspaceTable<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>favorite</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>next_start_at</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
@ -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": "",

View File

@ -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