mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
生产实证:channels 插件的 auth/state 在主机已授权后仍被拒
(auth/state 失败: host "copilot.tencent.com" is not an allowed outbound destination),
而 /api/plugins/channels/kernel 明明显示 hosts_granted=26。
缺陷一(授权对已启动 worker 无效):
httpAllowlist 是不可变快照;bootPluginState 在 worker 启动时把指针绑进 Lua
闭包,闭包与 worker 同寿。GrantExtraHosts 之后 ps.allowHTTP 被**整个换新**
(newHTTPAllowlist 返回新实例),已启动的 worker 仍持旧指针 → 授权接口返回
ok、设置页显示已批,请求却继续被拒。分片 VM(plugin_shard.go)同样中招。
修法:allowlist 改为带 RWMutex 的**可变共享对象**,GrantExtraHosts 与
rebuildOutboundAllowlistLocked 原地 add —— 所有闭包立即看到新授权,worker
无需重建。rebuild 从"替换实例"改为"原地折叠",并在注释里写明为什么。
缺陷二(授权不持久化):
.kernel-perms.json 只写 actions;hosts 授权只存在运行态 map,重启即失。
设置页看不出异常(actions 有持久化,读起来像"都持久化了")。
修法:文件行改为 jsonPluginPolicy{actions,all_read,hosts}(旧文件向后兼容,
纯 actions 行照常解析);Load 恢复 extraHosts;Save 把每个插件的 host 授权
与 actions 一起写入(支持"只批了主机、没批动作"的行)。
回归测试(internal/lua/plugin_http_test.go):
- TestGrantExtraHostsVisibleToAlreadyBootedWorker:按事故顺序构造(先启动
worker → 再授权),断言同一批 worker 立即可用新授权 + 旧引用不再失效。
- TestHostGrantsPersistAcrossRestart:授权落盘 + 新 registry 读回 + 插件重载
后授权保留。
go test ./internal/lua/ 全绿;build/vet 通过。
499 lines
20 KiB
Go
499 lines
20 KiB
Go
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}
|
||
}
|
||
|
||
type jsonPluginPolicy struct {
|
||
Actions []string `json:"actions,omitempty"`
|
||
AllRead bool `json:"all_read,omitempty"`
|
||
Hosts []string `json:"hosts,omitempty"`
|
||
}
|
||
|
||
func policyFromActions(p KernelPermission) jsonPluginPolicy {
|
||
return jsonPluginPolicy{Actions: p.Actions, AllRead: p.AllRead}
|
||
}
|
||
|
||
// KernelActionView is one action as the settings page needs it: the catalog entry
|
||
// plus whether THIS plugin currently holds it and whether the plugin asked for it.
|
||
type KernelActionView struct {
|
||
KernelAction
|
||
Granted bool `json:"granted"`
|
||
Declared bool `json:"declared"`
|
||
}
|
||
|
||
// KernelActionsView lists the catalog annotated for one plugin.
|
||
//
|
||
// Granted and Declared are both reported because they answer different questions:
|
||
// "may this plugin do it" and "did this plugin ask for it". An action that is
|
||
// declared but not granted is the one an operator most needs to notice.
|
||
func (ps *Plugins) KernelActionsView(plugin string) []KernelActionView {
|
||
perm := ps.PluginKernelPermission(plugin)
|
||
declared := map[string]bool{}
|
||
ps.mu.RLock()
|
||
for _, p := range ps.plugins {
|
||
if p.Info.Name == plugin {
|
||
for _, d := range p.KernelDeclared {
|
||
declared[d] = true
|
||
}
|
||
}
|
||
}
|
||
ps.mu.RUnlock()
|
||
out := make([]KernelActionView, 0, len(kernelCatalog))
|
||
for _, a := range KernelActions() {
|
||
out = append(out, KernelActionView{
|
||
KernelAction: a,
|
||
Granted: perm.allows(a),
|
||
Declared: declared[a.Name],
|
||
})
|
||
}
|
||
return out
|
||
}
|