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:
@@ -86,6 +86,46 @@ func AuditLogs(ctx context.Context, db database.Store, query string) (database.G
|
||||
return filter, countFilter, parser.Errors
|
||||
}
|
||||
|
||||
func ConnectionLogs(ctx context.Context, db database.Store, query string, apiKey database.APIKey) (database.GetConnectionLogsOffsetParams, []codersdk.ValidationError) {
|
||||
// Always lowercase for all searches.
|
||||
query = strings.ToLower(query)
|
||||
values, errors := searchTerms(query, func(term string, values url.Values) error {
|
||||
values.Add("search", term)
|
||||
return nil
|
||||
})
|
||||
if len(errors) > 0 {
|
||||
return database.GetConnectionLogsOffsetParams{}, errors
|
||||
}
|
||||
|
||||
parser := httpapi.NewQueryParamParser()
|
||||
filter := database.GetConnectionLogsOffsetParams{
|
||||
OrganizationID: parseOrganization(ctx, db, parser, values, "organization"),
|
||||
WorkspaceOwner: parser.String(values, "", "workspace_owner"),
|
||||
WorkspaceOwnerEmail: parser.String(values, "", "workspace_owner_email"),
|
||||
Type: string(httpapi.ParseCustom(parser, values, "", "type", httpapi.ParseEnum[database.ConnectionType])),
|
||||
Username: parser.String(values, "", "username"),
|
||||
UserEmail: parser.String(values, "", "user_email"),
|
||||
ConnectedAfter: parser.Time3339Nano(values, time.Time{}, "connected_after"),
|
||||
ConnectedBefore: parser.Time3339Nano(values, time.Time{}, "connected_before"),
|
||||
WorkspaceID: parser.UUID(values, uuid.Nil, "workspace_id"),
|
||||
ConnectionID: parser.UUID(values, uuid.Nil, "connection_id"),
|
||||
Status: string(httpapi.ParseCustom(parser, values, "", "status", httpapi.ParseEnum[codersdk.ConnectionLogStatus])),
|
||||
}
|
||||
|
||||
if filter.Username == "me" {
|
||||
filter.UserID = apiKey.UserID
|
||||
filter.Username = ""
|
||||
}
|
||||
|
||||
if filter.WorkspaceOwner == "me" {
|
||||
filter.WorkspaceOwnerID = apiKey.UserID
|
||||
filter.WorkspaceOwner = ""
|
||||
}
|
||||
|
||||
parser.ErrorExcessParams(values)
|
||||
return filter, parser.Errors
|
||||
}
|
||||
|
||||
func Users(query string) (database.GetUsersParams, []codersdk.ValidationError) {
|
||||
// Always lowercase for all searches.
|
||||
query = strings.ToLower(query)
|
||||
|
||||
@@ -408,6 +408,72 @@ func TestSearchAudit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchConnectionLogs(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("All", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
orgID := uuid.New()
|
||||
workspaceOwnerID := uuid.New()
|
||||
workspaceID := uuid.New()
|
||||
connectionID := uuid.New()
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
dbgen.Organization(t, db, database.Organization{
|
||||
ID: orgID,
|
||||
Name: "testorg",
|
||||
})
|
||||
dbgen.User(t, db, database.User{
|
||||
ID: workspaceOwnerID,
|
||||
Username: "testowner",
|
||||
Email: "owner@example.com",
|
||||
})
|
||||
|
||||
query := fmt.Sprintf(`organization:testorg workspace_owner:testowner `+
|
||||
`workspace_owner_email:owner@example.com type:port_forwarding username:testuser `+
|
||||
`user_email:test@example.com connected_after:"2023-01-01T00:00:00Z" `+
|
||||
`connected_before:"2023-01-16T12:00:00+12:00" workspace_id:%s connection_id:%s status:ongoing`,
|
||||
workspaceID.String(), connectionID.String())
|
||||
|
||||
values, errs := searchquery.ConnectionLogs(context.Background(), db, query, database.APIKey{})
|
||||
require.Len(t, errs, 0)
|
||||
|
||||
expected := database.GetConnectionLogsOffsetParams{
|
||||
OrganizationID: orgID,
|
||||
WorkspaceOwner: "testowner",
|
||||
WorkspaceOwnerEmail: "owner@example.com",
|
||||
Type: string(database.ConnectionTypePortForwarding),
|
||||
Username: "testuser",
|
||||
UserEmail: "test@example.com",
|
||||
ConnectedAfter: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
ConnectedBefore: time.Date(2023, 1, 16, 0, 0, 0, 0, time.UTC),
|
||||
WorkspaceID: workspaceID,
|
||||
ConnectionID: connectionID,
|
||||
Status: string(codersdk.ConnectionLogStatusOngoing),
|
||||
}
|
||||
|
||||
require.Equal(t, expected, values)
|
||||
})
|
||||
|
||||
t.Run("Me", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
userID := uuid.New()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
|
||||
query := `username:me workspace_owner:me`
|
||||
values, errs := searchquery.ConnectionLogs(context.Background(), db, query, database.APIKey{UserID: userID})
|
||||
require.Len(t, errs, 0)
|
||||
|
||||
expected := database.GetConnectionLogsOffsetParams{
|
||||
UserID: userID,
|
||||
WorkspaceOwnerID: userID,
|
||||
}
|
||||
|
||||
require.Equal(t, expected, values)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSearchUsers(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := []struct {
|
||||
|
||||
Reference in New Issue
Block a user