Files
coder/coderd/audit_test.go
T
Michael Suchacz 299e72ad30 feat: audit MCP server config changes (#27943)
Adds enterprise audit logging for MCP server config create, update, and
delete, with strict secret redaction. MCP configs hold credentials
(OAuth2 client secrets, API keys, custom headers), so admin changes to
them need an audit trail.

## Summary

- `enterprise/audit/table.go` gains an `MCPServerConfig` entry
enumerating every column: `oauth2_client_secret`, `api_key_value`, and
`custom_headers` are `ActionSecret` (never appear in diffs); dbcrypt
`*_key_id` bookkeeping, IDs, and timestamps are ignored; the remaining
config fields, including the endpoint URL fields, are tracked so
auditors can see which endpoints a config points at.
- Type registration in `coderd/audit` (diff, request, resource target
with org attribution), `codersdk/audit.go`, and a `resource_type` enum
migration.
- Handlers wire `audit.InitRequest`: create records `New`; update and
delete record `Old` from the param middleware before the
write-authorization check, so a readable-but-not-writable caller
produces an audited 403 while read-denied callers stay concealed as
unaudited 404s.
- Tests: create/update/delete audit entries, write-denied and
delete-denied 403 auditing, cross-org concealment producing zero
entries, and a serializer-level regression test proving none of the
three secret classes can reach a serialized diff.
- Review round: MCP config audit entries link to
`/ai/settings/mcp-servers/{id}`, audit table comments are trimmed per
review, and a fault-injection test pins that a config row surviving a
failed post-discovery credential update still gets its creation audit
entry.

Stacked on #27942 (org-scoped MCP configs). Part of the MCP
org-separation stack.

Closes https://linear.app/codercom/issue/CODAGT-717

UAT: verified on a trial-licensed dogfood instance: audit entries for
the full CRUD lifecycle with correct actor/org/target, redacted secrets
in the update and OAuth2 create diffs, and a full plaintext scan of the
audit dump finding zero secret leaks.

> Mux (AI agent) authored this PR on Mike's behalf.

<!-- mux-attribution: model=claude-fable-5 thinking=high -->
2026-08-19 19:11:52 +00:00

669 lines
22 KiB
Go

package coderd_test
import (
"context"
"encoding/json"
"fmt"
"strconv"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"cdr.dev/slog/v3/sloggers/slogtest"
"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/database/dbtestutil"
"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) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
err := client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{
ResourceID: user.UserID,
OrganizationID: user.OrganizationID,
})
require.NoError(t, err)
alogs, err := client.AuditLogs(ctx, codersdk.AuditLogsRequest{
Pagination: codersdk.Pagination{
Limit: 1,
},
})
require.NoError(t, err)
require.Equal(t, int64(1), alogs.Count)
require.Len(t, alogs.AuditLogs, 1)
})
t.Run("IncludeUser", func(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
client2, user2 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleOwner())
err := client2.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{
ResourceID: user2.ID,
OrganizationID: user.OrganizationID,
})
require.NoError(t, err)
alogs, err := client.AuditLogs(ctx, codersdk.AuditLogsRequest{
Pagination: codersdk.Pagination{
Limit: 1,
},
})
require.NoError(t, err)
require.Equal(t, int64(1), alogs.Count)
require.Len(t, alogs.AuditLogs, 1)
// Make sure the returned user is fully populated.
foundUser, err := client.User(ctx, user2.ID.String())
foundUser.OrganizationIDs = []uuid.UUID{} // Not included.
require.NoError(t, err)
require.Equal(t, foundUser, *alogs.AuditLogs[0].User)
// Delete the user and try again. This is a soft delete so nothing should
// change. If users are hard deleted we should get nil, but there is no way
// to test this at the moment.
err = client.DeleteUser(ctx, user2.ID)
require.NoError(t, err)
alogs, err = client.AuditLogs(ctx, codersdk.AuditLogsRequest{
Pagination: codersdk.Pagination{
Limit: 1,
},
})
require.NoError(t, err)
require.Equal(t, int64(1), alogs.Count)
require.Len(t, alogs.AuditLogs, 1)
foundUser, err = client.User(ctx, user2.ID.String())
foundUser.OrganizationIDs = []uuid.UUID{} // Not included.
require.NoError(t, err)
require.Equal(t, foundUser, *alogs.AuditLogs[0].User)
})
t.Run("WorkspaceBuildAuditLink", func(t *testing.T) {
t.Parallel()
var (
ctx = context.Background()
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
buildResourceInfo := audit.AdditionalFields{
WorkspaceName: workspace.Name,
BuildNumber: strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10),
BuildReason: database.BuildReason(string(workspace.LatestBuild.Reason)),
}
wriBytes, err := json.Marshal(buildResourceInfo)
require.NoError(t, err)
err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{
Action: codersdk.AuditActionStop,
ResourceType: codersdk.ResourceTypeWorkspaceBuild,
ResourceID: workspace.LatestBuild.ID,
AdditionalFields: wriBytes,
OrganizationID: user.OrganizationID,
})
require.NoError(t, err)
auditLogs, err := client.AuditLogs(ctx, codersdk.AuditLogsRequest{
Pagination: codersdk.Pagination{
Limit: 1,
},
})
require.NoError(t, err)
buildNumberString := strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10)
require.Equal(t, auditLogs.AuditLogs[0].ResourceLink, fmt.Sprintf("/@%s/%s/builds/%s",
workspace.OwnerName, workspace.Name, buildNumberString))
})
t.Run("MCPServerConfigAuditLink", func(t *testing.T) {
t.Parallel()
ctx := context.Background()
db, ps := dbtestutil.NewDB(t)
client := coderdtest.New(t, &coderdtest.Options{Database: db, Pubsub: ps})
user := coderdtest.CreateFirstUser(t, client)
auditor, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleAuditor())
orgAdmin, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.ScopedRoleOrgAdmin(user.OrganizationID))
config := dbgen.MCPServerConfig(t, db, database.MCPServerConfig{
OrganizationID: user.OrganizationID,
})
err := client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{
Action: codersdk.AuditActionCreate,
ResourceType: codersdk.ResourceTypeMCPServerConfig,
ResourceID: config.ID,
OrganizationID: user.OrganizationID,
})
require.NoError(t, err)
auditorLogs, err := auditor.AuditLogs(ctx, codersdk.AuditLogsRequest{
Pagination: codersdk.Pagination{
Limit: 1,
},
})
require.NoError(t, err)
require.Len(t, auditorLogs.AuditLogs, 1)
require.Empty(t, auditorLogs.AuditLogs[0].ResourceLink)
// Organization admins hold MCP permissions but not the
// deployment-config access the settings page requires.
orgAdminLogs, err := orgAdmin.AuditLogs(ctx, codersdk.AuditLogsRequest{
Pagination: codersdk.Pagination{
Limit: 1,
},
})
require.NoError(t, err)
require.Len(t, orgAdminLogs.AuditLogs, 1)
require.Empty(t, orgAdminLogs.AuditLogs[0].ResourceLink)
ownerLogs, err := client.AuditLogs(ctx, codersdk.AuditLogsRequest{
Pagination: codersdk.Pagination{
Limit: 1,
},
})
require.NoError(t, err)
require.Len(t, ownerLogs.AuditLogs, 1)
require.Equal(t, fmt.Sprintf("/ai/settings/mcp-servers/%s", config.ID), ownerLogs.AuditLogs[0].ResourceLink)
})
t.Run("Organization", func(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, &slogtest.Options{
IgnoreErrors: true,
})
ctx := context.Background()
client := coderdtest.New(t, &coderdtest.Options{
Logger: &logger,
})
owner := coderdtest.CreateFirstUser(t, client)
orgAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgAdmin(owner.OrganizationID))
err := client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{
ResourceID: owner.UserID,
OrganizationID: owner.OrganizationID,
})
require.NoError(t, err)
// Add an extra audit log in another organization
err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{
ResourceID: owner.UserID,
OrganizationID: uuid.New(),
})
require.NoError(t, err)
// Fetching audit logs without an organization selector should only
// return organization audit logs the org admin is an admin of.
alogs, err := orgAdmin.AuditLogs(ctx, codersdk.AuditLogsRequest{
Pagination: codersdk.Pagination{
Limit: 5,
},
})
require.NoError(t, err)
require.Len(t, alogs.AuditLogs, 1)
// Using the organization selector allows the org admin to fetch audit logs
alogs, err = orgAdmin.AuditLogs(ctx, codersdk.AuditLogsRequest{
SearchQuery: fmt.Sprintf("organization:%s", owner.OrganizationID.String()),
Pagination: codersdk.Pagination{
Limit: 5,
},
})
require.NoError(t, err)
require.Len(t, alogs.AuditLogs, 1)
// Also try fetching by organization name
organization, err := orgAdmin.Organization(ctx, owner.OrganizationID)
require.NoError(t, err)
alogs, err = orgAdmin.AuditLogs(ctx, codersdk.AuditLogsRequest{
SearchQuery: fmt.Sprintf("organization:%s", organization.Name),
Pagination: codersdk.Pagination{
Limit: 5,
},
})
require.NoError(t, err)
require.Len(t, alogs.AuditLogs, 1)
})
t.Run("Organization404", func(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, &slogtest.Options{
IgnoreErrors: true,
})
ctx := context.Background()
client := coderdtest.New(t, &coderdtest.Options{
Logger: &logger,
})
owner := coderdtest.CreateFirstUser(t, client)
orgAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgAdmin(owner.OrganizationID))
_, err := orgAdmin.AuditLogs(ctx, codersdk.AuditLogsRequest{
SearchQuery: fmt.Sprintf("organization:%s", "random-name"),
Pagination: codersdk.Pagination{
Limit: 5,
},
})
require.Error(t, err)
})
}
func TestAuditLogsFilter(t *testing.T) {
t.Parallel()
t.Run("Filter", func(t *testing.T) {
t.Parallel()
var (
ctx = context.Background()
client = coderdtest.New(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)
// Create two logs with "Create"
err := client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{
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{
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{
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{
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{
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)
// Test cases
testCases := []struct {
Name string
SearchQuery string
ExpectedResult int
ExpectedError bool
}{
{
Name: "FilterByCreateAction",
SearchQuery: "action:create",
ExpectedResult: 2,
},
{
Name: "FilterByDeleteAction",
SearchQuery: "action:delete",
ExpectedResult: 1,
},
{
Name: "FilterByUserResourceType",
SearchQuery: "resource_type:user",
ExpectedResult: 2,
},
{
Name: "FilterByTemplateResourceType",
SearchQuery: "resource_type:template",
ExpectedResult: 1,
},
{
Name: "FilterByEmail",
SearchQuery: "email:" + coderdtest.FirstUserParams.Email,
ExpectedResult: 9,
},
{
Name: "FilterByUsername",
SearchQuery: "username:" + coderdtest.FirstUserParams.Username,
ExpectedResult: 9,
},
{
Name: "FilterByResourceID",
SearchQuery: "resource_id:" + user.UserID.String(),
ExpectedResult: 2,
},
{
Name: "FilterInvalidSingleValue",
SearchQuery: "invalid",
ExpectedError: true,
},
{
Name: "FilterWithInvalidResourceType",
SearchQuery: "resource_type:invalid",
ExpectedError: true,
},
{
Name: "FilterWithInvalidAction",
SearchQuery: "action:invalid",
ExpectedError: true,
},
{
Name: "FilterOnCreateSingleDay",
SearchQuery: "action:create date_from:2022-08-15 date_to:2022-08-15",
ExpectedResult: 1,
},
{
Name: "FilterOnCreateDateFrom",
SearchQuery: "action:create date_from:2022-08-15",
ExpectedResult: 2,
},
{
Name: "FilterOnCreateDateTo",
SearchQuery: "action:create date_to:2022-08-15",
ExpectedResult: 1,
},
{
Name: "FilterOnWorkspaceBuildStart",
SearchQuery: "resource_type:workspace_build action:start",
ExpectedResult: 1,
},
{
Name: "FilterOnWorkspaceBuildStop",
SearchQuery: "resource_type:workspace_build action:stop",
ExpectedResult: 1,
},
{
Name: "FilterOnWorkspaceBuildStartByInitiator",
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 {
// Test filtering
t.Run(testCase.Name, func(t *testing.T) {
t.Parallel()
auditLogs, err := client.AuditLogs(ctx, codersdk.AuditLogsRequest{
SearchQuery: testCase.SearchQuery,
})
if testCase.ExpectedError {
require.Error(t, err, "expected error")
} else {
require.NoError(t, err, "fetch audit logs")
require.Len(t, auditLogs.AuditLogs, testCase.ExpectedResult, "expected audit logs returned")
require.Equal(t, testCase.ExpectedResult, int(auditLogs.Count), "expected audit log count returned")
}
})
}
})
}
func completeWithAgentAndApp() *echo.Responses {
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{
{
Type: "compute",
Name: "main",
Agents: []*proto.Agent{
{
Name: "smith",
OperatingSystem: "linux",
Architecture: "i386",
Apps: []*proto.App{
{
Slug: "app",
DisplayName: "App",
},
},
},
},
},
},
},
},
},
},
}
}
// 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)
}