44 "fmt"
55 "net/url"
66 "strings"
7+
8+ "github.com/coder/boundary/rulesengine"
79)
810
911// Header names and paths for session correlation.
@@ -27,14 +29,6 @@ const (
2729 CoderAgentURLEnv = "CODER_AGENT_URL"
2830)
2931
30- // InjectTarget represents a parsed target for session correlation header
31- // injection. Requests matching the domain (and optional path glob) will
32- // receive the session ID and sequence number headers.
33- type InjectTarget struct {
34- Domain string
35- Path string
36- }
37-
3832// SessionCorrelationConfig holds configuration for session correlation
3933// header injection. When enabled, boundary injects its session ID and
4034// sequence number as custom headers on matching outbound requests so
@@ -45,63 +39,22 @@ type SessionCorrelationConfig struct {
4539 // Deployments without AI Bridge in front should set this to false.
4640 Enabled bool
4741
48- // InjectTargets is the list of domain/path patterns that should
49- // receive session correlation headers.
50- InjectTargets []InjectTarget
51- }
52-
53- // ParseInjectTarget parses a string of the form "domain=... path=..."
54- // into an InjectTarget. The domain key is required; path is optional.
55- func ParseInjectTarget (raw string ) (InjectTarget , error ) {
56- raw = strings .TrimSpace (raw )
57- if raw == "" {
58- return InjectTarget {}, fmt .Errorf ("inject target must not be empty" )
59- }
60-
61- var target InjectTarget
62- seen := make (map [string ]bool )
63- for _ , part := range strings .Fields (raw ) {
64- key , value , ok := strings .Cut (part , "=" )
65- if ! ok {
66- return InjectTarget {}, fmt .Errorf (
67- "inject target: malformed key-value pair %q, expected key=value" , part ,
68- )
69- }
70- if seen [key ] {
71- return InjectTarget {}, fmt .Errorf (
72- "inject target: duplicate key %q (use separate flags for multiple targets)" , key ,
73- )
74- }
75- seen [key ] = true
76- switch key {
77- case "domain" :
78- if value == "" {
79- return InjectTarget {}, fmt .Errorf ("inject target: domain must not be empty" )
80- }
81- target .Domain = value
82- case "path" :
83- target .Path = value
84- default :
85- return InjectTarget {}, fmt .Errorf ("inject target: unknown key %q" , key )
86- }
87- }
88-
89- if target .Domain == "" {
90- return InjectTarget {}, fmt .Errorf ("inject target: domain is required" )
91- }
92-
93- return target , nil
42+ // InjectTargets is the list of raw rule specs (same syntax as --allow)
43+ // that should receive session correlation headers. Each string uses the
44+ // rulesengine "domain=... path=..." format so that inject target
45+ // matching is identical to allow-rule matching.
46+ InjectTargets []string
9447}
9548
96- // DefaultInjectTargetFromEnv derives an InjectTarget from the CODER_AGENT_URL
97- // variable in the provided environment slice. It returns nil if the variable is
98- // absent, empty, or not a valid URL with a host. The derived target uses
99- // DefaultAIBridgePath as the path glob so that all AI Bridge traffic on the
100- // control-plane host is matched.
49+ // DefaultInjectTargetFromEnv derives an inject target rule string from the
50+ // CODER_AGENT_URL variable in the provided environment slice. It returns ""
51+ // if the variable is absent, empty, or not a valid URL with a host. The
52+ // derived target uses DefaultAIBridgePath as the path glob so that all AI
53+ // Bridge traffic on the control-plane host is matched.
10154//
10255// The environ parameter is accepted rather than reading os.Environ directly so
10356// that callers (and tests) can supply an arbitrary environment.
104- func DefaultInjectTargetFromEnv (environ []string ) * InjectTarget {
57+ func DefaultInjectTargetFromEnv (environ []string ) string {
10558 var raw string
10659 for _ , e := range environ {
10760 k , v , ok := strings .Cut (e , "=" )
@@ -111,23 +64,22 @@ func DefaultInjectTargetFromEnv(environ []string) *InjectTarget {
11164 }
11265 }
11366 if raw == "" {
114- return nil
67+ return ""
11568 }
11669
11770 u , err := url .Parse (raw )
11871 if err != nil || u .Host == "" {
119- return nil
72+ return ""
12073 }
12174
122- return & InjectTarget {
123- Domain : u .Hostname (),
124- Path : DefaultAIBridgePath ,
125- }
75+ return fmt .Sprintf ("domain=%s path=%s" , u .Hostname (), DefaultAIBridgePath )
12676}
12777
12878// ValidateSessionCorrelation checks that the session correlation config
129- // is internally consistent. It returns an error describing the first
130- // problem found, or nil if the config is valid.
79+ // is internally consistent. When enabled it verifies that at least one
80+ // inject target is configured and that every target string is a valid
81+ // rulesengine rule. It returns an error describing the first problem
82+ // found, or nil if the config is valid.
13183func ValidateSessionCorrelation (cfg SessionCorrelationConfig ) error {
13284 if ! cfg .Enabled {
13385 return nil
@@ -139,5 +91,26 @@ func ValidateSessionCorrelation(cfg SessionCorrelationConfig) error {
13991 )
14092 }
14193
94+ // Reject empty target strings before passing to the parser.
95+ for _ , t := range cfg .InjectTargets {
96+ if strings .TrimSpace (t ) == "" {
97+ return fmt .Errorf ("inject target: must not be empty" )
98+ }
99+ }
100+
101+ // Validate each target parses as a rulesengine rule.
102+ rules , err := rulesengine .ParseAllowSpecs (cfg .InjectTargets )
103+ if err != nil {
104+ return fmt .Errorf ("inject target: %w" , err )
105+ }
106+
107+ // Inject targets must specify a domain; path-only rules are not
108+ // meaningful for header injection.
109+ for i , r := range rules {
110+ if r .HostPattern == nil {
111+ return fmt .Errorf ("inject target %q: domain is required" , cfg .InjectTargets [i ])
112+ }
113+ }
114+
142115 return nil
143116}
0 commit comments