@@ -10,9 +10,12 @@ import (
1010 "net/http"
1111 "net/url"
1212 "strings"
13+ "sync"
1314 "sync/atomic"
15+ "time"
1416
1517 "github.com/open-policy-agent/opa/sdk"
18+ opalog "github.com/open-policy-agent/opa/v1/logging"
1619
1720 "github.com/kagenti/kagenti-extensions/authbridge/authlib/config"
1821 "github.com/kagenti/kagenti-extensions/authbridge/authlib/pipeline"
@@ -82,21 +85,100 @@ func (c *opaConfig) validate() error {
8285 return nil
8386}
8487
88+ // slogBridge bridges OPA's logging.Logger interface to Go's slog, so that all
89+ // OPA SDK internal messages (bundle downloads, parse errors, activation) surface
90+ // in the authbridge log stream.
91+ type slogBridge struct {
92+ level opalog.Level
93+ fields map [string ]interface {}
94+ }
95+
96+ func newSlogBridge () * slogBridge {
97+ return & slogBridge {level : opalog .Debug }
98+ }
99+
100+ func (b * slogBridge ) attrs () []slog.Attr {
101+ attrs := make ([]slog.Attr , 0 , len (b .fields )+ 1 )
102+ attrs = append (attrs , slog .String ("component" , "opa-sdk" ))
103+ for k , v := range b .fields {
104+ attrs = append (attrs , slog .Any (k , v ))
105+ }
106+ return attrs
107+ }
108+
109+ func (b * slogBridge ) Debug (msg string , a ... interface {}) {
110+ if len (a ) > 0 {
111+ msg = fmt .Sprintf (msg , a ... )
112+ }
113+ slog .LogAttrs (context .Background (), slog .LevelDebug , msg , b .attrs ()... )
114+ }
115+
116+ func (b * slogBridge ) Info (msg string , a ... interface {}) {
117+ if len (a ) > 0 {
118+ msg = fmt .Sprintf (msg , a ... )
119+ }
120+ slog .LogAttrs (context .Background (), slog .LevelInfo , msg , b .attrs ()... )
121+ }
122+
123+ func (b * slogBridge ) Warn (msg string , a ... interface {}) {
124+ if len (a ) > 0 {
125+ msg = fmt .Sprintf (msg , a ... )
126+ }
127+ slog .LogAttrs (context .Background (), slog .LevelWarn , msg , b .attrs ()... )
128+ }
129+
130+ func (b * slogBridge ) Error (msg string , a ... interface {}) {
131+ if len (a ) > 0 {
132+ msg = fmt .Sprintf (msg , a ... )
133+ }
134+ slog .LogAttrs (context .Background (), slog .LevelError , msg , b .attrs ()... )
135+ }
136+
137+ func (b * slogBridge ) WithFields (fields map [string ]interface {}) opalog.Logger {
138+ cp := & slogBridge {level : b .level , fields : make (map [string ]interface {}, len (b .fields )+ len (fields ))}
139+ for k , v := range b .fields {
140+ cp .fields [k ] = v
141+ }
142+ for k , v := range fields {
143+ cp .fields [k ] = v
144+ }
145+ return cp
146+ }
147+
148+ func (b * slogBridge ) SetLevel (level opalog.Level ) { b .level = level }
149+ func (b * slogBridge ) GetLevel () opalog.Level { return b .level }
150+
85151// decider abstracts OPA decision-making for testability.
86152type decider interface {
87153 Decision (ctx context.Context , options sdk.DecisionOptions ) (* sdk.DecisionResult , error )
88154 Stop (ctx context.Context )
89155}
90156
157+ // sharedSDK holds the process-wide singleton OPA SDK instance. Both inbound
158+ // and outbound plugin instances share one SDK (one bundle download, one
159+ // memory footprint) because the bundle is per-agent, not per-direction.
160+ type sharedSDK struct {
161+ decider decider
162+ ready atomic.Bool
163+ done chan struct {}
164+ bundleURL string
165+ agentID string
166+ refCount int
167+ }
168+
169+ var (
170+ singletonMu sync.Mutex
171+ singleton * sharedSDK
172+ )
173+
91174// OPA evaluates requests against OPA bundles downloaded from a Kagenti
92175// Bundle Server. The bundle resource path is derived from the agent's
93176// identity (/shared/client-id.txt).
94177type OPA struct {
95178 cfg opaConfig
96179 inc includeSet
97180 agentID string
98- decider atomic.Pointer [decider ]
99- ready atomic.Bool
181+ shared atomic.Pointer [sharedSDK ]
100182 bgCancel atomic.Pointer [context.CancelFunc ]
101183}
102184
@@ -108,8 +190,6 @@ func (p *OPA) Name() string { return "opa" }
108190
109191func (p * OPA ) Capabilities () pipeline.PluginCapabilities {
110192 return pipeline.PluginCapabilities {
111- Requires : []string {"jwt-validation" },
112- RequiresAny : []string {"a2a-parser" , "mcp-parser" , "inference-parser" },
113193 Description : "OPA policy enforcement for inbound and outbound requests." ,
114194 }
115195}
@@ -152,7 +232,12 @@ func (p *OPA) Configure(raw json.RawMessage) error {
152232
153233func (p * OPA ) Init (_ context.Context ) error {
154234 if p .agentID != "" {
155- return p .startOPA ()
235+ slog .Info ("opa: initializing with agent_id" , "agent_id" , p .agentID )
236+ if err := p .startOPA (); err != nil {
237+ slog .Error ("opa: initialization failed" , "error" , err , "agent_id" , p .agentID )
238+ return err
239+ }
240+ return nil
156241 }
157242 if p .cfg .AgentIDFile == "" {
158243 return errors .New ("opa: no agent_id or agent_id_file configured" )
@@ -179,24 +264,74 @@ func (p *OPA) Init(_ context.Context) error {
179264}
180265
181266func (p * OPA ) startOPA () error {
267+ singletonMu .Lock ()
268+ defer singletonMu .Unlock ()
269+
270+ if singleton != nil {
271+ if singleton .bundleURL != p .cfg .BundleURL {
272+ slog .Warn ("opa: redundant bundle_url for the opa sdk singleton - second config skipped" ,
273+ "skipped_bundle_url" , p .cfg .BundleURL ,
274+ "active_bundle_url" , singleton .bundleURL )
275+ }
276+ singleton .refCount ++
277+ p .shared .Store (singleton )
278+ slog .Info ("opa: reusing shared OPA SDK singleton" , "agent_id" , singleton .agentID )
279+ return nil
280+ }
281+
182282 cfgBytes , agentID , err := p .buildOPAConfig ()
183283 if err != nil {
284+ slog .Error ("opa: failed to build OPA config" , "error" , err )
184285 return err
185286 }
287+
288+ slog .Info ("opa: starting OPA SDK with config" , "config" , string (cfgBytes ), "agent_id" , agentID )
289+
186290 readyCh := make (chan struct {})
291+
292+ opaLogger := newSlogBridge ()
293+
187294 opa , err := sdk .New (context .Background (), sdk.Options {
188- Config : bytes .NewReader (cfgBytes ),
189- Ready : readyCh ,
295+ Config : bytes .NewReader (cfgBytes ),
296+ Ready : readyCh ,
297+ Logger : opaLogger ,
298+ ConsoleLogger : opaLogger ,
299+ V1Compatible : true ,
190300 })
191301 if err != nil {
302+ slog .Error ("opa: failed to initialize OPA SDK" , "error" , err , "agent_id" , agentID )
192303 return fmt .Errorf ("opa sdk.New: %w" , err )
193304 }
194- var dec decider = opa
195- p .decider .Store (& dec )
305+
306+ s := & sharedSDK {
307+ decider : opa ,
308+ done : make (chan struct {}),
309+ bundleURL : p .cfg .BundleURL ,
310+ agentID : agentID ,
311+ refCount : 1 ,
312+ }
313+ singleton = s
314+ p .shared .Store (s )
315+
196316 go func () {
197- <- readyCh
198- p .ready .Store (true )
199- slog .Info ("opa: bundle loaded and policy activated" , "agent_id" , agentID )
317+ select {
318+ case <- readyCh :
319+ s .ready .Store (true )
320+ slog .Info ("opa: bundle loaded and policy activated" , "agent_id" , agentID )
321+ return
322+ case <- s .done :
323+ return
324+ case <- time .After (30 * time .Second ):
325+ slog .Warn ("opa: bundle not yet available after 30s, will keep retrying" ,
326+ "agent_id" , agentID ,
327+ "bundle_url" , p .cfg .BundleURL )
328+ }
329+ select {
330+ case <- readyCh :
331+ s .ready .Store (true )
332+ slog .Info ("opa: bundle loaded and policy activated" , "agent_id" , agentID )
333+ case <- s .done :
334+ }
200335 }()
201336 return nil
202337}
@@ -208,8 +343,12 @@ func (p *OPA) buildOPAConfig() ([]byte, string, error) {
208343 return nil , "" , errors .New ("agentID is empty" )
209344 }
210345
211- // Escape agentID to prevent path traversal and ensure safe URL path segment
212- escapedAgentID := url .PathEscape (agentID )
346+ // Strip "spiffe://" prefix if present to get the SPIFFE ID for the query parameter
347+ spiffeID := strings .TrimPrefix (agentID , "spiffe://" )
348+
349+ // URL-encode SPIFFE ID for query parameter
350+ // This is REQUIRED because SPIFFE IDs contain '/' which must be encoded as %2F
351+ escapedSPIFFEID := url .QueryEscape (spiffeID )
213352
214353 cfg := map [string ]any {
215354 "services" : map [string ]any {
@@ -220,13 +359,19 @@ func (p *OPA) buildOPAConfig() ([]byte, string, error) {
220359 "bundles" : map [string ]any {
221360 "authz" : map [string ]any {
222361 "service" : "kagenti" ,
223- "resource" : fmt .Sprintf ("bundles/%s.tar.gz " , escapedAgentID ),
362+ "resource" : fmt .Sprintf ("bundles?spiffe=%s " , escapedSPIFFEID ),
224363 "polling" : map [string ]any {
225364 "min_delay_seconds" : p .cfg .PollingMinDelay ,
226365 "max_delay_seconds" : p .cfg .PollingMaxDelay ,
227366 },
228367 },
229368 },
369+ "decision_logs" : map [string ]any {
370+ "console" : true ,
371+ },
372+ "logging" : map [string ]any {
373+ "level" : "info" ,
374+ },
230375 }
231376 data , _ := json .Marshal (cfg )
232377 return data , agentID , nil
@@ -236,14 +381,27 @@ func (p *OPA) Shutdown(ctx context.Context) error {
236381 if cancel := p .bgCancel .Swap (nil ); cancel != nil {
237382 (* cancel )()
238383 }
239- if dec := p .decider .Load (); dec != nil {
240- (* dec ).Stop (ctx )
384+ s := p .shared .Swap (nil )
385+ if s == nil {
386+ return nil
387+ }
388+ singletonMu .Lock ()
389+ defer singletonMu .Unlock ()
390+ s .refCount --
391+ if s .refCount <= 0 {
392+ close (s .done )
393+ s .decider .Stop (ctx )
394+ singleton = nil
241395 }
242396 return nil
243397}
244398
245399func (p * OPA ) Ready () bool {
246- return p .ready .Load ()
400+ s := p .shared .Load ()
401+ if s == nil {
402+ return false
403+ }
404+ return s .ready .Load ()
247405}
248406
249407func (p * OPA ) decisionPath (pctx * pipeline.Context , phase string ) string {
@@ -260,8 +418,8 @@ func (p *OPA) decisionPath(pctx *pipeline.Context, phase string) string {
260418}
261419
262420func (p * OPA ) OnRequest (ctx context.Context , pctx * pipeline.Context ) pipeline.Action {
263- dec := p .decider .Load ()
264- if dec == nil || ! p .ready .Load () {
421+ s := p .shared .Load ()
422+ if s == nil || ! s .ready .Load () {
265423 pctx .Record (pipeline.Invocation {
266424 Action : pipeline .ActionDeny ,
267425 Reason : "opa_not_ready" ,
@@ -271,7 +429,7 @@ func (p *OPA) OnRequest(ctx context.Context, pctx *pipeline.Context) pipeline.Ac
271429
272430 path := p .decisionPath (pctx , "request" )
273431 input := buildInput (pctx , p .inc )
274- result , err := ( * dec ) .Decision (ctx , sdk.DecisionOptions {
432+ result , err := s . decider .Decision (ctx , sdk.DecisionOptions {
275433 Path : path ,
276434 Input : input ,
277435 })
@@ -308,8 +466,8 @@ func (p *OPA) OnRequest(ctx context.Context, pctx *pipeline.Context) pipeline.Ac
308466}
309467
310468func (p * OPA ) OnResponse (ctx context.Context , pctx * pipeline.Context ) pipeline.Action {
311- dec := p .decider .Load ()
312- if dec == nil || ! p .ready .Load () {
469+ s := p .shared .Load ()
470+ if s == nil || ! s .ready .Load () {
313471 pctx .Record (pipeline.Invocation {
314472 Action : pipeline .ActionDeny ,
315473 Reason : "opa_not_ready" ,
@@ -323,7 +481,7 @@ func (p *OPA) OnResponse(ctx context.Context, pctx *pipeline.Context) pipeline.A
323481 "status_code" : pctx .StatusCode ,
324482 "headers" : flattenHeaders (pctx .ResponseHeaders ),
325483 }
326- result , err := ( * dec ) .Decision (ctx , sdk.DecisionOptions {
484+ result , err := s . decider .Decision (ctx , sdk.DecisionOptions {
327485 Path : path ,
328486 Input : input ,
329487 })
0 commit comments