mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add connectionlogs API (#18628)
This is the second PR for moving connection events out of the audit log.
This PR:
- Adds the `/api/v2/connectionlog` endpoint
- Adds filtering for `GetAuthorizedConnectionLogsOffset` and thus the endpoint.
There's quite a few, but I was aiming for feature parity with the audit log.
1. `organization:<id|name>`
2. `workspace_owner:<username>`
3. `workspace_owner_email:<email>`
4. `type:<ssh|vscode|jetbrains|reconnecting_pty|workspace_app|port_forwarding>`
5. `username:<username>`
- Only includes web-based connection events (workspace apps, web port forwarding) as only those include user metadata.
6. `user_email:<email>`
7. `connected_after:<time>`
8. `connected_before:<time>`
9. `workspace_id:<id>`
10. `connection_id:<id>`
- If you have one snapshot of the connection log, and some sessions are ongoing in that snapshot, you could use this filter to check if they've been closed since.
11. `status:<connected|disconnected>`
- If `connected` only sessions with a null `close_time` are returned, if `disconnected`, only those with a non-null `close_time`. If filter is omitted, both are returned.
Future PRs:
- Populate `count` on `ConnectionLogResponse` using a seperate query (to preemptively mitigate the issue described in #17689)
- Implement a table in the Web UI for viewing connection logs.
- Write a query to delete old events from the audit log, call it from dbpurge.
- Write documentation for the endpoint / feature (including these filters)
This commit is contained in:
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/provisionerdserver"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/provisionersdk"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
@@ -2245,6 +2246,249 @@ func TestGetAuthorizedConnectionLogsOffset(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestConnectionLogsOffsetFilters(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
|
||||
orgA := dbfake.Organization(t, db).Do()
|
||||
orgB := dbfake.Organization(t, db).Do()
|
||||
|
||||
user1 := dbgen.User(t, db, database.User{
|
||||
Username: "user1",
|
||||
Email: "user1@test.com",
|
||||
})
|
||||
user2 := dbgen.User(t, db, database.User{
|
||||
Username: "user2",
|
||||
Email: "user2@test.com",
|
||||
})
|
||||
user3 := dbgen.User(t, db, database.User{
|
||||
Username: "user3",
|
||||
Email: "user3@test.com",
|
||||
})
|
||||
|
||||
ws1Tpl := dbgen.Template(t, db, database.Template{OrganizationID: orgA.Org.ID, CreatedBy: user1.ID})
|
||||
ws1 := dbgen.Workspace(t, db, database.WorkspaceTable{
|
||||
OwnerID: user1.ID,
|
||||
OrganizationID: orgA.Org.ID,
|
||||
TemplateID: ws1Tpl.ID,
|
||||
})
|
||||
ws2Tpl := dbgen.Template(t, db, database.Template{OrganizationID: orgB.Org.ID, CreatedBy: user2.ID})
|
||||
ws2 := dbgen.Workspace(t, db, database.WorkspaceTable{
|
||||
OwnerID: user2.ID,
|
||||
OrganizationID: orgB.Org.ID,
|
||||
TemplateID: ws2Tpl.ID,
|
||||
})
|
||||
|
||||
now := dbtime.Now()
|
||||
log1ConnID := uuid.New()
|
||||
log1 := dbgen.ConnectionLog(t, db, database.UpsertConnectionLogParams{
|
||||
Time: now.Add(-4 * time.Hour),
|
||||
OrganizationID: ws1.OrganizationID,
|
||||
WorkspaceOwnerID: ws1.OwnerID,
|
||||
WorkspaceID: ws1.ID,
|
||||
WorkspaceName: ws1.Name,
|
||||
Type: database.ConnectionTypeWorkspaceApp,
|
||||
ConnectionStatus: database.ConnectionStatusConnected,
|
||||
UserID: uuid.NullUUID{UUID: user1.ID, Valid: true},
|
||||
UserAgent: sql.NullString{String: "Mozilla/5.0", Valid: true},
|
||||
SlugOrPort: sql.NullString{String: "code-server", Valid: true},
|
||||
ConnectionID: uuid.NullUUID{UUID: log1ConnID, Valid: true},
|
||||
})
|
||||
|
||||
log2ConnID := uuid.New()
|
||||
log2 := dbgen.ConnectionLog(t, db, database.UpsertConnectionLogParams{
|
||||
Time: now.Add(-3 * time.Hour),
|
||||
OrganizationID: ws1.OrganizationID,
|
||||
WorkspaceOwnerID: ws1.OwnerID,
|
||||
WorkspaceID: ws1.ID,
|
||||
WorkspaceName: ws1.Name,
|
||||
Type: database.ConnectionTypeVscode,
|
||||
ConnectionStatus: database.ConnectionStatusConnected,
|
||||
ConnectionID: uuid.NullUUID{UUID: log2ConnID, Valid: true},
|
||||
})
|
||||
|
||||
// Mark log2 as disconnected
|
||||
log2 = dbgen.ConnectionLog(t, db, database.UpsertConnectionLogParams{
|
||||
Time: now.Add(-2 * time.Hour),
|
||||
ConnectionID: log2.ConnectionID,
|
||||
WorkspaceID: ws1.ID,
|
||||
WorkspaceOwnerID: ws1.OwnerID,
|
||||
AgentName: log2.AgentName,
|
||||
ConnectionStatus: database.ConnectionStatusDisconnected,
|
||||
|
||||
OrganizationID: log2.OrganizationID,
|
||||
})
|
||||
|
||||
log3ConnID := uuid.New()
|
||||
log3 := dbgen.ConnectionLog(t, db, database.UpsertConnectionLogParams{
|
||||
Time: now.Add(-2 * time.Hour),
|
||||
OrganizationID: ws2.OrganizationID,
|
||||
WorkspaceOwnerID: ws2.OwnerID,
|
||||
WorkspaceID: ws2.ID,
|
||||
WorkspaceName: ws2.Name,
|
||||
Type: database.ConnectionTypeSsh,
|
||||
ConnectionStatus: database.ConnectionStatusConnected,
|
||||
UserID: uuid.NullUUID{UUID: user2.ID, Valid: true},
|
||||
ConnectionID: uuid.NullUUID{UUID: log3ConnID, Valid: true},
|
||||
})
|
||||
|
||||
// Mark log3 as disconnected
|
||||
log3 = dbgen.ConnectionLog(t, db, database.UpsertConnectionLogParams{
|
||||
Time: now.Add(-1 * time.Hour),
|
||||
ConnectionID: log3.ConnectionID,
|
||||
WorkspaceOwnerID: log3.WorkspaceOwnerID,
|
||||
WorkspaceID: ws2.ID,
|
||||
AgentName: log3.AgentName,
|
||||
ConnectionStatus: database.ConnectionStatusDisconnected,
|
||||
|
||||
OrganizationID: log3.OrganizationID,
|
||||
})
|
||||
|
||||
log4 := dbgen.ConnectionLog(t, db, database.UpsertConnectionLogParams{
|
||||
Time: now.Add(-1 * time.Hour),
|
||||
OrganizationID: ws2.OrganizationID,
|
||||
WorkspaceOwnerID: ws2.OwnerID,
|
||||
WorkspaceID: ws2.ID,
|
||||
WorkspaceName: ws2.Name,
|
||||
Type: database.ConnectionTypeVscode,
|
||||
ConnectionStatus: database.ConnectionStatusConnected,
|
||||
UserID: uuid.NullUUID{UUID: user3.ID, Valid: true},
|
||||
})
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
params database.GetConnectionLogsOffsetParams
|
||||
expectedLogIDs []uuid.UUID
|
||||
}{
|
||||
{
|
||||
name: "NoFilter",
|
||||
params: database.GetConnectionLogsOffsetParams{},
|
||||
expectedLogIDs: []uuid.UUID{
|
||||
log1.ID, log2.ID, log3.ID, log4.ID,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OrganizationID",
|
||||
params: database.GetConnectionLogsOffsetParams{
|
||||
OrganizationID: orgB.Org.ID,
|
||||
},
|
||||
expectedLogIDs: []uuid.UUID{log3.ID, log4.ID},
|
||||
},
|
||||
{
|
||||
name: "WorkspaceOwner",
|
||||
params: database.GetConnectionLogsOffsetParams{
|
||||
WorkspaceOwner: user1.Username,
|
||||
},
|
||||
expectedLogIDs: []uuid.UUID{log1.ID, log2.ID},
|
||||
},
|
||||
{
|
||||
name: "WorkspaceOwnerID",
|
||||
params: database.GetConnectionLogsOffsetParams{
|
||||
WorkspaceOwnerID: user1.ID,
|
||||
},
|
||||
expectedLogIDs: []uuid.UUID{log1.ID, log2.ID},
|
||||
},
|
||||
{
|
||||
name: "WorkspaceOwnerEmail",
|
||||
params: database.GetConnectionLogsOffsetParams{
|
||||
WorkspaceOwnerEmail: user2.Email,
|
||||
},
|
||||
expectedLogIDs: []uuid.UUID{log3.ID, log4.ID},
|
||||
},
|
||||
{
|
||||
name: "Type",
|
||||
params: database.GetConnectionLogsOffsetParams{
|
||||
Type: string(database.ConnectionTypeVscode),
|
||||
},
|
||||
expectedLogIDs: []uuid.UUID{log2.ID, log4.ID},
|
||||
},
|
||||
{
|
||||
name: "UserID",
|
||||
params: database.GetConnectionLogsOffsetParams{
|
||||
UserID: user1.ID,
|
||||
},
|
||||
expectedLogIDs: []uuid.UUID{log1.ID},
|
||||
},
|
||||
{
|
||||
name: "Username",
|
||||
params: database.GetConnectionLogsOffsetParams{
|
||||
Username: user1.Username,
|
||||
},
|
||||
expectedLogIDs: []uuid.UUID{log1.ID},
|
||||
},
|
||||
{
|
||||
name: "UserEmail",
|
||||
params: database.GetConnectionLogsOffsetParams{
|
||||
UserEmail: user3.Email,
|
||||
},
|
||||
expectedLogIDs: []uuid.UUID{log4.ID},
|
||||
},
|
||||
{
|
||||
name: "ConnectedAfter",
|
||||
params: database.GetConnectionLogsOffsetParams{
|
||||
ConnectedAfter: now.Add(-90 * time.Minute), // 1.5 hours ago
|
||||
},
|
||||
expectedLogIDs: []uuid.UUID{log4.ID},
|
||||
},
|
||||
{
|
||||
name: "ConnectedBefore",
|
||||
params: database.GetConnectionLogsOffsetParams{
|
||||
ConnectedBefore: now.Add(-150 * time.Minute),
|
||||
},
|
||||
expectedLogIDs: []uuid.UUID{log1.ID, log2.ID},
|
||||
},
|
||||
{
|
||||
name: "WorkspaceID",
|
||||
params: database.GetConnectionLogsOffsetParams{
|
||||
WorkspaceID: ws2.ID,
|
||||
},
|
||||
expectedLogIDs: []uuid.UUID{log3.ID, log4.ID},
|
||||
},
|
||||
{
|
||||
name: "ConnectionID",
|
||||
params: database.GetConnectionLogsOffsetParams{
|
||||
ConnectionID: log1.ConnectionID.UUID,
|
||||
},
|
||||
expectedLogIDs: []uuid.UUID{log1.ID},
|
||||
},
|
||||
{
|
||||
name: "StatusOngoing",
|
||||
params: database.GetConnectionLogsOffsetParams{
|
||||
Status: string(codersdk.ConnectionLogStatusOngoing),
|
||||
},
|
||||
expectedLogIDs: []uuid.UUID{log4.ID},
|
||||
},
|
||||
{
|
||||
name: "StatusCompleted",
|
||||
params: database.GetConnectionLogsOffsetParams{
|
||||
Status: string(codersdk.ConnectionLogStatusCompleted),
|
||||
},
|
||||
expectedLogIDs: []uuid.UUID{log2.ID, log3.ID},
|
||||
},
|
||||
{
|
||||
name: "OrganizationAndTypeAndStatus",
|
||||
params: database.GetConnectionLogsOffsetParams{
|
||||
OrganizationID: orgA.Org.ID,
|
||||
Type: string(database.ConnectionTypeVscode),
|
||||
Status: string(codersdk.ConnectionLogStatusCompleted),
|
||||
},
|
||||
expectedLogIDs: []uuid.UUID{log2.ID},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
logs, err := db.GetConnectionLogsOffset(ctx, tc.params)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, tc.expectedLogIDs, connectionOnlyIDs(logs))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func connectionOnlyIDs[T database.ConnectionLog | database.GetConnectionLogsOffsetRow](logs []T) []uuid.UUID {
|
||||
ids := make([]uuid.UUID, 0, len(logs))
|
||||
for _, log := range logs {
|
||||
@@ -2313,7 +2557,7 @@ func TestUpsertConnectionLog(t *testing.T) {
|
||||
log1, err := db.UpsertConnectionLog(ctx, connectParams)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, connectParams.ID, log1.ID)
|
||||
require.False(t, log1.DisconnectTime.Valid, "CloseTime should not be set on connect")
|
||||
require.False(t, log1.DisconnectTime.Valid, "DisconnectTime should not be set on connect")
|
||||
|
||||
// Check that one row exists.
|
||||
rows, err := db.GetConnectionLogsOffset(ctx, database.GetConnectionLogsOffsetParams{LimitOpt: 10})
|
||||
|
||||
Reference in New Issue
Block a user