mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(coderd/database): add boundary_sessions and boundary_logs tables (#25441)
RFC: [Bridge ↔ Boundaries Correlation RFC](https://www.notion.so/coderhq/Gateway-and-Firewall-Correlation-RFC-31ad579be592803aa8b3d48348ccdde9) Add up/down migrations and matching sqlc queries for persisting Boundary audit events, as specified in the Bridge/Boundaries Correlation RFC. **Tables:** - `boundary_sessions`: session metadata with `workspace_agent_id` FK, `confined_process_name`, and timestamps (`started_at`, `updated_at`). ID is externally supplied by the Boundary process (no DB-side default). Created lazily when the first log for a session arrives. - `boundary_logs`: individual audit events with `session_id` FK, `sequence_number` (INT, primary ordering key), protocol/method/detail fields, and `matched_rule` (nullable; non-NULL implies allowed). **Indexes (per RFC):** - `(session_id, sequence_number)` for the ordering query path - `(captured_at)` for the retention purge path **Queries:** - `InsertBoundarySession` / `GetBoundarySessionByID` - `InsertBoundaryLog` / `GetBoundaryLogByID` - `ListBoundaryLogsBySessionID` with nullable `seq_after`/`seq_before` exclusive bounds for fetching events between two known interception sequence numbers - `DeleteOldBoundaryLogs` with row limit to avoid long-running transactions **Also includes:** dbgen helpers (`BoundarySession`, `BoundaryLog`), dbauthz implementations (reads gated on `ResourceAuditLog`, deletes on `ResourceSystem`), and all generated wrappers (dbmock, dbmetrics). No callers yet. A follow-up PR will add the dedicated `boundary_log` RBAC resource type. > Generated by Coder Agents
This commit is contained in:
@@ -3549,6 +3549,243 @@ func (q *sqlQuerier) InsertAuditLog(ctx context.Context, arg InsertAuditLogParam
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteOldBoundaryLogs = `-- name: DeleteOldBoundaryLogs :execrows
|
||||
WITH old_logs AS (
|
||||
SELECT id
|
||||
FROM boundary_logs
|
||||
WHERE captured_at < $1::timestamptz
|
||||
ORDER BY captured_at ASC
|
||||
LIMIT $2
|
||||
)
|
||||
DELETE FROM boundary_logs
|
||||
USING old_logs
|
||||
WHERE boundary_logs.id = old_logs.id
|
||||
`
|
||||
|
||||
type DeleteOldBoundaryLogsParams struct {
|
||||
BeforeTime time.Time `db:"before_time" json:"before_time"`
|
||||
LimitCount int32 `db:"limit_count" json:"limit_count"`
|
||||
}
|
||||
|
||||
// Deletes boundary logs older than the given time, bounded by a row limit
|
||||
// to avoid long-running transactions.
|
||||
func (q *sqlQuerier) DeleteOldBoundaryLogs(ctx context.Context, arg DeleteOldBoundaryLogsParams) (int64, error) {
|
||||
result, err := q.db.ExecContext(ctx, deleteOldBoundaryLogs, arg.BeforeTime, arg.LimitCount)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected()
|
||||
}
|
||||
|
||||
const getBoundaryLogByID = `-- name: GetBoundaryLogByID :one
|
||||
SELECT id, session_id, sequence_number, captured_at, created_at, proto, method, detail, matched_rule FROM boundary_logs WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetBoundaryLogByID(ctx context.Context, id uuid.UUID) (BoundaryLog, error) {
|
||||
row := q.db.QueryRowContext(ctx, getBoundaryLogByID, id)
|
||||
var i BoundaryLog
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SessionID,
|
||||
&i.SequenceNumber,
|
||||
&i.CapturedAt,
|
||||
&i.CreatedAt,
|
||||
&i.Proto,
|
||||
&i.Method,
|
||||
&i.Detail,
|
||||
&i.MatchedRule,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getBoundarySessionByID = `-- name: GetBoundarySessionByID :one
|
||||
SELECT id, workspace_agent_id, confined_process_name, started_at, updated_at FROM boundary_sessions WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetBoundarySessionByID(ctx context.Context, id uuid.UUID) (BoundarySession, error) {
|
||||
row := q.db.QueryRowContext(ctx, getBoundarySessionByID, id)
|
||||
var i BoundarySession
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.WorkspaceAgentID,
|
||||
&i.ConfinedProcessName,
|
||||
&i.StartedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const insertBoundaryLog = `-- name: InsertBoundaryLog :one
|
||||
INSERT INTO boundary_logs (
|
||||
id,
|
||||
session_id,
|
||||
sequence_number,
|
||||
captured_at,
|
||||
created_at,
|
||||
proto,
|
||||
method,
|
||||
detail,
|
||||
matched_rule
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7,
|
||||
$8,
|
||||
$9
|
||||
) RETURNING id, session_id, sequence_number, captured_at, created_at, proto, method, detail, matched_rule
|
||||
`
|
||||
|
||||
type InsertBoundaryLogParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
SessionID uuid.UUID `db:"session_id" json:"session_id"`
|
||||
SequenceNumber int32 `db:"sequence_number" json:"sequence_number"`
|
||||
CapturedAt time.Time `db:"captured_at" json:"captured_at"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
Proto string `db:"proto" json:"proto"`
|
||||
Method string `db:"method" json:"method"`
|
||||
Detail string `db:"detail" json:"detail"`
|
||||
MatchedRule sql.NullString `db:"matched_rule" json:"matched_rule"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertBoundaryLog(ctx context.Context, arg InsertBoundaryLogParams) (BoundaryLog, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertBoundaryLog,
|
||||
arg.ID,
|
||||
arg.SessionID,
|
||||
arg.SequenceNumber,
|
||||
arg.CapturedAt,
|
||||
arg.CreatedAt,
|
||||
arg.Proto,
|
||||
arg.Method,
|
||||
arg.Detail,
|
||||
arg.MatchedRule,
|
||||
)
|
||||
var i BoundaryLog
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SessionID,
|
||||
&i.SequenceNumber,
|
||||
&i.CapturedAt,
|
||||
&i.CreatedAt,
|
||||
&i.Proto,
|
||||
&i.Method,
|
||||
&i.Detail,
|
||||
&i.MatchedRule,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const insertBoundarySession = `-- name: InsertBoundarySession :one
|
||||
INSERT INTO boundary_sessions (
|
||||
id,
|
||||
workspace_agent_id,
|
||||
confined_process_name,
|
||||
started_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5
|
||||
) RETURNING id, workspace_agent_id, confined_process_name, started_at, updated_at
|
||||
`
|
||||
|
||||
type InsertBoundarySessionParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
|
||||
ConfinedProcessName string `db:"confined_process_name" json:"confined_process_name"`
|
||||
StartedAt time.Time `db:"started_at" json:"started_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertBoundarySession(ctx context.Context, arg InsertBoundarySessionParams) (BoundarySession, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertBoundarySession,
|
||||
arg.ID,
|
||||
arg.WorkspaceAgentID,
|
||||
arg.ConfinedProcessName,
|
||||
arg.StartedAt,
|
||||
arg.UpdatedAt,
|
||||
)
|
||||
var i BoundarySession
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.WorkspaceAgentID,
|
||||
&i.ConfinedProcessName,
|
||||
&i.StartedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listBoundaryLogsBySessionID = `-- name: ListBoundaryLogsBySessionID :many
|
||||
SELECT id, session_id, sequence_number, captured_at, created_at, proto, method, detail, matched_rule
|
||||
FROM boundary_logs
|
||||
WHERE
|
||||
session_id = $1
|
||||
AND CASE
|
||||
WHEN $2::int IS NOT NULL THEN sequence_number > $2
|
||||
ELSE true
|
||||
END
|
||||
AND CASE
|
||||
WHEN $3::int IS NOT NULL THEN sequence_number < $3
|
||||
ELSE true
|
||||
END
|
||||
ORDER BY sequence_number ASC
|
||||
LIMIT COALESCE(NULLIF($4::int, 0), 100)
|
||||
`
|
||||
|
||||
type ListBoundaryLogsBySessionIDParams struct {
|
||||
SessionID uuid.UUID `db:"session_id" json:"session_id"`
|
||||
SeqAfter sql.NullInt32 `db:"seq_after" json:"seq_after"`
|
||||
SeqBefore sql.NullInt32 `db:"seq_before" json:"seq_before"`
|
||||
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
|
||||
}
|
||||
|
||||
// Lists boundary logs for a session, sorted by sequence number ascending.
|
||||
// Supports optional exclusive sequence number bounds (seq_after, seq_before)
|
||||
// for fetching events between two known interceptions.
|
||||
func (q *sqlQuerier) ListBoundaryLogsBySessionID(ctx context.Context, arg ListBoundaryLogsBySessionIDParams) ([]BoundaryLog, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listBoundaryLogsBySessionID,
|
||||
arg.SessionID,
|
||||
arg.SeqAfter,
|
||||
arg.SeqBefore,
|
||||
arg.LimitOpt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []BoundaryLog
|
||||
for rows.Next() {
|
||||
var i BoundaryLog
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SessionID,
|
||||
&i.SequenceNumber,
|
||||
&i.CapturedAt,
|
||||
&i.CreatedAt,
|
||||
&i.Proto,
|
||||
&i.Method,
|
||||
&i.Detail,
|
||||
&i.MatchedRule,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getAndResetBoundaryUsageSummary = `-- name: GetAndResetBoundaryUsageSummary :one
|
||||
WITH deleted AS (
|
||||
DELETE FROM boundary_usage_stats
|
||||
|
||||
Reference in New Issue
Block a user