mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: GET /api/v2/agent-firewall/sessions/{id} (#24814)
Add a GET endpoint at `/api/v2/agent-firewall/sessions/{id}` that
returns agent firewall session metadata (`id`, `workspace_id`,
`owner_id`, `confined_process`, `started_at`). The handler authorizes
against the `boundary_log` resource with `ActionRead` via dbauthz.
The endpoint is enterprise-only, gated behind the `FeatureBoundary`
entitlement.
The `GetBoundarySessionByID` SQL query JOINs through `workspace_agents`
→ `workspace_resources` → `workspace_builds` → `workspaces` to return
`workspace_id` and `workspace_owner_id` directly, avoiding a separate
query.
Also adds an `owner_id` column to the `boundary_logs` table (migration
000526) with a FK to `users(id)` and a backfill from
`boundary_sessions`. This enables user-scoped RBAC authorization for
`InsertBoundaryLogs` via `.WithOwner()`, ensuring workspace agents can
only insert logs for their own owner.
Depends on #24810
**RBAC behaviour:**
| Role | Result |
|---------|--------|
| Owner | read |
| Auditor | read |
| Member | 404 |
> [!NOTE]
> This PR was authored by Coder Agents.
This commit is contained in:
@@ -96,6 +96,7 @@ func (a *BoundaryLogsAPI) ReportBoundaryLogs(ctx context.Context, req *agentprot
|
||||
// Collect batch insert params while iterating.
|
||||
batch := database.InsertBoundaryLogsParams{
|
||||
SessionID: sessionID,
|
||||
OwnerID: a.OwnerID,
|
||||
ID: nil,
|
||||
SequenceNumber: nil,
|
||||
CapturedAt: nil,
|
||||
|
||||
Generated
+59
@@ -1292,6 +1292,41 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/agent-firewall/sessions/{id}": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Get agent firewall session by ID",
|
||||
"operationId": "get-agent-firewall-session-by-id",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Agent firewall session ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.AgentFirewallSession"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ai/providers": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@@ -15908,6 +15943,30 @@ const docTemplate = `{
|
||||
"AgentDisplayModeAlwaysCollapsed"
|
||||
]
|
||||
},
|
||||
"codersdk.AgentFirewallSession": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"confined_process": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"owner_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"workspace_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AgentScriptTiming": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Generated
+55
@@ -1143,6 +1143,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/agent-firewall/sessions/{id}": {
|
||||
"get": {
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Get agent firewall session by ID",
|
||||
"operationId": "get-agent-firewall-session-by-id",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Agent firewall session ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.AgentFirewallSession"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ai/providers": {
|
||||
"get": {
|
||||
"produces": ["application/json"],
|
||||
@@ -14247,6 +14278,30 @@
|
||||
"AgentDisplayModeAlwaysCollapsed"
|
||||
]
|
||||
},
|
||||
"codersdk.AgentFirewallSession": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"confined_process": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"owner_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"workspace_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AgentScriptTiming": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -2963,9 +2963,9 @@ func (q *querier) GetBoundaryLogByID(ctx context.Context, id uuid.UUID) (databas
|
||||
return q.db.GetBoundaryLogByID(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) GetBoundarySessionByID(ctx context.Context, id uuid.UUID) (database.BoundarySession, error) {
|
||||
func (q *querier) GetBoundarySessionByID(ctx context.Context, id uuid.UUID) (database.GetBoundarySessionByIDRow, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceBoundaryLog); err != nil {
|
||||
return database.BoundarySession{}, err
|
||||
return database.GetBoundarySessionByIDRow{}, err
|
||||
}
|
||||
return q.db.GetBoundarySessionByID(ctx, id)
|
||||
}
|
||||
@@ -5810,7 +5810,8 @@ func (q *querier) InsertAuditLog(ctx context.Context, arg database.InsertAuditLo
|
||||
}
|
||||
|
||||
func (q *querier) InsertBoundaryLogs(ctx context.Context, arg database.InsertBoundaryLogsParams) ([]database.BoundaryLog, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceBoundaryLog); err != nil {
|
||||
if err := q.authorizeContext(ctx, policy.ActionCreate,
|
||||
rbac.ResourceBoundaryLog.WithOwner(arg.OwnerID.String())); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.InsertBoundaryLogs(ctx, arg)
|
||||
|
||||
@@ -457,16 +457,20 @@ func (s *MethodTestSuite) TestBoundaryLogs() {
|
||||
)
|
||||
}))
|
||||
s.Run("GetBoundarySessionByID", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
dbm.EXPECT().GetBoundarySessionByID(gomock.Any(), uuid.Nil).Return(database.BoundarySession{}, nil).AnyTimes()
|
||||
dbm.EXPECT().GetBoundarySessionByID(gomock.Any(), uuid.Nil).Return(database.GetBoundarySessionByIDRow{}, nil).AnyTimes()
|
||||
check.Args(uuid.Nil).Asserts(rbac.ResourceBoundaryLog, policy.ActionRead)
|
||||
}))
|
||||
s.Run("InsertBoundaryLogs", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
ownerID := uuid.New()
|
||||
arg := database.InsertBoundaryLogsParams{
|
||||
SessionID: uuid.New(),
|
||||
OwnerID: ownerID,
|
||||
ID: []uuid.UUID{uuid.New(), uuid.New()},
|
||||
}
|
||||
dbm.EXPECT().InsertBoundaryLogs(gomock.Any(), arg).Return([]database.BoundaryLog{}, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceBoundaryLog, policy.ActionCreate)
|
||||
check.Args(arg).Asserts(
|
||||
rbac.ResourceBoundaryLog.WithOwner(ownerID.String()), policy.ActionCreate,
|
||||
)
|
||||
}))
|
||||
s.Run("GetBoundaryLogByID", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
dbm.EXPECT().GetBoundaryLogByID(gomock.Any(), uuid.Nil).Return(database.BoundaryLog{}, nil).AnyTimes()
|
||||
|
||||
@@ -481,6 +481,7 @@ func BoundarySession(t testing.TB, db database.Store, seed database.BoundarySess
|
||||
func BoundaryLogs(t testing.TB, db database.Store, seed []database.BoundaryLog) []database.BoundaryLog {
|
||||
ids := make([]uuid.UUID, 0, len(seed))
|
||||
sessionID := seed[0].SessionID
|
||||
ownerID := seed[0].OwnerID.UUID
|
||||
sequenceNumbers := make([]int32, 0, len(seed))
|
||||
capturedAt := make([]time.Time, 0, len(seed))
|
||||
createdAt := make([]time.Time, 0, len(seed))
|
||||
@@ -502,6 +503,7 @@ func BoundaryLogs(t testing.TB, db database.Store, seed []database.BoundaryLog)
|
||||
logs, err := db.InsertBoundaryLogs(genCtx, database.InsertBoundaryLogsParams{
|
||||
ID: ids,
|
||||
SessionID: sessionID,
|
||||
OwnerID: ownerID,
|
||||
SequenceNumber: sequenceNumbers,
|
||||
CapturedAt: capturedAt,
|
||||
CreatedAt: createdAt,
|
||||
|
||||
+1
-1
@@ -1378,7 +1378,7 @@ func (m queryMetricsStore) GetBoundaryLogByID(ctx context.Context, id uuid.UUID)
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetBoundarySessionByID(ctx context.Context, id uuid.UUID) (database.BoundarySession, error) {
|
||||
func (m queryMetricsStore) GetBoundarySessionByID(ctx context.Context, id uuid.UUID) (database.GetBoundarySessionByIDRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetBoundarySessionByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("GetBoundarySessionByID").Observe(time.Since(start).Seconds())
|
||||
|
||||
Generated
+2
-2
@@ -2533,10 +2533,10 @@ func (mr *MockStoreMockRecorder) GetBoundaryLogByID(ctx, id any) *gomock.Call {
|
||||
}
|
||||
|
||||
// GetBoundarySessionByID mocks base method.
|
||||
func (m *MockStore) GetBoundarySessionByID(ctx context.Context, id uuid.UUID) (database.BoundarySession, error) {
|
||||
func (m *MockStore) GetBoundarySessionByID(ctx context.Context, id uuid.UUID) (database.GetBoundarySessionByIDRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetBoundarySessionByID", ctx, id)
|
||||
ret0, _ := ret[0].(database.BoundarySession)
|
||||
ret0, _ := ret[0].(database.GetBoundarySessionByIDRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
@@ -1753,6 +1753,7 @@ func TestDeleteOldBoundaryLogs(t *testing.T) {
|
||||
// Create old boundary log.
|
||||
oldLogs := dbgen.BoundaryLogs(t, db, []database.BoundaryLog{{
|
||||
SessionID: session.ID,
|
||||
OwnerID: uuid.NullUUID{UUID: user.ID, Valid: true},
|
||||
SequenceNumber: 0,
|
||||
CapturedAt: tc.oldLogTime,
|
||||
CreatedAt: tc.oldLogTime,
|
||||
@@ -1764,6 +1765,7 @@ func TestDeleteOldBoundaryLogs(t *testing.T) {
|
||||
if tc.recentLogTime != nil {
|
||||
recentLogs := dbgen.BoundaryLogs(t, db, []database.BoundaryLog{{
|
||||
SessionID: session.ID,
|
||||
OwnerID: uuid.NullUUID{UUID: user.ID, Valid: true},
|
||||
SequenceNumber: 1,
|
||||
CapturedAt: *tc.recentLogTime,
|
||||
CreatedAt: *tc.recentLogTime,
|
||||
@@ -1905,6 +1907,7 @@ func TestDeleteOldBoundarySessions(t *testing.T) {
|
||||
if tc.logTime != nil {
|
||||
dbgen.BoundaryLogs(t, db, []database.BoundaryLog{{
|
||||
SessionID: session.ID,
|
||||
OwnerID: uuid.NullUUID{UUID: user.ID, Valid: true},
|
||||
SequenceNumber: 0,
|
||||
CapturedAt: *tc.logTime,
|
||||
CreatedAt: *tc.logTime,
|
||||
|
||||
Generated
+6
@@ -1678,6 +1678,7 @@ CREATE TABLE boundary_logs (
|
||||
method text DEFAULT ''::text NOT NULL,
|
||||
detail text DEFAULT ''::text NOT NULL,
|
||||
matched_rule text,
|
||||
owner_id uuid,
|
||||
CONSTRAINT boundary_logs_sequence_number_check CHECK ((sequence_number >= 0))
|
||||
);
|
||||
|
||||
@@ -1699,6 +1700,8 @@ COMMENT ON COLUMN boundary_logs.detail IS 'Protocol-specific detail. e.g. the fu
|
||||
|
||||
COMMENT ON COLUMN boundary_logs.matched_rule IS 'The allow-list rule that matched. NULL when the request was denied; non-NULL implies the request was allowed.';
|
||||
|
||||
COMMENT ON COLUMN boundary_logs.owner_id IS 'The ID of the user who owns the workspace. NULL for logs inserted before this column existed or if the user was deleted.';
|
||||
|
||||
CREATE TABLE boundary_sessions (
|
||||
id uuid NOT NULL,
|
||||
workspace_agent_id uuid NOT NULL,
|
||||
@@ -4935,6 +4938,9 @@ ALTER TABLE ONLY aibridge_interceptions
|
||||
ALTER TABLE ONLY api_keys
|
||||
ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY boundary_logs
|
||||
ADD CONSTRAINT boundary_logs_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY boundary_sessions
|
||||
ADD CONSTRAINT boundary_sessions_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE SET NULL;
|
||||
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ const (
|
||||
ForeignKeyAISeatStateUserID ForeignKeyConstraint = "ai_seat_state_user_id_fkey" // ALTER TABLE ONLY ai_seat_state ADD CONSTRAINT ai_seat_state_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ForeignKeyAibridgeInterceptionsInitiatorID ForeignKeyConstraint = "aibridge_interceptions_initiator_id_fkey" // ALTER TABLE ONLY aibridge_interceptions ADD CONSTRAINT aibridge_interceptions_initiator_id_fkey FOREIGN KEY (initiator_id) REFERENCES users(id);
|
||||
ForeignKeyAPIKeysUserIDUUID ForeignKeyConstraint = "api_keys_user_id_uuid_fkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ForeignKeyBoundaryLogsOwnerID ForeignKeyConstraint = "boundary_logs_owner_id_fkey" // ALTER TABLE ONLY boundary_logs ADD CONSTRAINT boundary_logs_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE SET NULL;
|
||||
ForeignKeyBoundarySessionsOwnerID ForeignKeyConstraint = "boundary_sessions_owner_id_fkey" // ALTER TABLE ONLY boundary_sessions ADD CONSTRAINT boundary_sessions_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE SET NULL;
|
||||
ForeignKeyBoundarySessionsWorkspaceAgentID ForeignKeyConstraint = "boundary_sessions_workspace_agent_id_fkey" // ALTER TABLE ONLY boundary_sessions ADD CONSTRAINT boundary_sessions_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id);
|
||||
ForeignKeyChatContextResourcesChatID ForeignKeyConstraint = "chat_context_resources_chat_id_fkey" // ALTER TABLE ONLY chat_context_resources ADD CONSTRAINT chat_context_resources_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE boundary_logs DROP CONSTRAINT IF EXISTS boundary_logs_owner_id_fkey;
|
||||
ALTER TABLE boundary_logs DROP COLUMN IF EXISTS owner_id;
|
||||
@@ -0,0 +1,14 @@
|
||||
ALTER TABLE boundary_logs ADD COLUMN owner_id UUID;
|
||||
|
||||
COMMENT ON COLUMN boundary_logs.owner_id IS 'The ID of the user who owns the workspace. NULL for logs inserted before this column existed or if the user was deleted.';
|
||||
|
||||
-- Backfill from sessions where possible.
|
||||
UPDATE boundary_logs bl
|
||||
SET owner_id = bs.owner_id
|
||||
FROM boundary_sessions bs
|
||||
WHERE bl.session_id = bs.id
|
||||
AND bs.owner_id IS NOT NULL;
|
||||
|
||||
ALTER TABLE boundary_logs
|
||||
ADD CONSTRAINT boundary_logs_owner_id_fkey
|
||||
FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE SET NULL;
|
||||
Generated
+2
@@ -4726,6 +4726,8 @@ type BoundaryLog struct {
|
||||
Detail string `db:"detail" json:"detail"`
|
||||
// The allow-list rule that matched. NULL when the request was denied; non-NULL implies the request was allowed.
|
||||
MatchedRule sql.NullString `db:"matched_rule" json:"matched_rule"`
|
||||
// The ID of the user who owns the workspace. NULL for logs inserted before this column existed or if the user was deleted.
|
||||
OwnerID uuid.NullUUID `db:"owner_id" json:"owner_id"`
|
||||
}
|
||||
|
||||
// Boundary session metadata. Each row represents a single invocation of a Boundary process wrapping a confined agent.
|
||||
|
||||
Generated
+1
-1
@@ -367,7 +367,7 @@ type sqlcQuerier interface {
|
||||
// limits roots, not total family members.
|
||||
GetAutoArchiveInactiveChatCandidates(ctx context.Context, arg GetAutoArchiveInactiveChatCandidatesParams) ([]GetAutoArchiveInactiveChatCandidatesRow, error)
|
||||
GetBoundaryLogByID(ctx context.Context, id uuid.UUID) (BoundaryLog, error)
|
||||
GetBoundarySessionByID(ctx context.Context, id uuid.UUID) (BoundarySession, error)
|
||||
GetBoundarySessionByID(ctx context.Context, id uuid.UUID) (GetBoundarySessionByIDRow, error)
|
||||
GetChatACLByID(ctx context.Context, id uuid.UUID) (GetChatACLByIDRow, error)
|
||||
// GetChatAdvisorConfig returns the deployment-wide runtime configuration
|
||||
// for the experimental chat advisor as a JSON blob. Callers unmarshal the
|
||||
|
||||
Generated
+45
-10
@@ -3686,7 +3686,7 @@ func (q *sqlQuerier) DeleteOldBoundarySessions(ctx context.Context, arg DeleteOl
|
||||
}
|
||||
|
||||
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
|
||||
SELECT id, session_id, sequence_number, captured_at, created_at, proto, method, detail, matched_rule, owner_id FROM boundary_logs WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetBoundaryLogByID(ctx context.Context, id uuid.UUID) (BoundaryLog, error) {
|
||||
@@ -3702,17 +3702,44 @@ func (q *sqlQuerier) GetBoundaryLogByID(ctx context.Context, id uuid.UUID) (Boun
|
||||
&i.Method,
|
||||
&i.Detail,
|
||||
&i.MatchedRule,
|
||||
&i.OwnerID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getBoundarySessionByID = `-- name: GetBoundarySessionByID :one
|
||||
SELECT id, workspace_agent_id, confined_process_name, started_at, updated_at, owner_id FROM boundary_sessions WHERE id = $1
|
||||
SELECT
|
||||
bs.id, bs.workspace_agent_id, bs.confined_process_name, bs.started_at, bs.updated_at, bs.owner_id,
|
||||
w.id AS workspace_id,
|
||||
w.owner_id AS workspace_owner_id
|
||||
FROM
|
||||
boundary_sessions bs
|
||||
JOIN
|
||||
workspace_agents wa ON wa.id = bs.workspace_agent_id
|
||||
JOIN
|
||||
workspace_resources wr ON wr.id = wa.resource_id
|
||||
JOIN
|
||||
workspace_builds wb ON wb.job_id = wr.job_id
|
||||
JOIN
|
||||
workspaces w ON w.id = wb.workspace_id
|
||||
WHERE
|
||||
bs.id = $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetBoundarySessionByID(ctx context.Context, id uuid.UUID) (BoundarySession, error) {
|
||||
type GetBoundarySessionByIDRow 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"`
|
||||
OwnerID uuid.NullUUID `db:"owner_id" json:"owner_id"`
|
||||
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
|
||||
WorkspaceOwnerID uuid.UUID `db:"workspace_owner_id" json:"workspace_owner_id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetBoundarySessionByID(ctx context.Context, id uuid.UUID) (GetBoundarySessionByIDRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, getBoundarySessionByID, id)
|
||||
var i BoundarySession
|
||||
var i GetBoundarySessionByIDRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.WorkspaceAgentID,
|
||||
@@ -3720,6 +3747,8 @@ func (q *sqlQuerier) GetBoundarySessionByID(ctx context.Context, id uuid.UUID) (
|
||||
&i.StartedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OwnerID,
|
||||
&i.WorkspaceID,
|
||||
&i.WorkspaceOwnerID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -3728,6 +3757,7 @@ const insertBoundaryLogs = `-- name: InsertBoundaryLogs :many
|
||||
INSERT INTO boundary_logs (
|
||||
id,
|
||||
session_id,
|
||||
owner_id,
|
||||
sequence_number,
|
||||
captured_at,
|
||||
created_at,
|
||||
@@ -3739,19 +3769,21 @@ INSERT INTO boundary_logs (
|
||||
SELECT
|
||||
unnest($1 :: uuid[]),
|
||||
$2 :: uuid,
|
||||
unnest($3 :: int[]),
|
||||
unnest($4 :: timestamptz[]),
|
||||
$3 :: uuid,
|
||||
unnest($4 :: int[]),
|
||||
unnest($5 :: timestamptz[]),
|
||||
unnest($6 :: text[]),
|
||||
unnest($6 :: timestamptz[]),
|
||||
unnest($7 :: text[]),
|
||||
unnest($8 :: text[]),
|
||||
NULLIF(unnest($9 :: text[]), '')
|
||||
RETURNING id, session_id, sequence_number, captured_at, created_at, proto, method, detail, matched_rule
|
||||
unnest($9 :: text[]),
|
||||
NULLIF(unnest($10 :: text[]), '')
|
||||
RETURNING id, session_id, sequence_number, captured_at, created_at, proto, method, detail, matched_rule, owner_id
|
||||
`
|
||||
|
||||
type InsertBoundaryLogsParams struct {
|
||||
ID []uuid.UUID `db:"id" json:"id"`
|
||||
SessionID uuid.UUID `db:"session_id" json:"session_id"`
|
||||
OwnerID uuid.UUID `db:"owner_id" json:"owner_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"`
|
||||
@@ -3765,6 +3797,7 @@ func (q *sqlQuerier) InsertBoundaryLogs(ctx context.Context, arg InsertBoundaryL
|
||||
rows, err := q.db.QueryContext(ctx, insertBoundaryLogs,
|
||||
pq.Array(arg.ID),
|
||||
arg.SessionID,
|
||||
arg.OwnerID,
|
||||
pq.Array(arg.SequenceNumber),
|
||||
pq.Array(arg.CapturedAt),
|
||||
pq.Array(arg.CreatedAt),
|
||||
@@ -3790,6 +3823,7 @@ func (q *sqlQuerier) InsertBoundaryLogs(ctx context.Context, arg InsertBoundaryL
|
||||
&i.Method,
|
||||
&i.Detail,
|
||||
&i.MatchedRule,
|
||||
&i.OwnerID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -3853,7 +3887,7 @@ func (q *sqlQuerier) InsertBoundarySession(ctx context.Context, arg InsertBounda
|
||||
}
|
||||
|
||||
const listBoundaryLogsBySessionID = `-- name: ListBoundaryLogsBySessionID :many
|
||||
SELECT id, session_id, sequence_number, captured_at, created_at, proto, method, detail, matched_rule
|
||||
SELECT id, session_id, sequence_number, captured_at, created_at, proto, method, detail, matched_rule, owner_id
|
||||
FROM boundary_logs
|
||||
WHERE
|
||||
session_id = $1
|
||||
@@ -3903,6 +3937,7 @@ func (q *sqlQuerier) ListBoundaryLogsBySessionID(ctx context.Context, arg ListBo
|
||||
&i.Method,
|
||||
&i.Detail,
|
||||
&i.MatchedRule,
|
||||
&i.OwnerID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -16,12 +16,28 @@ INSERT INTO boundary_sessions (
|
||||
) RETURNING *;
|
||||
|
||||
-- name: GetBoundarySessionByID :one
|
||||
SELECT * FROM boundary_sessions WHERE id = @id;
|
||||
SELECT
|
||||
bs.*,
|
||||
w.id AS workspace_id,
|
||||
w.owner_id AS workspace_owner_id
|
||||
FROM
|
||||
boundary_sessions bs
|
||||
JOIN
|
||||
workspace_agents wa ON wa.id = bs.workspace_agent_id
|
||||
JOIN
|
||||
workspace_resources wr ON wr.id = wa.resource_id
|
||||
JOIN
|
||||
workspace_builds wb ON wb.job_id = wr.job_id
|
||||
JOIN
|
||||
workspaces w ON w.id = wb.workspace_id
|
||||
WHERE
|
||||
bs.id = @id;
|
||||
|
||||
-- name: InsertBoundaryLogs :many
|
||||
INSERT INTO boundary_logs (
|
||||
id,
|
||||
session_id,
|
||||
owner_id,
|
||||
sequence_number,
|
||||
captured_at,
|
||||
created_at,
|
||||
@@ -33,6 +49,7 @@ INSERT INTO boundary_logs (
|
||||
SELECT
|
||||
unnest(@id :: uuid[]),
|
||||
@session_id :: uuid,
|
||||
@owner_id :: uuid,
|
||||
unnest(@sequence_number :: int[]),
|
||||
unnest(@captured_at :: timestamptz[]),
|
||||
unnest(@created_at :: timestamptz[]),
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package codersdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// AgentFirewallSession represents a firewall session for a workspace agent.
|
||||
type AgentFirewallSession struct {
|
||||
ID uuid.UUID `json:"id" format:"uuid"`
|
||||
WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"`
|
||||
OwnerID uuid.UUID `json:"owner_id" format:"uuid"`
|
||||
ConfinedProcess string `json:"confined_process"`
|
||||
StartedAt time.Time `json:"started_at" format:"date-time"`
|
||||
}
|
||||
|
||||
// AgentFirewallSessionByID returns an agent firewall session by its ID.
|
||||
func (c *Client) AgentFirewallSessionByID(ctx context.Context, id uuid.UUID) (AgentFirewallSession, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/agent-firewall/sessions/%s", id), nil)
|
||||
if err != nil {
|
||||
return AgentFirewallSession{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return AgentFirewallSession{}, ReadBodyAsError(res)
|
||||
}
|
||||
var session AgentFirewallSession
|
||||
return session, json.NewDecoder(res.Body).Decode(&session)
|
||||
}
|
||||
Generated
+41
@@ -84,6 +84,47 @@ curl -X GET http://coder-server:8080/.well-known/oauth-protected-resource \
|
||||
|--------|---------------------------------------------------------|-------------|------------------------------------------------------------------------------------------------|
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.OAuth2ProtectedResourceMetadata](schemas.md#codersdkoauth2protectedresourcemetadata) |
|
||||
|
||||
## Get agent firewall session by ID
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X GET http://coder-server:8080/api/v2/agent-firewall/sessions/{id} \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`GET /api/v2/agent-firewall/sessions/{id}`
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|------|--------------|----------|---------------------------|
|
||||
| `id` | path | string(uuid) | true | Agent firewall session ID |
|
||||
|
||||
### Example responses
|
||||
|
||||
> 200 Response
|
||||
|
||||
```json
|
||||
{
|
||||
"confined_process": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
|
||||
"started_at": "2019-08-24T14:15:22Z",
|
||||
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9"
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
|--------|---------------------------------------------------------|-------------|--------------------------------------------------------------------------|
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.AgentFirewallSession](schemas.md#codersdkagentfirewallsession) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## List AI Gateway keys
|
||||
|
||||
### Code samples
|
||||
|
||||
Generated
+22
@@ -1263,6 +1263,28 @@ None
|
||||
|-----------------------------------------------|
|
||||
| `always_collapsed`, `always_expanded`, `auto` |
|
||||
|
||||
## codersdk.AgentFirewallSession
|
||||
|
||||
```json
|
||||
{
|
||||
"confined_process": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
|
||||
"started_at": "2019-08-24T14:15:22Z",
|
||||
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|--------------------|--------|----------|--------------|-------------|
|
||||
| `confined_process` | string | false | | |
|
||||
| `id` | string | false | | |
|
||||
| `owner_id` | string | false | | |
|
||||
| `started_at` | string | false | | |
|
||||
| `workspace_id` | string | false | | |
|
||||
|
||||
## codersdk.AgentScriptTiming
|
||||
|
||||
```json
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package coderd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
// @Summary Get agent firewall session by ID
|
||||
// @ID get-agent-firewall-session-by-id
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Enterprise
|
||||
// @Param id path string true "Agent firewall session ID" format(uuid)
|
||||
// @Success 200 {object} codersdk.AgentFirewallSession
|
||||
// @Router /api/v2/agent-firewall/sessions/{id} [get]
|
||||
func (api *API) agentFirewallSessionByID(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
id, ok := httpmw.ParseUUIDParam(rw, r, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
session, err := api.Database.GetBoundarySessionByID(ctx, id)
|
||||
if httpapi.Is404Error(err) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.AgentFirewallSession{
|
||||
ID: session.ID,
|
||||
WorkspaceID: session.WorkspaceID,
|
||||
OwnerID: session.WorkspaceOwnerID,
|
||||
ConfinedProcess: session.ConfinedProcessName,
|
||||
StartedAt: session.StartedAt,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
package coderd_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"cdr.dev/slog/v3/sloggers/slogtest"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbfake"
|
||||
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/license"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestAgentFirewallSessionByID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// seedBoundarySession inserts a boundary session linked to a workspace agent.
|
||||
// Uses the raw DB store to avoid dbauthz permission and ordering constraints during setup.
|
||||
seedBoundarySession := func(t *testing.T, rawDB database.Store, ownerID, orgID uuid.UUID) (database.BoundarySession, database.WorkspaceTable) {
|
||||
t.Helper()
|
||||
|
||||
resp := dbfake.WorkspaceBuild(t, rawDB, database.WorkspaceTable{
|
||||
OwnerID: ownerID,
|
||||
OrganizationID: orgID,
|
||||
}).WithAgent().Do()
|
||||
|
||||
require.NotEmpty(t, resp.Agents, "expected at least one agent")
|
||||
|
||||
session := dbgen.BoundarySession(t, rawDB, database.BoundarySession{
|
||||
WorkspaceAgentID: resp.Agents[0].ID,
|
||||
OwnerID: uuid.NullUUID{UUID: ownerID, Valid: true},
|
||||
ConfinedProcessName: "claude-code",
|
||||
})
|
||||
return session, resp.Workspace
|
||||
}
|
||||
|
||||
t.Run("Owner", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, pubsub := dbtestutil.NewDB(t)
|
||||
ownerClient, _, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
Database: db,
|
||||
Pubsub: pubsub,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureBoundary: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
session, ws := seedBoundarySession(t, db, owner.UserID, owner.OrganizationID)
|
||||
|
||||
//nolint:gocritic // Testing owner role.
|
||||
got, err := ownerClient.AgentFirewallSessionByID(ctx, session.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, session.ID, got.ID)
|
||||
require.Equal(t, ws.OwnerID, got.OwnerID)
|
||||
require.Equal(t, session.ConfinedProcessName, got.ConfinedProcess)
|
||||
require.Equal(t, ws.ID, got.WorkspaceID)
|
||||
})
|
||||
|
||||
t.Run("Auditor", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, pubsub := dbtestutil.NewDB(t)
|
||||
ownerClient, _, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
Database: db,
|
||||
Pubsub: pubsub,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureBoundary: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
session, _ := seedBoundarySession(t, db, owner.UserID, owner.OrganizationID)
|
||||
|
||||
auditorClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleAuditor())
|
||||
|
||||
got, err := auditorClient.AgentFirewallSessionByID(ctx, session.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, session.ID, got.ID)
|
||||
require.Equal(t, "claude-code", got.ConfinedProcess)
|
||||
})
|
||||
|
||||
t.Run("MemberDenied", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, pubsub := dbtestutil.NewDB(t)
|
||||
ownerClient, _, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
Database: db,
|
||||
Pubsub: pubsub,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureBoundary: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
session, _ := seedBoundarySession(t, db, owner.UserID, owner.OrganizationID)
|
||||
|
||||
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||||
|
||||
_, err := memberClient.AgentFirewallSessionByID(ctx, session.ID)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ownerClient, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureBoundary: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
//nolint:gocritic // Testing owner role.
|
||||
_, err := ownerClient.AgentFirewallSessionByID(ctx, uuid.New())
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
|
||||
})
|
||||
}
|
||||
|
||||
// TestInsertBoundaryLogs_AgentAuth verifies that a workspace agent context
|
||||
// can insert boundary logs through the dbauthz layer. Create is user-scoped
|
||||
// in the member role; the agent's owner ID must match the resource owner.
|
||||
func TestInsertBoundaryLogs_AgentAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rawDB, _ := dbtestutil.NewDB(t)
|
||||
authorizer := rbac.NewStrictAuthorizer(prometheus.NewRegistry())
|
||||
authzDB := dbauthz.New(rawDB, authorizer, slogtest.Make(t, nil), &atomic.Pointer[dbauthz.AccessControlStore]{})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// Seed a workspace with an agent.
|
||||
user := dbgen.User(t, rawDB, database.User{})
|
||||
org := dbgen.Organization(t, rawDB, database.Organization{})
|
||||
_ = dbgen.OrganizationMember(t, rawDB, database.OrganizationMember{
|
||||
UserID: user.ID,
|
||||
OrganizationID: org.ID,
|
||||
})
|
||||
tmpl := dbgen.Template(t, rawDB, database.Template{
|
||||
OrganizationID: org.ID,
|
||||
CreatedBy: user.ID,
|
||||
})
|
||||
tmplVer := dbgen.TemplateVersion(t, rawDB, database.TemplateVersion{
|
||||
TemplateID: uuid.NullUUID{Valid: true, UUID: tmpl.ID},
|
||||
OrganizationID: org.ID,
|
||||
CreatedBy: user.ID,
|
||||
})
|
||||
ws := dbgen.Workspace(t, rawDB, database.WorkspaceTable{
|
||||
OrganizationID: org.ID,
|
||||
TemplateID: tmpl.ID,
|
||||
OwnerID: user.ID,
|
||||
})
|
||||
job := dbgen.ProvisionerJob(t, rawDB, nil, database.ProvisionerJob{
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
})
|
||||
build := dbgen.WorkspaceBuild(t, rawDB, database.WorkspaceBuild{
|
||||
JobID: job.ID,
|
||||
WorkspaceID: ws.ID,
|
||||
TemplateVersionID: tmplVer.ID,
|
||||
})
|
||||
resource := dbgen.WorkspaceResource(t, rawDB, database.WorkspaceResource{
|
||||
JobID: build.JobID,
|
||||
})
|
||||
agent := dbgen.WorkspaceAgent(t, rawDB, database.WorkspaceAgent{
|
||||
ResourceID: resource.ID,
|
||||
})
|
||||
|
||||
// Insert a boundary session using the raw DB (no auth check).
|
||||
now := time.Now().UTC()
|
||||
sessionID := uuid.New()
|
||||
_, err := rawDB.InsertBoundarySession(ctx, database.InsertBoundarySessionParams{
|
||||
ID: sessionID,
|
||||
WorkspaceAgentID: agent.ID,
|
||||
OwnerID: uuid.NullUUID{UUID: user.ID, Valid: true},
|
||||
ConfinedProcessName: "claude-code",
|
||||
StartedAt: now,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Build a workspace agent RBAC subject.
|
||||
memberRole, err := rbac.RoleByName(rbac.RoleMember())
|
||||
require.NoError(t, err)
|
||||
agentSubject := rbac.Subject{
|
||||
ID: user.ID.String(),
|
||||
Roles: rbac.Roles{memberRole},
|
||||
Scope: rbac.WorkspaceAgentScope(rbac.WorkspaceAgentScopeParams{
|
||||
WorkspaceID: ws.ID,
|
||||
OwnerID: user.ID,
|
||||
TemplateID: tmpl.ID,
|
||||
VersionID: tmplVer.ID,
|
||||
}),
|
||||
}.WithCachedASTValue()
|
||||
agentCtx := dbauthz.As(ctx, agentSubject)
|
||||
|
||||
// Insert boundary logs through dbauthz with the correct owner.
|
||||
// User-scoped create succeeds because the agent subject ID matches.
|
||||
logID := uuid.New()
|
||||
_, err = authzDB.InsertBoundaryLogs(agentCtx, database.InsertBoundaryLogsParams{
|
||||
SessionID: sessionID,
|
||||
OwnerID: user.ID,
|
||||
ID: []uuid.UUID{logID},
|
||||
SequenceNumber: []int32{1},
|
||||
CapturedAt: []time.Time{now},
|
||||
CreatedAt: []time.Time{now},
|
||||
Proto: []string{"tcp"},
|
||||
Method: []string{"connect"},
|
||||
Detail: []string{"example.com:443"},
|
||||
MatchedRule: []string{"allow-all"},
|
||||
})
|
||||
require.NoError(t, err, "agent should be able to insert boundary logs for own owner")
|
||||
|
||||
// Verify the logs were actually persisted.
|
||||
got, err := rawDB.GetBoundaryLogByID(ctx, logID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, sessionID, got.SessionID)
|
||||
|
||||
// Inserting with a different owner ID must fail (user-scoped create).
|
||||
otherUser := dbgen.User(t, rawDB, database.User{})
|
||||
_, err = authzDB.InsertBoundaryLogs(agentCtx, database.InsertBoundaryLogsParams{
|
||||
SessionID: sessionID,
|
||||
OwnerID: otherUser.ID,
|
||||
ID: []uuid.UUID{uuid.New()},
|
||||
SequenceNumber: []int32{2},
|
||||
CapturedAt: []time.Time{now},
|
||||
CreatedAt: []time.Time{now},
|
||||
Proto: []string{"tcp"},
|
||||
Method: []string{"connect"},
|
||||
Detail: []string{"evil.com:443"},
|
||||
MatchedRule: []string{"allow-all"},
|
||||
})
|
||||
require.Error(t, err, "agent must not insert boundary logs for a different owner")
|
||||
}
|
||||
@@ -329,6 +329,15 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
)
|
||||
r.Get("/", api.connectionLogs)
|
||||
})
|
||||
r.Route("/agent-firewall", func(r chi.Router) {
|
||||
r.Use(
|
||||
apiKeyMiddleware,
|
||||
api.RequireFeatureMW(codersdk.FeatureBoundary),
|
||||
)
|
||||
r.Route("/sessions/{id}", func(r chi.Router) {
|
||||
r.Get("/", api.agentFirewallSessionByID)
|
||||
})
|
||||
})
|
||||
r.Route("/licenses", func(r chi.Router) {
|
||||
r.Use(apiKeyMiddleware)
|
||||
r.Post("/refresh-entitlements", api.postRefreshEntitlements)
|
||||
|
||||
Generated
+12
@@ -1024,6 +1024,18 @@ export const AgentDisplayModes: AgentDisplayMode[] = [
|
||||
"auto",
|
||||
];
|
||||
|
||||
// From codersdk/agentfirewall.go
|
||||
/**
|
||||
* AgentFirewallSession represents a firewall session for a workspace agent.
|
||||
*/
|
||||
export interface AgentFirewallSession {
|
||||
readonly id: string;
|
||||
readonly workspace_id: string;
|
||||
readonly owner_id: string;
|
||||
readonly confined_process: string;
|
||||
readonly started_at: string;
|
||||
}
|
||||
|
||||
// From codersdk/workspacebuilds.go
|
||||
export interface AgentScriptTiming {
|
||||
readonly started_at: string;
|
||||
|
||||
Reference in New Issue
Block a user