Files
coder/scripts/clidocgen/gen.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

159 lines
3.5 KiB
Go

package main
import (
_ "embed"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/acarl005/stripansi"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/scripts/atomicwrite"
"github.com/coder/flog"
"github.com/coder/serpent"
)
//go:embed command.tpl
var commandTemplateRaw string
var commandTemplate *template.Template
func init() {
commandTemplate = template.Must(
template.New("command.tpl").Funcs(template.FuncMap{
"visibleSubcommands": func(cmd *serpent.Command) []*serpent.Command {
var visible []*serpent.Command
for _, sub := range cmd.Children {
if sub.Hidden {
continue
}
visible = append(visible, sub)
}
return visible
},
"visibleOptions": func(cmd *serpent.Command) []serpent.Option {
var visible []serpent.Option
for _, opt := range cmd.Options {
if opt.Hidden {
continue
}
// Skip YAML-only options that have no CLI flag; documenting them
// as if they were flags is misleading in the CLI reference.
if opt.Flag == "" && opt.FlagShorthand == "" {
continue
}
visible = append(visible, opt)
}
return visible
},
"atRoot": func(cmd *serpent.Command) bool {
return cmd.FullName() == "coder"
},
"newLinesToBr": func(s string) string {
return strings.ReplaceAll(s, "\n", "<br/>")
},
"wrapCode": func(s string) string {
return fmt.Sprintf("<code>%s</code>", s)
},
"commandURI": fmtDocFilename,
"fullName": fullName,
"tableHeader": func() string {
return `| | |
| --- | --- |`
},
"typeHelper": func(opt *serpent.Option) string {
switch v := opt.Value.(type) {
case *serpent.Enum:
return strings.Join(v.Choices, "\\|")
case *serpent.EnumArray:
return fmt.Sprintf("[%s]", strings.Join(v.Choices, "\\|"))
default:
return v.Type()
}
},
},
).Parse(strings.TrimSpace(commandTemplateRaw)),
)
}
func fullName(cmd *serpent.Command) string {
if cmd.FullName() == "coder" {
return "coder"
}
return strings.TrimPrefix(cmd.FullName(), "coder ")
}
func fmtDocFilename(cmd *serpent.Command) string {
if cmd.FullName() == "coder" {
// Special case for index.
return "./index.md"
}
name := strings.ReplaceAll(fullName(cmd), " ", "_")
return fmt.Sprintf("%s.md", name)
}
func writeCommand(w io.Writer, cmd *serpent.Command) error {
var b strings.Builder
err := commandTemplate.Execute(&b, cmd)
if err != nil {
return err
}
content := stripansi.Strip(b.String())
// Remove the version and its right space, since during this script running
// there is no build info available
content = strings.ReplaceAll(content, buildinfo.Version()+" ", "")
// Remove references to the current working directory
cwd, err := os.Getwd()
if err != nil {
return err
}
content = strings.ReplaceAll(content, cwd, ".")
homedir, err := os.UserHomeDir()
if err != nil {
return err
}
content = strings.ReplaceAll(content, homedir, "~")
_, err = w.Write([]byte(content))
return err
}
func genTree(dir string, cmd *serpent.Command, wroteLog map[string]*serpent.Command) error {
if cmd.Hidden {
return nil
}
path := filepath.Join(dir, fmtDocFilename(cmd))
var buf strings.Builder
err := writeCommand(&buf, cmd)
if err != nil {
return err
}
err = atomicwrite.File(path, []byte(buf.String()))
if err != nil {
return err
}
flog.Successf(
"wrote\t%s",
path,
)
wroteLog[path] = cmd
for _, sub := range cmd.Children {
err = genTree(dir, sub, wroteLog)
if err != nil {
return err
}
}
return nil
}