Files
coder/coderd/boundary_logs_test.go
T
Sas Swart fc188fdaee fix: create agent firewall sessions without requiring agent read access (#26990)
## Overview

Part of the **boundary correlation** feature. Fixes lazy creation of
`boundary_sessions` rows so it works within the agent's RBAC
constraints, and consumes the new `ConfinedProcessName` field reported
by boundary.

Pairs with coder/boundary#206, which adds `ConfinedProcessName` to
`ReportBoundaryLogsRequest`. This branch bumps the
`github.com/coder/boundary` module to pick up that work.

## Problem

`ensureSession` did a pre-insert existence check via
`GetBoundarySessionByID`. Agents are **not permitted to read boundary
sessions**, so that read path is not viable when the session is created
from an agent-reported log batch.

## Changes

- **Remove the pre-insert read.** `ensureSession` now inserts directly
and treats a primary-key unique violation as success, covering sessions
already created by a prior batch, a reconnection, or another coderd
replica — without requiring read access.
- **Per-connection guard.** Add a mutex-protected `ensuredSessions` set
so repeated log batches on the same connection skip the existence check
and insert entirely, touching the database only for the logs. On a
transient insert failure the session is left unmarked so the next batch
retries.
- **Consume `ConfinedProcessName`.** Pass `req.GetConfinedProcessName()`
through to the session insert.
- **Bump boundary module** from `v0.9.0` to
`v0.9.1-0.20260706095856-35ba90f9e8b2`.
- **Tests.**
- Add `TestReportBoundaryLogsAgentRBAC`
(`coderd/boundary_logs_test.go`), an integration test that connects as a
real workspace agent, verifies the session and log are persisted under
agent RBAC, and asserts the agent subject cannot read boundary sessions
— guarding against reintroducing a pre-insert read.
- Add `TestReportBoundaryLogsSessionGuard` (session inserted once across
two batches, logs inserted per batch) and
`TestReportBoundaryLogsSessionRetriedOnError` (insert retried after a
transient error).
- Regenerate `agent-firewall` CLI docs/golden files and adjust the
clidocgen template to render the YAML path when a flag has no long name.

> 🤖 This PR was opened by Coder Agents on behalf of @SasSwart.
2026-07-07 10:42:01 +00:00

108 lines
3.7 KiB
Go

package coderd_test
import (
"context"
"sync/atomic"
"testing"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
agentproto "github.com/coder/coder/v2/agent/proto"
"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/dbtestutil"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/coder/v2/testutil"
)
// TestReportBoundaryLogsAgentRBAC guards against regressions where
// a pre-insert read (e.g. GetBoundarySessionByID) would be silently denied for
// agents and prevent session creation.
func TestReportBoundaryLogsAgentRBAC(t *testing.T) {
t.Parallel()
store, ps := dbtestutil.NewDB(t)
client := coderdtest.New(t, &coderdtest.Options{Database: store, Pubsub: ps})
user := coderdtest.CreateFirstUser(t, client)
r := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{
OrganizationID: user.OrganizationID,
OwnerID: user.UserID,
}).WithAgent().Do()
ctx := testutil.Context(t, testutil.WaitLong)
// Connect as a real workspace agent.
ac := agentsdk.New(client.URL, agentsdk.WithFixedToken(r.AgentToken))
conn, err := ac.ConnectRPC(ctx)
require.NoError(t, err)
defer conn.Close()
agentClient := agentproto.NewDRPCAgentClient(conn)
sessionID := uuid.New()
_, err = agentClient.ReportBoundaryLogs(ctx, &agentproto.ReportBoundaryLogsRequest{
SessionId: sessionID.String(),
ConfinedProcessName: "claude-code",
Logs: []*agentproto.BoundaryLog{
{
Allowed: true,
Time: timestamppb.New(dbtime.Now()),
SequenceNumber: 0,
Resource: &agentproto.BoundaryLog_HttpRequest_{
HttpRequest: &agentproto.BoundaryLog_HttpRequest{
Method: "GET",
Url: "https://example.com",
MatchedRule: "domain=example.com",
},
},
},
},
})
require.NoError(t, err)
// Verify persistence via the raw store: because ReportBoundaryLogs swallows
// DB errors and returns success regardless, only a direct read proves the
// session and log were actually persisted under agent RBAC.
sess, err := store.GetBoundarySessionByID(ctx, sessionID)
require.NoError(t, err, "session must be persisted")
require.Equal(t, r.Agents[0].ID, sess.WorkspaceAgentID)
logs, err := store.ListBoundaryLogsBySessionID(ctx, database.ListBoundaryLogsBySessionIDParams{
SessionID: sessionID,
})
require.NoError(t, err)
require.Len(t, logs, 1, "log must be persisted")
// Assert that the agent subject cannot read boundary sessions.
memberRole, err := rbac.RoleByName(rbac.RoleMember())
require.NoError(t, err)
agentSubject := rbac.Subject{
ID: r.Workspace.OwnerID.String(),
Roles: rbac.Roles{memberRole},
Scope: rbac.WorkspaceAgentScope(rbac.WorkspaceAgentScopeParams{
WorkspaceID: r.Workspace.ID,
OwnerID: r.Workspace.OwnerID,
TemplateID: r.Workspace.TemplateID,
VersionID: r.Build.TemplateVersionID,
}),
}.WithCachedASTValue()
auth := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
acsPtr := &atomic.Pointer[dbauthz.AccessControlStore]{}
var acs dbauthz.AccessControlStore = dbauthz.AGPLTemplateAccessControlStore{}
acsPtr.Store(&acs)
authzStore := dbauthz.New(store, auth, testutil.Logger(t), acsPtr)
agentCtx := dbauthz.As(context.Background(), agentSubject)
_, err = authzStore.GetBoundarySessionByID(agentCtx, sessionID)
require.True(t, dbauthz.IsNotAuthorizedError(err),
"agents must not be able to read boundary sessions, got: %v", err)
}