mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
feat(lua): 插件可调内核动作面(R4-c)+ 凭据保险库(R4-d)
用户指出「接口不全」——此前插件只有 __llmsproxy_adapters 三个手写闭包,
够不到内核。对照参考插件(dsh-our-free-model,13 个账号渠道)的真实能力,
缺的正是这两块:
插件自己驱动链接认证 → 存凭据 → 定时领额度 → 把源接进内核
## R4-c 内核动作面:__llmsproxy_kernel
18 个动作,全部映射到既有 Core 方法(不是新造能力):
只读 6:status / list_sources / list_models / list_adapters /
list_auto_rules / adapter_owner
写 8:reload / reset_health / probe_all / save_auto_rules /
save_key_auto / add_source / create_key / update_key
破坏性 4:remove_source / delete_key / remove_adapter / set_plugin_enabled
**add_source 是「插件注册自己的上游」的落点**——插件拿到凭据后可以自己把
端点接进网关,无需运维改 config.yaml。已端到端验证:插件调它 → 内核源列表
出现 → 模型进入可路由列表。
依赖方向:lua 不能 import core,所以接口定义在 lua、实现放 core
(kernel_actions.go),走 SetKernelAPI 注入——与 SetOutboundHosts 同一先例。
**凭据边界**:动作面**有意排除** list_keys/find_key。Core.ListKeys() 返回
明文网关密钥,交给第三方 Lua 等于交出整个部署的凭据面。有回归测试锁死
这条(TestKernelActionSurfaceExcludesPlaintextKeys)。list_sources 返回的
api_key 也强制掩码(只给 ***+后4位)。
**权限**(用户选「全开+设置页可配」):18 个动作全部可达,但**默认拒绝**,
授权落盘 .kernel-perms.json(独立文件,不并进 .access.json——后者是
map[string]string,塞进去会破坏旧二进制能读的格式)。danger 级不接受通配符。
插件可在源码 plugin.kernel={...} **申请**,设置页显示申请 vs 已授予。
**Go 侧 deadline**(用户选定):每个动作有超时预算,超时把 (nil,err) 还给
Lua。这可行的原因:Lua 只是等返回值,不占 Lua 栈执行权——与「Lua 死循环
不可中断」是两件事。
## R4-d 凭据保险库:__llmsproxy_secrets
此前插件存 token 只能写 plugin.state:0755 目录、明文、与计费累计量同文件、
会被 PUT /state 整体替换、会进 WebUI。现在独立文件:
<plugin_dir>/.secrets/<plugin>.json 文件 0600,目录 0700
临时文件**先 chmod 再 rename**,全程不存在宽权限窗口。读取身份在插件 boot
时绑定(不从 Lua 状态现读)⇒ 跨插件读不可行是机制而非约定。插件删除时清空
(DeleteAllSecrets + 启动 PurgeOrphanVaults)。
## 开发中被测试抓出的两个真实缺陷(我自己引入的)
写了 TestNoKernelActionDeadlocksFromLua 对**每个**动作断言「在插件 Lua 调用
(此时持有 p.mu)内不得死锁」,抓出:
1. **自死锁**:set_plugin_enabled → SetEnabled → p.mu.Lock(),而死锁发生在
调用者已持有的同一把锁上。
2. **更隐蔽的迟到副作用**:超时把 error 返回给插件后,动作仍在幕后执行完成——
插件以为失败,实际已生效。
修法:标注 Deferred 的动作在本次调用返回后异步执行,如实返回 accepted:true
而不是谎报成功。18 个动作全部复验无死锁,另有单独测试确认副作用真的落地
(只测「不死锁」的话,一个全空转的 dispatch 也能通过)。
测试 11 个(core 8 + lua 3):动作可用/密钥掩码/默认拒绝/授权计数/
插件注册源并变为可路由/授权落盘重启保留/未知动作名被拒/凭据面排除明文/
全动作无死锁/破坏性动作生效/Lua 侧桥接可用。
This commit is contained in:
452
internal/lua/kernel_api.go
Normal file
452
internal/lua/kernel_api.go
Normal file
@ -0,0 +1,452 @@
|
||||
package lua
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Package lua — 插件可调用的内核动作面(R4-c)
|
||||
//
|
||||
// 用户需求(原话):「现在这个接口不全,可以多给一些接口让 lua 调用内核。」
|
||||
//
|
||||
// 在此之前,插件只能通过 __llmsproxy_adapters 管适配器 —— 那是三个手写闭包。
|
||||
// 内核另有十几个公开动作(重载配置、清冷却、重探上游、增删源/密钥、改 AUTO 链…),
|
||||
// 插件一个都调不到。而"自愈 / 联动 / 自动孵化"这类插件能力恰恰需要它们:
|
||||
//
|
||||
// 探测到上游整片挂掉 → probe_all + reset_health
|
||||
// 发现额度变化 → save_auto_rules 重排链路
|
||||
// 拿到新端点 → add_source
|
||||
//
|
||||
// # 为什么 Go 侧 deadline 是可行的(而 Lua 死循环不可中断)
|
||||
//
|
||||
// 这是整个设计的支点。两者形态完全不同:
|
||||
//
|
||||
// Lua 里 while true do end → 无法中断(LuaJIT 限制,实测三次)
|
||||
// Lua 里 __llmsproxy_kernel.call(...) → **可以**超时
|
||||
//
|
||||
// 因为后者 Lua 只是在等一个返回值,不占用 Lua 栈的执行权。所以每次调用在 Go
|
||||
// 侧包一层 select + ctx deadline:超时就把 (nil, err) 还给 Lua。这比"Lua 投递
|
||||
// 请求、Go 队列执行"干净 —— 队列还要定义"超时后积压的命令算什么",而 deadline
|
||||
// 的语义是闭合的。
|
||||
//
|
||||
// # 权限:全开,但默认不授予
|
||||
//
|
||||
// 用户选定「全开」(13 个动作都可达)+ 「设置页可配权限」。全开描述的是**能力
|
||||
// 存在**,不是**默认授予**:一个插件能删密钥这件事必须由运维在设置页点过确认。
|
||||
// 因此默认拒绝,授权记录在 .access.json 里(复用现有覆盖机制)。
|
||||
//
|
||||
// danger 级动作必须**逐个**授权,不接受通配符 —— 否则运维图省事写个 "*",
|
||||
// "删密钥"就和"读状态"同权了。
|
||||
|
||||
// KernelAPI 是内核暴露给插件的能力面。由 internal/core 实现并注入。
|
||||
//
|
||||
// 依赖方向:internal/lua 不能 import internal/core(方向是 core → lua),
|
||||
// 所以这里定义接口、core 侧实现 —— 与 SetOutboundHosts 同一个注入先例。
|
||||
type KernelAPI interface {
|
||||
// Actions 列出可调用的动作。设置页据此渲染权限开关,插件据此发现能力。
|
||||
Actions() []KernelAction
|
||||
// Invoke 执行一个动作。实现**必须**尊重 ctx:ctx 取消后应尽快返回,
|
||||
// 否则插件侧的超时虽然会返回错误,但该动作仍在后台跑(变成不可见的泄漏)。
|
||||
Invoke(ctx context.Context, name string, args map[string]interface{}) (interface{}, error)
|
||||
}
|
||||
|
||||
// 危险级。它决定权限授予的严格程度,也决定设置页用什么颜色提示。
|
||||
const (
|
||||
// DangerRead 无副作用,只读。仍然需要授权,但可以一次授予全部。
|
||||
DangerRead = "read"
|
||||
// DangerWrite 修改运行状态或配置,但可恢复(重载、清冷却、改链路)。
|
||||
DangerWrite = "write"
|
||||
// DangerDestructive 删除类,或让服务中断。**必须逐个授权**。
|
||||
DangerDestructive = "destructive"
|
||||
)
|
||||
|
||||
// KernelAction 描述一个可调用动作。
|
||||
type KernelAction struct {
|
||||
// Deferred marks an action that MUST NOT run while the Lua caller holds its
|
||||
// plugin lock. The Lua entry points (hook, exported call, scheduled task) all
|
||||
// hold p.mu for the whole call, and an action that re-enters the plugin layer
|
||||
// (set_plugin_enabled reaches SetEnabled → p.mu.Lock()) would self-deadlock —
|
||||
// and with a deadline, would then execute invisibly AFTER the plugin was told
|
||||
// it failed.
|
||||
//
|
||||
// Such an action is queued and run on its own goroutine once the Lua call has
|
||||
// returned, so the caller gets "accepted" rather than a lock-up or a lie.
|
||||
Deferred bool
|
||||
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Danger string `json:"danger"`
|
||||
Params []string `json:"params,omitempty"`
|
||||
// TimeoutMS is the Go-side deadline for one invocation. Long actions (a
|
||||
// full re-probe walks every source over the network) need a bigger budget
|
||||
// than a state read; making it per-action rather than one global number is
|
||||
// what keeps the deadline honest rather than arbitrary.
|
||||
TimeoutMS int `json:"timeout_ms"`
|
||||
}
|
||||
|
||||
// kernelCatalog is the full action set. It lives in the lua package (rather than
|
||||
// being discovered from the implementation) so that permissions can be validated
|
||||
// even when no Core is wired — tests, and a gateway with plugins but no kernel.
|
||||
//
|
||||
// Every action here maps to an existing Core method; adding one is a deliberate
|
||||
// act rather than a reflex. ListKeys/FindKey are deliberately ABSENT: they return
|
||||
// PLAINTEXT gateway keys, and handing those to third-party Lua would hand over
|
||||
// the deployment's entire credential surface. Key-related reads go through
|
||||
// masked forms instead.
|
||||
var kernelCatalog = []KernelAction{
|
||||
// ---- 只读 ----
|
||||
{Name: "status", Title: "运行状态", Danger: DangerRead, TimeoutMS: 3000,
|
||||
Description: "各源的调度状态(冷却、连续失败、最近错误)"},
|
||||
{Name: "list_sources", Title: "源列表", Danger: DangerRead, TimeoutMS: 3000,
|
||||
Description: "已配置的上游源(api_key 已掩码)"},
|
||||
{Name: "list_models", Title: "模型列表", Danger: DangerRead, TimeoutMS: 3000,
|
||||
Description: "当前可路由的模型 id 列表"},
|
||||
{Name: "list_adapters", Title: "适配器列表", Danger: DangerRead, TimeoutMS: 3000,
|
||||
Description: "已加载的适配器名与版本"},
|
||||
{Name: "list_auto_rules", Title: "AUTO 链路", Danger: DangerRead, TimeoutMS: 3000,
|
||||
Description: "当前全局 AUTO 链的槽位"},
|
||||
{Name: "adapter_owner", Title: "适配器归属", Danger: DangerRead, TimeoutMS: 3000,
|
||||
Params: []string{"name"},
|
||||
Description: "某适配器由谁安装(operator 或 plugin:名字)"},
|
||||
|
||||
// ---- 写(可恢复) ----
|
||||
{Name: "reload", Title: "重载配置", Danger: DangerWrite, TimeoutMS: 30000,
|
||||
Description: "重新读取运行配置并重建源/链路"},
|
||||
{Name: "reset_health", Title: "清除冷却", Danger: DangerWrite, TimeoutMS: 5000,
|
||||
Description: "清掉所有源的退避状态(修好上游后立即恢复)"},
|
||||
{Name: "probe_all", Title: "全量探测", Danger: DangerWrite, TimeoutMS: 60000,
|
||||
Description: "对每个源做一次真实可达性探测(走网络,慢)"},
|
||||
{Name: "save_auto_rules", Title: "设置 AUTO 链", Danger: DangerWrite, TimeoutMS: 10000,
|
||||
Params: []string{"entries"},
|
||||
Description: "替换全局 AUTO 链;entries 为 [{model, source, ...}] 数组"},
|
||||
{Name: "save_key_auto", Title: "设置按 key 链路", Danger: DangerWrite, TimeoutMS: 10000,
|
||||
Params: []string{"key_name", "entries"},
|
||||
Description: "替换某个网关 key 自己的 AUTO 链"},
|
||||
{Name: "add_source", Title: "新增源", Danger: DangerWrite, TimeoutMS: 30000,
|
||||
Params: []string{"source"},
|
||||
Description: "把一个新的上游源接入(source 为源定义表)"},
|
||||
{Name: "create_key", Title: "新建网关密钥", Danger: DangerWrite, TimeoutMS: 10000,
|
||||
Params: []string{"name", "role", "models"},
|
||||
Description: "创建一个网关 key(返回的明文密钥只在此次响应里出现)"},
|
||||
|
||||
// ---- 破坏性:必须逐个授权 ----
|
||||
{Name: "remove_source", Title: "删除源", Danger: DangerDestructive, TimeoutMS: 30000,
|
||||
Params: []string{"name"},
|
||||
Description: "删除一个上游源"},
|
||||
{Name: "delete_key", Title: "删除网关密钥", Danger: DangerDestructive, TimeoutMS: 10000,
|
||||
Params: []string{"key"},
|
||||
Description: "删除一个网关 key(正在使用的客户端会立刻 401)"},
|
||||
{Name: "update_key", Title: "修改网关密钥", Danger: DangerDestructive, TimeoutMS: 10000,
|
||||
Params: []string{"key", "name", "role", "models"},
|
||||
Description: "改动 key 的角色/可见模型(可能扩大访问面)"},
|
||||
{Name: "remove_adapter", Title: "删除适配器", Danger: DangerDestructive, TimeoutMS: 30000,
|
||||
Params: []string{"name"},
|
||||
Description: "删除适配器(仅限插件自己装的;内置与非自有的会被拒)"},
|
||||
{Name: "set_plugin_enabled", Title: "启停插件", Danger: DangerDestructive, TimeoutMS: 5000,
|
||||
Params: []string{"name", "enabled"}, Deferred: true,
|
||||
Description: "启用或停用某个插件(异步执行:在本次调用返回后生效)"},
|
||||
}
|
||||
|
||||
// kernelActionByName indexes the catalog.
|
||||
var kernelActionByName = func() map[string]KernelAction {
|
||||
m := make(map[string]KernelAction, len(kernelCatalog))
|
||||
for _, a := range kernelCatalog {
|
||||
m[a.Name] = a
|
||||
}
|
||||
return m
|
||||
}()
|
||||
|
||||
// KernelActions returns the catalog sorted by name (stable for the UI).
|
||||
func KernelActions() []KernelAction {
|
||||
out := append([]KernelAction(nil), kernelCatalog...)
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out
|
||||
}
|
||||
|
||||
// LookupKernelAction finds an action, reporting whether it exists.
|
||||
func LookupKernelAction(name string) (KernelAction, bool) {
|
||||
a, ok := kernelActionByName[name]
|
||||
return a, ok
|
||||
}
|
||||
|
||||
// ---- 权限 ----
|
||||
|
||||
// errNotPermitted is returned when a plugin calls an action it was not granted.
|
||||
// It names the action and the plugin so the operator can act on it directly.
|
||||
func notPermitted(plugin, action string) error {
|
||||
return fmt.Errorf("kernel action %q is not granted to plugin %q; "+
|
||||
"enable it in the plugin's kernel permissions", action, plugin)
|
||||
}
|
||||
|
||||
// KernelPermission is the operator's grant for one plugin: the set of action
|
||||
// names it may call, plus whether it was granted "everything readable".
|
||||
type KernelPermission struct {
|
||||
// Actions is the explicit allowlist, already expanded (no wildcards stored,
|
||||
// so a NEW action added in a future release is NOT silently granted to an
|
||||
// existing plugin — that is the safe direction for a security default).
|
||||
Actions []string
|
||||
// AllRead is true when the operator granted every read-only action at once.
|
||||
// Stored separately from Actions so that a future read-only action IS covered
|
||||
// without re-saving, while writes still require an explicit grant.
|
||||
AllRead bool
|
||||
}
|
||||
|
||||
// clone returns a copy so callers cannot mutate the stored permission.
|
||||
func (p KernelPermission) clone() KernelPermission {
|
||||
return KernelPermission{Actions: append([]string(nil), p.Actions...), AllRead: p.AllRead}
|
||||
}
|
||||
|
||||
// allows reports whether the permission covers action.
|
||||
//
|
||||
// The "all readable" grant deliberately does NOT cover a future WRITE action:
|
||||
// expanding a wildcard into whatever happens to exist later is how a convenience
|
||||
// grant turns into a privilege escalation.
|
||||
func (p KernelPermission) allows(a KernelAction) bool {
|
||||
if p.AllRead && a.Danger == DangerRead {
|
||||
return true
|
||||
}
|
||||
for _, n := range p.Actions {
|
||||
if n == a.Name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ---- 调用(带 Go 侧超时) ----
|
||||
|
||||
// ErrKernelUnavailable is returned when no KernelAPI was injected (a gateway
|
||||
// running plugins without a wired core, e.g. some tests).
|
||||
var ErrKernelUnavailable = errors.New("kernel actions are not available in this build")
|
||||
|
||||
// callKernel invokes one action with the permission check and the Go-side
|
||||
// deadline, returning a value Lua can consume.
|
||||
//
|
||||
// The deadline is the whole point: a plugin can block here harmlessly, because
|
||||
// Lua is waiting on a return value rather than spinning. On timeout the caller
|
||||
// gets a normal error; the action's own goroutine is told to stop via ctx.
|
||||
func (ps *Plugins) callKernel(plugin, name string, args map[string]interface{}) (interface{}, error) {
|
||||
api := ps.kernel()
|
||||
if api == nil {
|
||||
return nil, ErrKernelUnavailable
|
||||
}
|
||||
action, ok := LookupKernelAction(name)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown kernel action %q", name)
|
||||
}
|
||||
if !ps.kernelPermitted(plugin, action) {
|
||||
return nil, notPermitted(plugin, name)
|
||||
}
|
||||
|
||||
timeout := time.Duration(action.TimeoutMS) * time.Millisecond
|
||||
if timeout <= 0 {
|
||||
timeout = 10 * time.Second
|
||||
}
|
||||
// The caller's identity is injected HERE, not taken from args: an action that
|
||||
// needs to know who is calling (remove_adapter, to enforce ownership) must not
|
||||
// be able to be lied to by the plugin passing someone else's name.
|
||||
if args == nil {
|
||||
args = map[string]interface{}{}
|
||||
}
|
||||
args["plugin"] = plugin
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
type result struct {
|
||||
v interface{}
|
||||
err error
|
||||
}
|
||||
// A deferred action must not run under the caller's plugin lock, so it is
|
||||
// queued and returns "accepted" immediately. Reporting success here without
|
||||
// waiting is honest: the caller is told the action was ACCEPTED, and the
|
||||
// actual outcome lands in the plugin's hook_errors surface if it fails.
|
||||
if action.Deferred {
|
||||
ps.runDeferred(action, args)
|
||||
return map[string]interface{}{
|
||||
"accepted": true,
|
||||
"note": "this action runs asynchronously; check the plugin state/errors for the outcome",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// The action runs on its own goroutine so this call can give up on time.
|
||||
// Buffered so a late-finishing action never blocks on sending.
|
||||
done := make(chan result, 1)
|
||||
go func() {
|
||||
defer func() {
|
||||
// A panicking kernel action must not take the gateway down through a
|
||||
// Lua call. Convert it into an error the plugin sees.
|
||||
if rec := recover(); rec != nil {
|
||||
done <- result{err: fmt.Errorf("kernel action %s panicked: %v", name, rec)}
|
||||
}
|
||||
}()
|
||||
v, err := api.Invoke(ctx, name, args)
|
||||
done <- result{v: v, err: err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case r := <-done:
|
||||
return r.v, r.err
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("kernel action %q exceeded its %s budget", name, timeout)
|
||||
}
|
||||
}
|
||||
|
||||
// runDeferred executes a lock-re-entrant action outside the Lua call.
|
||||
//
|
||||
// It runs on a fresh goroutine precisely so it can take the plugin lock that the
|
||||
// caller still holds; waiting for the caller to unwind first would need a
|
||||
// handshake that is more machinery than the one action needs. A failure is
|
||||
// surfaced through hook_errors, which is where an operator already looks for
|
||||
// "a plugin's background work stopped working".
|
||||
func (ps *Plugins) runDeferred(a KernelAction, args map[string]interface{}) {
|
||||
api := ps.kernel()
|
||||
if api == nil {
|
||||
return
|
||||
}
|
||||
timeout := time.Duration(a.TimeoutMS) * time.Millisecond
|
||||
if timeout <= 0 {
|
||||
timeout = 10 * time.Second
|
||||
}
|
||||
// Copy the args: the caller's map is still owned by the Lua call frame.
|
||||
cp := make(map[string]interface{}, len(args))
|
||||
for k, v := range args {
|
||||
cp[k] = v
|
||||
}
|
||||
go func() {
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
ps.hookErr.note(StageRequestEnd, fmt.Sprintf("deferred kernel action %s panicked: %v", a.Name, rec))
|
||||
}
|
||||
}()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
if _, err := api.Invoke(ctx, a.Name, cp); err != nil {
|
||||
ps.hookErr.note(StageRequestEnd, fmt.Sprintf("deferred kernel action %s failed: %v", a.Name, err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// kernelPermitted applies the stored grant, with one deliberate exception: a
|
||||
// plugin's OWN adapter may always be removed by that plugin (it is not a kernel
|
||||
// resource, it is one the plugin created). Everything else needs a grant.
|
||||
func (ps *Plugins) kernelPermitted(plugin string, a KernelAction) bool {
|
||||
if a.Name == "remove_adapter" {
|
||||
return true
|
||||
}
|
||||
ps.mu.RLock()
|
||||
perm := ps.kernelPerms[plugin]
|
||||
ps.mu.RUnlock()
|
||||
return perm.allows(a)
|
||||
}
|
||||
|
||||
// SetPluginKernelPermission replaces one plugin's grant and persists it.
|
||||
//
|
||||
// Validation happens here rather than at call time so a typo'd action name is
|
||||
// reported to the operator immediately instead of silently granting nothing.
|
||||
func (ps *Plugins) SetPluginKernelPermission(plugin string, actions []string, allRead bool) error {
|
||||
if ps.find(plugin) == nil {
|
||||
return fmt.Errorf("plugin %s not found", plugin)
|
||||
}
|
||||
clean := make([]string, 0, len(actions))
|
||||
seen := map[string]bool{}
|
||||
for _, raw := range actions {
|
||||
name := strings.TrimSpace(raw)
|
||||
if name == "" || seen[name] {
|
||||
continue
|
||||
}
|
||||
a, ok := LookupKernelAction(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown kernel action %q", name)
|
||||
}
|
||||
seen[name] = true
|
||||
clean = append(clean, a.Name)
|
||||
}
|
||||
sort.Strings(clean)
|
||||
ps.mu.Lock()
|
||||
if ps.kernelPerms == nil {
|
||||
ps.kernelPerms = kernelPermissionSet{}
|
||||
}
|
||||
if len(clean) == 0 && !allRead {
|
||||
delete(ps.kernelPerms, plugin)
|
||||
} else {
|
||||
ps.kernelPerms[plugin] = KernelPermission{Actions: clean, AllRead: allRead}
|
||||
}
|
||||
ps.saveKernelPermissionsLocked()
|
||||
ps.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// PluginKernelPermission returns a copy of one plugin's grant.
|
||||
func (ps *Plugins) PluginKernelPermission(plugin string) KernelPermission {
|
||||
ps.mu.RLock()
|
||||
defer ps.mu.RUnlock()
|
||||
return ps.kernelPerms[plugin].clone()
|
||||
}
|
||||
|
||||
// KernelPermissionView is the UI-facing form: what the plugin ASKED for (from its
|
||||
// own source) next to what the operator GRANTED. The distinction matters — a
|
||||
// plugin declaring `plugin.kernel = {...}` is a request, not a privilege.
|
||||
type KernelPermissionView struct {
|
||||
Plugin string `json:"plugin"`
|
||||
Declared []string `json:"declared,omitempty"`
|
||||
Granted []string `json:"granted,omitempty"`
|
||||
AllRead bool `json:"all_read"`
|
||||
}
|
||||
|
||||
// KernelPermissions lists every plugin's declared-vs-granted state for the UI.
|
||||
func (ps *Plugins) KernelPermissions() []KernelPermissionView {
|
||||
ps.mu.RLock()
|
||||
defer ps.mu.RUnlock()
|
||||
out := make([]KernelPermissionView, 0, len(ps.plugins))
|
||||
for _, p := range ps.plugins {
|
||||
perm := ps.kernelPerms[p.Info.Name]
|
||||
out = append(out, KernelPermissionView{
|
||||
Plugin: p.Info.Name,
|
||||
Declared: append([]string(nil), p.KernelDeclared...),
|
||||
Granted: append([]string(nil), perm.Actions...),
|
||||
AllRead: perm.AllRead,
|
||||
})
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Plugin < out[j].Plugin })
|
||||
return out
|
||||
}
|
||||
|
||||
// kernel returns the injected API, or nil.
|
||||
func (ps *Plugins) kernel() KernelAPI {
|
||||
ps.mu.RLock()
|
||||
defer ps.mu.RUnlock()
|
||||
return ps.kernelAPI
|
||||
}
|
||||
|
||||
// SetKernelAPI injects the kernel action implementation. Called during wiring,
|
||||
// mirroring SetOutboundHosts.
|
||||
func (ps *Plugins) SetKernelAPI(api KernelAPI) {
|
||||
ps.mu.Lock()
|
||||
defer ps.mu.Unlock()
|
||||
ps.kernelAPI = api
|
||||
}
|
||||
|
||||
// kernelPermissionSet is the persisted per-plugin grant map.
|
||||
type kernelPermissionSet map[string]KernelPermission
|
||||
|
||||
// jsonPermission is the on-disk form of one grant. Kept separate from
|
||||
// KernelPermission so the persisted shape cannot drift with a struct refactor
|
||||
// (the access file is read by older and newer binaries across an upgrade).
|
||||
type jsonPermission struct {
|
||||
Actions []string `json:"actions,omitempty"`
|
||||
AllRead bool `json:"all_read,omitempty"`
|
||||
}
|
||||
|
||||
func (p KernelPermission) marshal() jsonPermission {
|
||||
return jsonPermission{Actions: p.Actions, AllRead: p.AllRead}
|
||||
}
|
||||
|
||||
func (j jsonPermission) unmarshal() KernelPermission {
|
||||
return KernelPermission{Actions: j.Actions, AllRead: j.AllRead}
|
||||
}
|
||||
Reference in New Issue
Block a user