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:
Sas Swart
2026-05-25 11:14:36 +02:00
committed by GitHub
parent eddd4a8c2f
commit 3bf5f80277
16 changed files with 753 additions and 0 deletions
+237
View File
@@ -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