package lua // Per-caller state scoping. // // Role visibility answers "may this role see the plugin at all". It does not // answer "which PART of its data", and for the billing plugin those are // different questions: the global figures (total spend, every source, every // model) are deployment data, while the per-key buckets describe one caller. // // So a plugin may declare a split, and the gateway then hands a non-admin // caller only the buckets that are theirs. The plugin declares it because only // the plugin author knows which of its buckets are global. import "strings" // StateScope is a plugin's declaration of which state buckets are global. type StateScope struct { // Global names the buckets that describe the whole deployment. Anything not // listed is treated as per-caller and filtered. Global []string `json:"global,omitempty"` // Depth gives the nesting level of each PER-CALLER bucket: 1 for // {maskedKey: bucket}, 2 for {day: {maskedKey: bucket}}. It is declared // because both shapes are maps and sniffing would confuse an empty bucket // with a nested one. Depth map[string]int `json:"depth,omitempty"` } // IsGlobal reports whether a top-level state key is deployment-wide. func (s *StateScope) IsGlobal(key string) bool { if s == nil { return true } for _, g := range s.Global { if g == key { return true } } return false } // StateScopeFor returns a plugin's declared split, or nil when it declares none. func (ps *Plugins) StateScopeFor(name string) *StateScope { ps.mu.RLock() defer ps.mu.RUnlock() for _, p := range ps.plugins { if p.Info.Name != name || p.UI == nil { continue } return p.UI.StateScope } return nil } // compileDepthMap normalizes the declared depth table, defaulting to 1 for a // per-caller bucket the author did not annotate. func compileDepthMap(in map[string]int) map[string]int { if len(in) == 0 { return nil } out := make(map[string]int, len(in)) for k, v := range in { if v < 1 { v = 1 } out[strings.TrimSpace(k)] = v } return out } // DepthOf returns the declared nesting level for a per-caller bucket, 1 by // default. A bucket the author did not annotate is assumed flat, which is the // common shape ({maskedKey: row}). func (s *StateScope) DepthOf(key string) int { if s == nil || s.Depth == nil { return 1 } if d, ok := s.Depth[key]; ok { return d } return 1 } // VisibilityOverrides returns every operator override in force, keyed by plugin // name. An absent name means the plugin's own declaration applies. // // The WebUI needs this to show which visibility is IMPOSED, not merely which is // in effect: an operator comparing two deployments should not have to open a // .lua to learn whether a page is admin-only by the plugin's choice or by their // own override. func (ps *Plugins) VisibilityOverrides() map[string]string { ps.mu.RLock() defer ps.mu.RUnlock() if len(ps.visibility) == 0 { return nil } epKeys := make(map[string]struct{}) for _, e := range ps.endpointAccessLocked() { epKeys[endpointOverrideKey(e.Plugin, e.Path)] = struct{}{} } out := make(map[string]string, len(ps.visibility)) for k, v := range ps.visibility { // Per-endpoint overrides live in the SAME map under a "name#path" key. // They are endpoint policy, not a plugin audience, so leaking them here // both mislabels them and lets the plugin page write an audience of the // form "probe#hello" that no code ever reads as one. // Per-endpoint overrides share this map under a "name#path" key. They // are endpoint policy, not a plugin audience: reporting them here both // mislabels them and invites the plugin page to save an audience of the // form "probe#hello" that nothing reads as an audience. The set of // endpoint keys is already computed, so ask it rather than re-deriving // the shape from the key. if _, isEndpoint := epKeys[k]; isEndpoint { continue } out[k] = v } return out }