mirror of
https://github.com/coder/coder.git
synced 2025-03-14 10:09:57 +00:00
This commit adds new audit resource types for workspace agents and workspace apps, as well as connect/disconnect and open/close actions. The idea is that we will log new audit events for connecting to the agent via SSH/editor. Likewise, we will log openings of `coder_app`s. This change also introduces support for filtering by `request_id`. Updates #15139
535 lines
16 KiB
Go
535 lines
16 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/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/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("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 {
|
|
testCase := testCase
|
|
// 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,
|
|
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",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|