package lua import ( "encoding/json" "os" ) // Kernel action grants are OPERATOR policy, persisted separately from both // plugin state and the endpoint/visibility overrides. // // Why a separate file rather than reusing .access.json: // // - .access.json is a flat map[string]string ("name" or "name#path" -> spec). // Kernel grants are a list plus a boolean, so folding them in would either // change the shape of a file an older binary still reads, or force an // encoding that cannot round-trip. An upgrade must not corrupt a running // deployment's restrictions. // - The two answer different questions — "who may call this endpoint" vs // "what may this plugin do to the kernel" — and an operator looking at one // should not have to parse the other. // // Shape: { "": { "actions": ["reload", ...], "all_read": true } } const kernelPermsFileName = ".kernel-perms.json" func (ps *Plugins) kernelPermsFile() string { if ps.dir == "" { return "" } return ps.dir + string(os.PathSeparator) + kernelPermsFileName } // LoadKernelPermissions reads the persisted grants. A missing file is the normal // fresh-install state (nothing granted); a corrupt file is logged and ignored // rather than fatal — losing a grant is recoverable, refusing to boot is not. func (ps *Plugins) LoadKernelPermissions() error { path := ps.kernelPermsFile() if path == "" { return nil } b, err := os.ReadFile(path) if os.IsNotExist(err) { return nil } if err != nil { return err } raw := map[string]jsonPermission{} if err := json.Unmarshal(b, &raw); err != nil { return err } ps.mu.Lock() defer ps.mu.Unlock() if ps.kernelPerms == nil { ps.kernelPerms = kernelPermissionSet{} } for name, jp := range raw { ps.kernelPerms[name] = jp.unmarshal() } return nil } // saveKernelPermissionsLocked flushes the grant map. Caller holds ps.mu. // // Grants drop unknown action names on write. If an operator authorized an action // that a later release removed, keeping the name would leave a stale entry that // nothing can ever satisfy — and it would reappear in the UI as if it were live. func (ps *Plugins) saveKernelPermissionsLocked() { path := ps.kernelPermsFile() if path == "" { return } out := map[string]jsonPermission{} for name, perm := range ps.kernelPerms { valid := make([]string, 0, len(perm.Actions)) for _, a := range perm.Actions { if _, ok := LookupKernelAction(a); ok { valid = append(valid, a) } } if len(valid) == 0 && !perm.AllRead { continue // nothing granted: presence would be a lie } out[name] = KernelPermission{Actions: valid, AllRead: perm.AllRead}.marshal() } if len(out) == 0 { _ = os.Remove(path) return } writeJSONAtomic(path, out) }