Files
coder/coderd/audit_test.go
T
Michael Suchacz 24b83dc727 feat: add organization picker to MCP server settings pages (#28150)
Adds an organization picker to the MCP server settings pages so
multi-org deployments can manage every organization's MCP servers
through the UI, not just the default organization's. Closes CODAGT-714
(scoped to MCP servers only; model configs stay deployment-wide until
CODAGT-709 lands).

## Stack Context

Top of the CODAGT-711/717/712/806 stack: MCP server configs are
org-scoped with RBAC, audit logging, per-config ACLs, and owner-scoped
user tokens. This layer makes the admin UI org-aware, resolving the
"default org only" limitation documented in the bottom PR.

## Summary

- The MCP servers list and add pages resolve the working organization
from an `org` search param (falling back to the default org), and render
an organization picker built on the existing `OrganizationAutocomplete`.
The picker only appears when more than one organization is visible, so
single-org deployments see no change.
- Selecting an organization rewrites the search param, refetches the
list for that org, and threads through to the add page so creation posts
to the selected organization.
- The update page now loads the server by ID (new organization-scoped
`GET /api/experimental/organizations/{organization}/mcp-servers/{id}`
frontend binding) instead of listing the default org and searching
client-side, so servers from any organization can be edited. Post-save
navigation returns to the owning organization's list via the server's
`organization_id`.
- Removes the now-unused `getDefaultOrganizationId` helper.
- Review hardening: organization display labels are disambiguated with
the unique org name when effective display names collide (picker
options, trigger text, and accessible name). Access is no longer
restricted to deployment admins: the pages, sidebar item, and nav entry
accept org-scoped `mcp_server_config` permissions, the picker lists only
organizations the user can view, content and affordances (add, edit,
delete) are gated on the selected organization's permissions, and
deployment admins keep full access everywhere.

## Testing

Storybook interaction stories cover: picker hidden with one org,
switching orgs refetches and swaps list contents, add posting to the
URL-selected org, add posting after an in-form org switch (asserting
typed form state survives the switch), and update loading by ID.
Red-green verified by breaking the search-param resolution and observing
the new stories fail.

> Mux acted on Mike's behalf for this PR.
2026-08-19 19:46:23 +00:00

673 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())
organization := dbgen.Organization(t, db, database.Organization{
Name: "mcp-audit-link-org",
})
require.NotEqual(t, user.OrganizationID, organization.ID)
orgAdmin, _ := coderdtest.CreateAnotherUser(t, client, organization.ID, rbac.ScopedRoleOrgAdmin(organization.ID))
config := dbgen.MCPServerConfig(t, db, database.MCPServerConfig{
OrganizationID: organization.ID,
})
err := client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{
Action: codersdk.AuditActionCreate,
ResourceType: codersdk.ResourceTypeMCPServerConfig,
ResourceID: config.ID,
OrganizationID: organization.ID,
})
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 management permissions, and the
// settings page now admits them.
orgAdminLogs, err := orgAdmin.AuditLogs(ctx, codersdk.AuditLogsRequest{
Pagination: codersdk.Pagination{
Limit: 1,
},
})
require.NoError(t, err)
require.Len(t, orgAdminLogs.AuditLogs, 1)
require.Equal(t, fmt.Sprintf("/ai/settings/mcp-servers/%s?org=mcp-audit-link-org", config.ID), 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?org=mcp-audit-link-org", 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)
}