mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
feat(addons): add schemaVersion 2 host policy with a deny list
A policy file may now carry a deny object. Deny rules name an add-on ID or "*" and list prefixes, exact operation names or the single token "*". Schema 1 files parse as before; a deny key in a schema 1 file, or a version above 2, is an error that names the problem. The selected file is now always opened, even when a flat --addon-host-ops list replaces its grants, because a deny must not vanish when someone adds a flag. An invalid selected file still stops startup. Add --addon-host-deny (LOCALAI_ADDON_HOST_DENY), unioned with the file, and the --addon-host-blanket and --addon-host-ui-grants flags with their config options. Assisted-by: Claude:claude-sonnet-5-5 go
This commit is contained in:
7 files changed
+442
-104
No files matched your search
@@ -43,6 +43,9 @@ type RunCMD struct {
|
||||
P2PEnableLimits string `name:"p2p-enable-limits" env:"LOCALAI_P2P_ENABLE_LIMITS" help:"Set to true to enable P2P resource limits" group:"p2p"`
|
||||
AddonHostPolicy string `name:"addon-host-policy" env:"LOCALAI_ADDON_HOST_POLICY" help:"Absolute path to administrator host operation policy JSON, read once at startup. Empty disables environment policy" group:"addons"`
|
||||
AddonHostOps string `name:"addon-host-ops" env:"LOCALAI_ADDON_HOST_OPS" help:"Add-ons that may call host operations, as a comma list of addon-id=operation-prefix. An add-on that is not listed gets no host operation channel. Empty gives none" group:"addons"`
|
||||
AddonHostDeny string `name:"addon-host-deny" env:"LOCALAI_ADDON_HOST_DENY" help:"Host operations no grant may enable, as a comma list of addon-id=operation (a prefix such as egress., an exact name or *). Use *=prefix for every add-on. Deny always wins over grants and is unioned with the deny rules of the policy file" group:"addons"`
|
||||
AddonHostBlanket string `name:"addon-host-blanket" env:"LOCALAI_ADDON_HOST_BLANKET" help:"Blanket approval of declared host operations at install and upgrade: off, official or any. When set, the UI shows it read-only. Unset leaves the choice to the administrator in the UI" group:"addons"`
|
||||
AddonHostUIGrants bool `name:"addon-host-ui-grants" env:"LOCALAI_ADDON_HOST_UI_GRANTS" default:"true" help:"Let an administrator grant host operations to add-ons in the UI. Turn it off with --addon-host-ui-grants=false to make the policy file and flags the only source of grants" group:"addons"`
|
||||
AddonsEnabled bool `name:"addons-enabled" env:"LOCALAI_ADDONS_ENABLED" default:"true" help:"Run the add-on owner, which installs and supervises add-ons. Turn it off with --addons-enabled=false" group:"addons"`
|
||||
AddonsOwnershipMode string `name:"addons-ownership-mode" env:"LOCALAI_ADDONS_OWNERSHIP_MODE" help:"Who may use add-ons: named-user (each account has its own access; needs authentication) or local-owner (every caller shares one owner). Default: local-owner when authentication is off, named-user when it is on" group:"addons"`
|
||||
AddonsRole string `name:"addons-role" env:"LOCALAI_ADDONS_ROLE" default:"combined" help:"How this process takes part in the add-on owner: combined (this node is its own owner, the default), owner (only the owner, serves the carrier on the network) or frontend (no owner here; every add-on request goes to the owner). owner and frontend need --addons-owner-address and --addons-carrier-dir" group:"addons"`
|
||||
@@ -329,6 +332,10 @@ func (r *RunCMD) Run(ctx *cliContext.Context) (result error) {
|
||||
return err
|
||||
}
|
||||
logAddonHostOpsResolution(hostOps)
|
||||
blanket, err := config.ParseAddonHostBlanket(r.AddonHostBlanket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
diagnosticOpts := r.diagnosticsOptions()
|
||||
if err := diagnosticOpts.Validate(); err != nil {
|
||||
@@ -384,6 +391,9 @@ func (r *RunCMD) Run(ctx *cliContext.Context) (result error) {
|
||||
}
|
||||
opts := []config.AppOption{
|
||||
config.WithAddonHostOps(hostOps.Grants),
|
||||
config.WithAddonHostDeny(hostOps.Deny),
|
||||
config.WithAddonHostBlanket(blanket),
|
||||
config.WithAddonHostUIGrantsLocked(!r.AddonHostUIGrants),
|
||||
config.WithProxyAPIKeyEnvLookup(os.Getenv),
|
||||
config.WithContext(runCtx),
|
||||
config.WithArtifactDownloadConcurrency(r.ArtifactDownloadConcurrency),
|
||||
|
||||
@@ -36,7 +36,7 @@ var _ = Describe("addon host policy CLI precedence", Serial, func() {
|
||||
return &command.Run
|
||||
}
|
||||
BeforeEach(func() {
|
||||
for _, key := range []string{"LOCALAI_ADDON_HOST_OPS", "LOCALAI_ADDON_HOST_POLICY"} {
|
||||
for _, key := range []string{"LOCALAI_ADDON_HOST_OPS", "LOCALAI_ADDON_HOST_POLICY", "LOCALAI_ADDON_HOST_DENY"} {
|
||||
old, ok := os.LookupEnv(key)
|
||||
DeferCleanup(func() {
|
||||
if ok {
|
||||
@@ -57,31 +57,66 @@ var _ = Describe("addon host policy CLI precedence", Serial, func() {
|
||||
Expect(got.Source).To(Equal("policy:" + p))
|
||||
Expect(got.Grants).To(Equal(map[string][]string{"custom": {"workspace."}}))
|
||||
})
|
||||
It("uses flag presence even when empty and ignores an invalid policy", func() {
|
||||
writePolicy := func(body string) string {
|
||||
p := filepath.Join(GinkgoT().TempDir(), "policy.json")
|
||||
Expect(os.WriteFile(p, []byte(body), 0600)).To(Succeed())
|
||||
return p
|
||||
}
|
||||
It("uses flag presence even when empty, ignores the file grants and still applies its deny", func() {
|
||||
GinkgoT().Setenv("LOCALAI_ADDON_HOST_OPS", "env=x.")
|
||||
GinkgoT().Setenv("LOCALAI_ADDON_HOST_POLICY", "/missing/policy")
|
||||
GinkgoT().Setenv("LOCALAI_ADDON_HOST_POLICY", writePolicy(`{"schemaVersion":2,"grants":{"file":["z."]},"deny":{"*":["egress."]}}`))
|
||||
got, err := parse("--addon-host-ops=").resolveAddonHostOps()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(got.Grants).To(BeEmpty())
|
||||
Expect(got.Source).To(Equal("flag"))
|
||||
Expect(got.PolicyIgnored).To(BeTrue())
|
||||
Expect(got.GrantsIgnored).To(BeTrue())
|
||||
Expect(got.Deny).To(Equal(map[string][]string{"*": {"egress."}}))
|
||||
})
|
||||
It("uses explicit empty environment instead of policy", func() {
|
||||
It("uses explicit empty environment instead of file grants and still applies its deny", func() {
|
||||
GinkgoT().Setenv("LOCALAI_ADDON_HOST_OPS", "")
|
||||
GinkgoT().Setenv("LOCALAI_ADDON_HOST_POLICY", "/missing/policy")
|
||||
GinkgoT().Setenv("LOCALAI_ADDON_HOST_POLICY", writePolicy(`{"schemaVersion":2,"deny":{"a":["*"]}}`))
|
||||
got, err := parse().resolveAddonHostOps()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(got.Grants).To(BeEmpty())
|
||||
Expect(got.Source).To(Equal("environment"))
|
||||
Expect(got.PolicyIgnored).To(BeTrue())
|
||||
Expect(got.GrantsIgnored).To(BeTrue())
|
||||
Expect(got.Deny).To(Equal(map[string][]string{"a": {"*"}}))
|
||||
})
|
||||
It("does not union a nonempty flag with environment or policy", func() {
|
||||
GinkgoT().Setenv("LOCALAI_ADDON_HOST_OPS", "env=x.")
|
||||
It("stops startup on an invalid selected file even when a flat list replaces its grants", func() {
|
||||
GinkgoT().Setenv("LOCALAI_ADDON_HOST_POLICY", "/missing/policy")
|
||||
_, err := parse("--addon-host-ops=flag=y.").resolveAddonHostOps()
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
It("does not union a nonempty flag with the file grants", func() {
|
||||
GinkgoT().Setenv("LOCALAI_ADDON_HOST_OPS", "env=x.")
|
||||
GinkgoT().Setenv("LOCALAI_ADDON_HOST_POLICY", writePolicy(`{"schemaVersion":1,"grants":{"file":["z."]}}`))
|
||||
got, err := parse("--addon-host-ops=flag=y.").resolveAddonHostOps()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(got.Grants).To(Equal(map[string][]string{"flag": {"y."}}))
|
||||
})
|
||||
It("unions the deny flag with the file deny", func() {
|
||||
got, err := parse("--addon-host-policy="+writePolicy(`{"schemaVersion":2,"deny":{"web":["workspace.admin"]}}`), "--addon-host-deny=web=egress.").resolveAddonHostOps()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(got.Deny).To(Equal(map[string][]string{"web": {"workspace.admin", "egress."}}))
|
||||
})
|
||||
It("reads the deny environment variable", func() {
|
||||
GinkgoT().Setenv("LOCALAI_ADDON_HOST_DENY", "*=connector.")
|
||||
got, err := parse("--addon-host-policy=" + writePolicy(`{"schemaVersion":2,"deny":{"web":["workspace.admin"]}}`)).resolveAddonHostOps()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(got.Deny).To(Equal(map[string][]string{"web": {"workspace.admin"}, "*": {"connector."}}))
|
||||
})
|
||||
It("rejects an invalid deny flag and blanket mode", func() {
|
||||
_, err := parse("--addon-host-deny=nonsense").resolveAddonHostOps()
|
||||
Expect(err).To(HaveOccurred())
|
||||
_, err = config.ParseAddonHostBlanket("sometimes")
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
It("leaves UI grants on and the blanket unset by default", func() {
|
||||
r := parse()
|
||||
Expect(r.AddonHostUIGrants).To(BeTrue())
|
||||
Expect(r.AddonHostBlanket).To(BeEmpty())
|
||||
Expect(parse("--addon-host-ui-grants=false").AddonHostUIGrants).To(BeFalse())
|
||||
})
|
||||
It("lets an empty policy flag disable environment policy without restoring defaults", func() {
|
||||
GinkgoT().Setenv("LOCALAI_ADDON_HOST_POLICY", "/missing/policy")
|
||||
got, err := parse("--addon-host-policy=").resolveAddonHostOps()
|
||||
|
||||
@@ -41,7 +41,13 @@ func (r *RunCMD) resolveAddonHostOps() (config.AddonHostOpsResolution, error) {
|
||||
} else if value, present := os.LookupEnv("LOCALAI_ADDON_HOST_POLICY"); present {
|
||||
policy = &value
|
||||
}
|
||||
resolved, err := config.ResolveAddonHostOps(flag, environment, policy)
|
||||
var deny *string
|
||||
if r.AddonHostDeny != "" {
|
||||
deny = &r.AddonHostDeny
|
||||
}
|
||||
// Kong has already merged the deny environment variable into the field,
|
||||
// so there is no separate environment value to union.
|
||||
resolved, err := config.ResolveAddonHostOps(flag, environment, policy, deny, nil)
|
||||
if err == nil && resolved.Source == "policy" {
|
||||
resolved.Source = "policy:" + *policy
|
||||
}
|
||||
@@ -50,15 +56,19 @@ func (r *RunCMD) resolveAddonHostOps() (config.AddonHostOpsResolution, error) {
|
||||
|
||||
func logAddonHostOpsResolution(resolved config.AddonHostOpsResolution) {
|
||||
if resolved.Source == "empty" {
|
||||
xlog.Warn("Addons receive no host operations: select --addon-host-policy or explicitly configure --addon-host-ops")
|
||||
xlog.Warn("Addons receive no host operations from the policy file or flags: an administrator can grant them in the UI at install, or select --addon-host-policy or --addon-host-ops")
|
||||
}
|
||||
prefixes := 0
|
||||
for _, grants := range resolved.Grants {
|
||||
prefixes += len(grants)
|
||||
}
|
||||
xlog.Info("Addon host operation policy resolved", "source", resolved.Source, "addons", len(resolved.Grants), "prefixes", prefixes)
|
||||
if resolved.PolicyIgnored {
|
||||
xlog.Warn("Addon host policy ignored: explicit host operations override takes precedence")
|
||||
denied := 0
|
||||
for _, entries := range resolved.Deny {
|
||||
denied += len(entries)
|
||||
}
|
||||
xlog.Info("Addon manifests and libraries cannot grant host operations; policy changes require restart")
|
||||
xlog.Info("Addon host operation policy resolved", "source", resolved.Source, "addons", len(resolved.Grants), "prefixes", prefixes, "denyRules", denied)
|
||||
if resolved.GrantsIgnored {
|
||||
xlog.Warn("Addon host policy grants ignored, deny applied: an explicit host operations list replaces the file grants")
|
||||
}
|
||||
xlog.Info("Addon manifests and libraries cannot grant host operations; file and flag policy changes require restart")
|
||||
}
|
||||
Reference in new issue
Block a user