diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 2e48634c7de13..7dcfdbe982de1 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -10098,7 +10098,11 @@ const docTemplate = `{ "login", "logout", "register", - "request_password_reset" + "request_password_reset", + "connect", + "disconnect", + "open", + "close" ], "x-enum-varnames": [ "AuditActionCreate", @@ -10109,7 +10113,11 @@ const docTemplate = `{ "AuditActionLogin", "AuditActionLogout", "AuditActionRegister", - "AuditActionRequestPasswordReset" + "AuditActionRequestPasswordReset", + "AuditActionConnect", + "AuditActionDisconnect", + "AuditActionOpen", + "AuditActionClose" ] }, "codersdk.AuditDiff": { @@ -10770,6 +10778,10 @@ const docTemplate = `{ "type": "string", "format": "uuid" }, + "request_id": { + "type": "string", + "format": "uuid" + }, "resource_id": { "type": "string", "format": "uuid" @@ -13864,7 +13876,9 @@ const docTemplate = `{ "notification_template", "idp_sync_settings_organization", "idp_sync_settings_group", - "idp_sync_settings_role" + "idp_sync_settings_role", + "workspace_agent", + "workspace_app" ], "x-enum-varnames": [ "ResourceTypeTemplate", @@ -13888,7 +13902,9 @@ const docTemplate = `{ "ResourceTypeNotificationTemplate", "ResourceTypeIdpSyncSettingsOrganization", "ResourceTypeIdpSyncSettingsGroup", - "ResourceTypeIdpSyncSettingsRole" + "ResourceTypeIdpSyncSettingsRole", + "ResourceTypeWorkspaceAgent", + "ResourceTypeWorkspaceApp" ] }, "codersdk.Response": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 0e03555da4720..0dfaf26184cb1 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -8962,7 +8962,11 @@ "login", "logout", "register", - "request_password_reset" + "request_password_reset", + "connect", + "disconnect", + "open", + "close" ], "x-enum-varnames": [ "AuditActionCreate", @@ -8973,7 +8977,11 @@ "AuditActionLogin", "AuditActionLogout", "AuditActionRegister", - "AuditActionRequestPasswordReset" + "AuditActionRequestPasswordReset", + "AuditActionConnect", + "AuditActionDisconnect", + "AuditActionOpen", + "AuditActionClose" ] }, "codersdk.AuditDiff": { @@ -9583,6 +9591,10 @@ "type": "string", "format": "uuid" }, + "request_id": { + "type": "string", + "format": "uuid" + }, "resource_id": { "type": "string", "format": "uuid" @@ -12549,7 +12561,9 @@ "notification_template", "idp_sync_settings_organization", "idp_sync_settings_group", - "idp_sync_settings_role" + "idp_sync_settings_role", + "workspace_agent", + "workspace_app" ], "x-enum-varnames": [ "ResourceTypeTemplate", @@ -12573,7 +12587,9 @@ "ResourceTypeNotificationTemplate", "ResourceTypeIdpSyncSettingsOrganization", "ResourceTypeIdpSyncSettingsGroup", - "ResourceTypeIdpSyncSettingsRole" + "ResourceTypeIdpSyncSettingsRole", + "ResourceTypeWorkspaceAgent", + "ResourceTypeWorkspaceApp" ] }, "codersdk.Response": { diff --git a/coderd/audit.go b/coderd/audit.go index f764094782a2f..72be70754c2ea 100644 --- a/coderd/audit.go +++ b/coderd/audit.go @@ -159,7 +159,7 @@ func (api *API) generateFakeAuditLog(rw http.ResponseWriter, r *http.Request) { Diff: diff, StatusCode: http.StatusOK, AdditionalFields: params.AdditionalFields, - RequestID: uuid.Nil, // no request ID to attach this to + RequestID: params.RequestID, ResourceIcon: "", OrganizationID: params.OrganizationID, }) diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go index 98e47e91893cb..0a4c35814df0c 100644 --- a/coderd/audit/diff.go +++ b/coderd/audit/diff.go @@ -30,7 +30,9 @@ type Auditable interface { database.NotificationTemplate | idpsync.OrganizationSyncSettings | idpsync.GroupSyncSettings | - idpsync.RoleSyncSettings + idpsync.RoleSyncSettings | + database.WorkspaceAgent | + database.WorkspaceApp } // 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 05c18e32fd183..3ed6891f12316 100644 --- a/coderd/audit/request.go +++ b/coderd/audit/request.go @@ -128,6 +128,10 @@ 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)) } @@ -187,6 +191,10 @@ 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)) } @@ -238,6 +246,10 @@ 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)) } @@ -291,6 +303,10 @@ 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 922e2b359b506..18bcd78b38807 100644 --- a/coderd/audit_test.go +++ b/coderd/audit_test.go @@ -17,6 +17,8 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/provisioner/echo" + "github.com/coder/coder/v2/provisionersdk/proto" ) func TestAuditLogs(t *testing.T) { @@ -30,7 +32,8 @@ func TestAuditLogs(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) err := client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ - ResourceID: user.UserID, + ResourceID: user.UserID, + OrganizationID: user.OrganizationID, }) require.NoError(t, err) @@ -54,7 +57,8 @@ func TestAuditLogs(t *testing.T) { client2, user2 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleOwner()) err := client2.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ - ResourceID: user2.ID, + ResourceID: user2.ID, + OrganizationID: user.OrganizationID, }) require.NoError(t, err) @@ -123,6 +127,7 @@ func TestAuditLogs(t *testing.T) { ResourceType: codersdk.ResourceTypeWorkspaceBuild, ResourceID: workspace.LatestBuild.ID, AdditionalFields: wriBytes, + OrganizationID: user.OrganizationID, }) require.NoError(t, err) @@ -158,7 +163,8 @@ func TestAuditLogs(t *testing.T) { // Add an extra audit log in another organization err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ - ResourceID: owner.UserID, + ResourceID: owner.UserID, + OrganizationID: uuid.New(), }) require.NoError(t, err) @@ -229,53 +235,102 @@ func TestAuditLogsFilter(t *testing.T) { ctx = context.Background() client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) user = coderdtest.CreateFirstUser(t, client) - version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + 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) // Create two logs with "Create" err := client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ - Action: codersdk.AuditActionCreate, - ResourceType: codersdk.ResourceTypeTemplate, - ResourceID: template.ID, - Time: time.Date(2022, 8, 15, 14, 30, 45, 100, time.UTC), // 2022-8-15 14:30:45 + OrganizationID: user.OrganizationID, + Action: codersdk.AuditActionCreate, + ResourceType: codersdk.ResourceTypeTemplate, + ResourceID: template.ID, + Time: time.Date(2022, 8, 15, 14, 30, 45, 100, time.UTC), // 2022-8-15 14:30:45 }) require.NoError(t, err) err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ - Action: codersdk.AuditActionCreate, - ResourceType: codersdk.ResourceTypeUser, - ResourceID: user.UserID, - Time: time.Date(2022, 8, 16, 14, 30, 45, 100, time.UTC), // 2022-8-16 14:30:45 + OrganizationID: user.OrganizationID, + Action: codersdk.AuditActionCreate, + ResourceType: codersdk.ResourceTypeUser, + ResourceID: user.UserID, + Time: time.Date(2022, 8, 16, 14, 30, 45, 100, time.UTC), // 2022-8-16 14:30:45 }) require.NoError(t, err) // Create one log with "Delete" err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ - Action: codersdk.AuditActionDelete, - ResourceType: codersdk.ResourceTypeUser, - ResourceID: user.UserID, - Time: time.Date(2022, 8, 15, 14, 30, 45, 100, time.UTC), // 2022-8-15 14:30:45 + OrganizationID: user.OrganizationID, + Action: codersdk.AuditActionDelete, + ResourceType: codersdk.ResourceTypeUser, + ResourceID: user.UserID, + Time: time.Date(2022, 8, 15, 14, 30, 45, 100, time.UTC), // 2022-8-15 14:30:45 }) require.NoError(t, err) // Create one log with "Start" err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ - Action: codersdk.AuditActionStart, - ResourceType: codersdk.ResourceTypeWorkspaceBuild, - ResourceID: workspace.LatestBuild.ID, - Time: time.Date(2022, 8, 15, 14, 30, 45, 100, time.UTC), // 2022-8-15 14:30:45 + OrganizationID: user.OrganizationID, + Action: codersdk.AuditActionStart, + ResourceType: codersdk.ResourceTypeWorkspaceBuild, + ResourceID: workspace.LatestBuild.ID, + Time: time.Date(2022, 8, 15, 14, 30, 45, 100, time.UTC), // 2022-8-15 14:30:45 }) require.NoError(t, err) // Create one log with "Stop" err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ - Action: codersdk.AuditActionStop, - ResourceType: codersdk.ResourceTypeWorkspaceBuild, - ResourceID: workspace.LatestBuild.ID, - Time: time.Date(2022, 8, 15, 14, 30, 45, 100, time.UTC), // 2022-8-15 14:30:45 + OrganizationID: user.OrganizationID, + Action: codersdk.AuditActionStop, + ResourceType: codersdk.ResourceTypeWorkspaceBuild, + ResourceID: workspace.LatestBuild.ID, + Time: time.Date(2022, 8, 15, 14, 30, 45, 100, time.UTC), // 2022-8-15 14:30:45 + }) + require.NoError(t, err) + + // Create one log with "Connect" and "Disconect". + connectRequestID := uuid.New() + err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ + OrganizationID: user.OrganizationID, + Action: codersdk.AuditActionConnect, + RequestID: connectRequestID, + ResourceType: codersdk.ResourceTypeWorkspaceAgent, + ResourceID: workspace.LatestBuild.Resources[0].Agents[0].ID, + Time: time.Date(2022, 8, 15, 14, 30, 45, 100, time.UTC), // 2022-8-15 14:30:45 + }) + require.NoError(t, err) + + err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ + OrganizationID: user.OrganizationID, + Action: codersdk.AuditActionDisconnect, + RequestID: connectRequestID, + ResourceType: codersdk.ResourceTypeWorkspaceAgent, + ResourceID: workspace.LatestBuild.Resources[0].Agents[0].ID, + Time: time.Date(2022, 8, 15, 14, 35, 0o0, 100, time.UTC), // 2022-8-15 14:35:00 + }) + require.NoError(t, err) + + // Create one log with "Open" and "Close". + openRequestID := uuid.New() + err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ + OrganizationID: user.OrganizationID, + Action: codersdk.AuditActionOpen, + RequestID: openRequestID, + ResourceType: codersdk.ResourceTypeWorkspaceApp, + ResourceID: workspace.LatestBuild.Resources[0].Agents[0].Apps[0].ID, + Time: time.Date(2022, 8, 15, 14, 30, 45, 100, time.UTC), // 2022-8-15 14:30:45 + }) + require.NoError(t, err) + err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ + OrganizationID: user.OrganizationID, + Action: codersdk.AuditActionClose, + RequestID: openRequestID, + ResourceType: codersdk.ResourceTypeWorkspaceApp, + ResourceID: workspace.LatestBuild.Resources[0].Agents[0].Apps[0].ID, + Time: time.Date(2022, 8, 15, 14, 35, 0o0, 100, time.UTC), // 2022-8-15 14:35:00 }) require.NoError(t, err) @@ -309,12 +364,12 @@ func TestAuditLogsFilter(t *testing.T) { { Name: "FilterByEmail", SearchQuery: "email:" + coderdtest.FirstUserParams.Email, - ExpectedResult: 5, + ExpectedResult: 9, }, { Name: "FilterByUsername", SearchQuery: "username:" + coderdtest.FirstUserParams.Username, - ExpectedResult: 5, + ExpectedResult: 9, }, { Name: "FilterByResourceID", @@ -366,6 +421,36 @@ func TestAuditLogsFilter(t *testing.T) { SearchQuery: "resource_type:workspace_build action:start build_reason:initiator", ExpectedResult: 1, }, + { + Name: "FilterOnWorkspaceAgentConnect", + SearchQuery: "resource_type:workspace_agent action:connect", + ExpectedResult: 1, + }, + { + Name: "FilterOnWorkspaceAgentDisconnect", + SearchQuery: "resource_type:workspace_agent action:disconnect", + ExpectedResult: 1, + }, + { + Name: "FilterOnWorkspaceAgentConnectionRequestID", + SearchQuery: "resource_type:workspace_agent request_id:" + connectRequestID.String(), + ExpectedResult: 2, + }, + { + Name: "FilterOnWorkspaceAppOpen", + SearchQuery: "resource_type:workspace_app action:open", + ExpectedResult: 1, + }, + { + Name: "FilterOnWorkspaceAppClose", + SearchQuery: "resource_type:workspace_app action:close", + ExpectedResult: 1, + }, + { + Name: "FilterOnWorkspaceAppOpenRequestID", + SearchQuery: "resource_type:workspace_app request_id:" + openRequestID.String(), + ExpectedResult: 2, + }, } for _, testCase := range testCases { @@ -387,3 +472,63 @@ func TestAuditLogsFilter(t *testing.T) { } }) } + +func completeWithAgentAndApp() *echo.Responses { + return &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: []*proto.Response{ + { + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ + Resources: []*proto.Resource{ + { + Type: "compute", + Name: "main", + Agents: []*proto.Agent{ + { + Name: "smith", + OperatingSystem: "linux", + Architecture: "i386", + Apps: []*proto.App{ + { + Slug: "app", + DisplayName: "App", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + ProvisionApply: []*proto.Response{ + { + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ + Resources: []*proto.Resource{ + { + Type: "compute", + Name: "main", + Agents: []*proto.Agent{ + { + Name: "smith", + OperatingSystem: "linux", + Architecture: "i386", + Apps: []*proto.App{ + { + Slug: "app", + DisplayName: "App", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } +} diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 21c40233718ef..cb183bd7e0df1 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -12370,10 +12370,13 @@ func (q *FakeQuerier) GetAuthorizedAuditLogsOffset(ctx context.Context, arg data arg.OffsetOpt-- continue } + if arg.RequestID != uuid.Nil && arg.RequestID != alog.RequestID { + continue + } if arg.OrganizationID != uuid.Nil && arg.OrganizationID != alog.OrganizationID { continue } - if arg.Action != "" && !strings.Contains(string(alog.Action), arg.Action) { + if arg.Action != "" && string(alog.Action) != arg.Action { continue } if arg.ResourceType != "" && !strings.Contains(string(alog.ResourceType), arg.ResourceType) { diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 20e7d14b57d01..44bf68a36eb40 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -25,7 +25,11 @@ CREATE TYPE audit_action AS ENUM ( 'login', 'logout', 'register', - 'request_password_reset' + 'request_password_reset', + 'connect', + 'disconnect', + 'open', + 'close' ); CREATE TYPE automatic_updates AS ENUM ( @@ -201,7 +205,9 @@ CREATE TYPE resource_type AS ENUM ( 'notification_template', 'idp_sync_settings_organization', 'idp_sync_settings_group', - 'idp_sync_settings_role' + 'idp_sync_settings_role', + 'workspace_agent', + 'workspace_app' ); CREATE TYPE startup_script_behavior AS ENUM ( diff --git a/coderd/database/migrations/000293_add_audit_types_for_connect_and_open.down.sql b/coderd/database/migrations/000293_add_audit_types_for_connect_and_open.down.sql new file mode 100644 index 0000000000000..35020b349fc4e --- /dev/null +++ b/coderd/database/migrations/000293_add_audit_types_for_connect_and_open.down.sql @@ -0,0 +1 @@ +-- No-op, enum values can't be dropped. diff --git a/coderd/database/migrations/000293_add_audit_types_for_connect_and_open.up.sql b/coderd/database/migrations/000293_add_audit_types_for_connect_and_open.up.sql new file mode 100644 index 0000000000000..b894a45eaf443 --- /dev/null +++ b/coderd/database/migrations/000293_add_audit_types_for_connect_and_open.up.sql @@ -0,0 +1,13 @@ +-- Add new audit types for connect and open actions. +ALTER TYPE audit_action + ADD VALUE IF NOT EXISTS 'connect'; +ALTER TYPE audit_action + ADD VALUE IF NOT EXISTS 'disconnect'; +ALTER TYPE resource_type + ADD VALUE IF NOT EXISTS 'workspace_agent'; +ALTER TYPE audit_action + ADD VALUE IF NOT EXISTS 'open'; +ALTER TYPE audit_action + ADD VALUE IF NOT EXISTS 'close'; +ALTER TYPE resource_type + ADD VALUE IF NOT EXISTS 'workspace_app'; diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 78f6285e3c11a..4c323fd91c1de 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -467,6 +467,7 @@ func (q *sqlQuerier) GetAuthorizedAuditLogsOffset(ctx context.Context, arg GetAu arg.DateFrom, arg.DateTo, arg.BuildReason, + arg.RequestID, arg.OffsetOpt, arg.LimitOpt, ) diff --git a/coderd/database/models.go b/coderd/database/models.go index fc11e1f4f5ebe..9ddcba7897699 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -147,6 +147,10 @@ const ( AuditActionLogout AuditAction = "logout" AuditActionRegister AuditAction = "register" AuditActionRequestPasswordReset AuditAction = "request_password_reset" + AuditActionConnect AuditAction = "connect" + AuditActionDisconnect AuditAction = "disconnect" + AuditActionOpen AuditAction = "open" + AuditActionClose AuditAction = "close" ) func (e *AuditAction) Scan(src interface{}) error { @@ -194,7 +198,11 @@ func (e AuditAction) Valid() bool { AuditActionLogin, AuditActionLogout, AuditActionRegister, - AuditActionRequestPasswordReset: + AuditActionRequestPasswordReset, + AuditActionConnect, + AuditActionDisconnect, + AuditActionOpen, + AuditActionClose: return true } return false @@ -211,6 +219,10 @@ func AllAuditActionValues() []AuditAction { AuditActionLogout, AuditActionRegister, AuditActionRequestPasswordReset, + AuditActionConnect, + AuditActionDisconnect, + AuditActionOpen, + AuditActionClose, } } @@ -1608,6 +1620,8 @@ const ( ResourceTypeIdpSyncSettingsOrganization ResourceType = "idp_sync_settings_organization" ResourceTypeIdpSyncSettingsGroup ResourceType = "idp_sync_settings_group" ResourceTypeIdpSyncSettingsRole ResourceType = "idp_sync_settings_role" + ResourceTypeWorkspaceAgent ResourceType = "workspace_agent" + ResourceTypeWorkspaceApp ResourceType = "workspace_app" ) func (e *ResourceType) Scan(src interface{}) error { @@ -1668,7 +1682,9 @@ func (e ResourceType) Valid() bool { ResourceTypeNotificationTemplate, ResourceTypeIdpSyncSettingsOrganization, ResourceTypeIdpSyncSettingsGroup, - ResourceTypeIdpSyncSettingsRole: + ResourceTypeIdpSyncSettingsRole, + ResourceTypeWorkspaceAgent, + ResourceTypeWorkspaceApp: return true } return false @@ -1698,6 +1714,8 @@ func AllResourceTypeValues() []ResourceType { ResourceTypeIdpSyncSettingsOrganization, ResourceTypeIdpSyncSettingsGroup, ResourceTypeIdpSyncSettingsRole, + ResourceTypeWorkspaceAgent, + ResourceTypeWorkspaceApp, } } diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 2d7fe83296deb..d472566849b2b 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -558,6 +558,12 @@ WHERE workspace_builds.reason::text = $11 ELSE true END + -- Filter request_id + AND CASE + WHEN $12 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + audit_logs.request_id = $12 + ELSE true + END -- Authorize Filter clause will be injected below in GetAuthorizedAuditLogsOffset -- @authorize_filter @@ -567,9 +573,9 @@ LIMIT -- a limit of 0 means "no limit". The audit log table is unbounded -- in size, and is expected to be quite large. Implement a default -- limit of 100 to prevent accidental excessively large queries. - COALESCE(NULLIF($13 :: int, 0), 100) + COALESCE(NULLIF($14 :: int, 0), 100) OFFSET - $12 + $13 ` type GetAuditLogsOffsetParams struct { @@ -584,6 +590,7 @@ type GetAuditLogsOffsetParams struct { DateFrom time.Time `db:"date_from" json:"date_from"` DateTo time.Time `db:"date_to" json:"date_to"` BuildReason string `db:"build_reason" json:"build_reason"` + RequestID uuid.UUID `db:"request_id" json:"request_id"` OffsetOpt int32 `db:"offset_opt" json:"offset_opt"` LimitOpt int32 `db:"limit_opt" json:"limit_opt"` } @@ -624,6 +631,7 @@ func (q *sqlQuerier) GetAuditLogsOffset(ctx context.Context, arg GetAuditLogsOff arg.DateFrom, arg.DateTo, arg.BuildReason, + arg.RequestID, arg.OffsetOpt, arg.LimitOpt, ) diff --git a/coderd/database/queries/auditlogs.sql b/coderd/database/queries/auditlogs.sql index 115bdcd4c8f6f..52efc40c73738 100644 --- a/coderd/database/queries/auditlogs.sql +++ b/coderd/database/queries/auditlogs.sql @@ -117,6 +117,12 @@ WHERE workspace_builds.reason::text = @build_reason ELSE true END + -- Filter request_id + AND CASE + WHEN @request_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + audit_logs.request_id = @request_id + ELSE true + END -- Authorize Filter clause will be injected below in GetAuthorizedAuditLogsOffset -- @authorize_filter diff --git a/coderd/searchquery/search.go b/coderd/searchquery/search.go index a4fe5d4775d6c..849dd7f584947 100644 --- a/coderd/searchquery/search.go +++ b/coderd/searchquery/search.go @@ -19,6 +19,20 @@ import ( // AuditLogs requires the database to fetch an organization by name // to convert to organization uuid. +// +// Supported query parameters: +// +// - request_id: UUID (can be used to search for associated audits e.g. connect/disconnect or open/close) +// - resource_id: UUID +// - resource_target: string +// - username: string +// - email: string +// - date_from: string (date in format "2006-01-02") +// - date_to: string (date in format "2006-01-02") +// - organization: string (organization UUID or name) +// - resource_type: string (enum) +// - action: string (enum) +// - build_reason: string (enum) func AuditLogs(ctx context.Context, db database.Store, query string) (database.GetAuditLogsOffsetParams, []codersdk.ValidationError) { // Always lowercase for all searches. query = strings.ToLower(query) @@ -33,6 +47,7 @@ func AuditLogs(ctx context.Context, db database.Store, query string) (database.G const dateLayout = "2006-01-02" parser := httpapi.NewQueryParamParser() filter := database.GetAuditLogsOffsetParams{ + RequestID: parser.UUID(values, uuid.Nil, "request_id"), ResourceID: parser.UUID(values, uuid.Nil, "resource_id"), ResourceTarget: parser.String(values, "", "resource_target"), Username: parser.String(values, "", "username"), diff --git a/coderd/searchquery/search_test.go b/coderd/searchquery/search_test.go index 91d285afbd8ec..0a8e08e3d45fe 100644 --- a/coderd/searchquery/search_test.go +++ b/coderd/searchquery/search_test.go @@ -344,6 +344,11 @@ func TestSearchAudit(t *testing.T) { ResourceTarget: "foo", }, }, + { + Name: "RequestID", + Query: "request_id:foo", + ExpectedErrorContains: "valid uuid", + }, } for _, c := range testCases { diff --git a/codersdk/audit.go b/codersdk/audit.go index 307eeb275b61c..1df5bd2d10e2c 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -37,6 +37,8 @@ const ( ResourceTypeIdpSyncSettingsOrganization ResourceType = "idp_sync_settings_organization" ResourceTypeIdpSyncSettingsGroup ResourceType = "idp_sync_settings_group" ResourceTypeIdpSyncSettingsRole ResourceType = "idp_sync_settings_role" + ResourceTypeWorkspaceAgent ResourceType = "workspace_agent" + ResourceTypeWorkspaceApp ResourceType = "workspace_app" ) func (r ResourceType) FriendlyString() string { @@ -87,6 +89,10 @@ func (r ResourceType) FriendlyString() string { return "settings" case ResourceTypeIdpSyncSettingsRole: return "settings" + case ResourceTypeWorkspaceAgent: + return "workspace agent" + case ResourceTypeWorkspaceApp: + return "workspace app" default: return "unknown" } @@ -104,6 +110,10 @@ const ( AuditActionLogout AuditAction = "logout" AuditActionRegister AuditAction = "register" AuditActionRequestPasswordReset AuditAction = "request_password_reset" + AuditActionConnect AuditAction = "connect" + AuditActionDisconnect AuditAction = "disconnect" + AuditActionOpen AuditAction = "open" + AuditActionClose AuditAction = "close" ) func (a AuditAction) Friendly() string { @@ -126,6 +136,14 @@ func (a AuditAction) Friendly() string { return "registered" case AuditActionRequestPasswordReset: return "password reset requested" + case AuditActionConnect: + return "connected" + case AuditActionDisconnect: + return "disconnected" + case AuditActionOpen: + return "opened" + case AuditActionClose: + return "closed" default: return "unknown" } @@ -184,6 +202,7 @@ type CreateTestAuditLogRequest struct { Time time.Time `json:"time,omitempty" format:"date-time"` BuildReason BuildReason `json:"build_reason,omitempty" enums:"autostart,autostop,initiator"` OrganizationID uuid.UUID `json:"organization_id,omitempty" format:"uuid"` + RequestID uuid.UUID `json:"request_id,omitempty" format:"uuid"` } // AuditLogs retrieves audit logs from the given page. diff --git a/docs/admin/security/audit-logs.md b/docs/admin/security/audit-logs.md index 2131e7746d2d6..5c6a6e6a802a1 100644 --- a/docs/admin/security/audit-logs.md +++ b/docs/admin/security/audit-logs.md @@ -29,6 +29,8 @@ We track the following resources: | Template
write, delete | |
FieldTracked
active_version_idtrue
activity_bumptrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
autostart_block_days_of_weektrue
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
default_ttltrue
deletedfalse
deprecatedtrue
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_port_sharing_leveltrue
nametrue
organization_display_namefalse
organization_iconfalse
organization_idfalse
organization_namefalse
provisionertrue
require_active_versiontrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
user_acltrue
| | TemplateVersion
create, write | |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
external_auth_providersfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
source_example_idfalse
template_idtrue
updated_atfalse
| | User
create, write, delete | |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
github_com_user_idfalse
hashed_one_time_passcodefalse
hashed_passwordtrue
idtrue
last_seen_atfalse
login_typetrue
nametrue
one_time_passcode_expires_attrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
theme_preferencefalse
updated_atfalse
usernametrue
| +| WorkspaceAgent
connect, disconnect | |
FieldTracked
api_versionfalse
architecturefalse
auth_instance_idfalse
auth_tokenfalse
connection_timeout_secondsfalse
created_atfalse
directoryfalse
disconnected_atfalse
display_appsfalse
display_orderfalse
environment_variablesfalse
expanded_directoryfalse
first_connected_atfalse
idfalse
instance_metadatafalse
last_connected_atfalse
last_connected_replica_idfalse
lifecycle_statefalse
logs_lengthfalse
logs_overflowedfalse
motd_filefalse
namefalse
operating_systemfalse
ready_atfalse
resource_idfalse
resource_metadatafalse
started_atfalse
subsystemsfalse
troubleshooting_urlfalse
updated_atfalse
versionfalse
| +| WorkspaceApp
open, close | |
FieldTracked
agent_idfalse
commandfalse
created_atfalse
display_namefalse
display_orderfalse
externalfalse
healthfalse
healthcheck_intervalfalse
healthcheck_thresholdfalse
healthcheck_urlfalse
hiddenfalse
iconfalse
idfalse
open_infalse
sharing_levelfalse
slugfalse
subdomainfalse
urlfalse
| | WorkspaceBuild
start, stop | |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
template_version_preset_idfalse
transitionfalse
updated_atfalse
workspace_idfalse
| | WorkspaceProxy
| |
FieldTracked
created_attrue
deletedfalse
derp_enabledtrue
derp_onlytrue
display_nametrue
icontrue
idtrue
nametrue
region_idtrue
token_hashed_secrettrue
updated_atfalse
urltrue
versiontrue
wildcard_hostnametrue
| | WorkspaceTable
| |
FieldTracked
automatic_updatestrue
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
dormant_attrue
favoritetrue
idtrue
last_used_atfalse
nametrue
next_start_attrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
| diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 7b2759e281f8e..f892c27e00d55 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -554,6 +554,10 @@ | `logout` | | `register` | | `request_password_reset` | +| `connect` | +| `disconnect` | +| `open` | +| `close` | ## codersdk.AuditDiff @@ -1314,6 +1318,7 @@ This is required on creation to enable a user-flow of validating a template work ], "build_reason": "autostart", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "request_id": "266ea41d-adf5-480b-af50-15b940c2b846", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "resource_type": "template", "time": "2019-08-24T14:15:22Z" @@ -1328,6 +1333,7 @@ This is required on creation to enable a user-flow of validating a template work | `additional_fields` | array of integer | false | | | | `build_reason` | [codersdk.BuildReason](#codersdkbuildreason) | false | | | | `organization_id` | string | false | | | +| `request_id` | string | false | | | | `resource_id` | string | false | | | | `resource_type` | [codersdk.ResourceType](#codersdkresourcetype) | false | | | | `time` | string | false | | | @@ -5358,6 +5364,8 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | `idp_sync_settings_organization` | | `idp_sync_settings_group` | | `idp_sync_settings_role` | +| `workspace_agent` | +| `workspace_app` | ## codersdk.Response diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index d43b2e224e374..b9367a6038e85 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -27,6 +27,8 @@ 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 @@ -307,6 +309,59 @@ 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, + }, + &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_order": ActionIgnore, + "hidden": ActionIgnore, + "open_in": ActionIgnore, + }, } // auditMap converts a map of struct pointers to a map of struct names as diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 09541c9767b89..012214a5124ac 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -115,10 +115,14 @@ export interface AssignableRoles extends Role { // From codersdk/audit.go export type AuditAction = + | "close" + | "connect" | "create" | "delete" + | "disconnect" | "login" | "logout" + | "open" | "register" | "request_password_reset" | "start" @@ -126,10 +130,14 @@ export type AuditAction = | "write"; export const AuditActions: AuditAction[] = [ + "close", + "connect", "create", "delete", + "disconnect", "login", "logout", + "open", "register", "request_password_reset", "start", @@ -405,6 +413,7 @@ export interface CreateTestAuditLogRequest { readonly time?: string; readonly build_reason?: BuildReason; readonly organization_id?: string; + readonly request_id?: string; } // From codersdk/apikey.go @@ -1997,6 +2006,8 @@ export type ResourceType = | "template_version" | "user" | "workspace" + | "workspace_agent" + | "workspace_app" | "workspace_build" | "workspace_proxy"; @@ -2021,6 +2032,8 @@ export const ResourceTypes: ResourceType[] = [ "template_version", "user", "workspace", + "workspace_agent", + "workspace_app", "workspace_build", "workspace_proxy", ];