mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-07 04:53:24 -05:00
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
134 lines
4.5 KiB
Go
134 lines
4.5 KiB
Go
// SPDX-License-Identifier: MIT
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
addonIDForHostOps = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
|
|
hostOpPrefix = regexp.MustCompile(`^[a-z]+\.([a-z]+\.)*$`)
|
|
hostOpExact = regexp.MustCompile(`^[a-z]+(\.[a-z]+)+$`)
|
|
)
|
|
|
|
// ParseAddonHostOps reads a comma list of addon-id=op-prefix pairs. An add-on
|
|
// may appear more than once. An empty string names nobody.
|
|
func ParseAddonHostOps(s string) (map[string][]string, error) {
|
|
out := map[string][]string{}
|
|
for _, item := range strings.Split(s, ",") {
|
|
item = strings.TrimSpace(item)
|
|
if item == "" {
|
|
continue
|
|
}
|
|
id, prefix, ok := strings.Cut(item, "=")
|
|
id, prefix = strings.TrimSpace(id), strings.TrimSpace(prefix)
|
|
if !ok || !addonIDForHostOps.MatchString(id) || !hostOpPrefix.MatchString(prefix) {
|
|
return nil, fmt.Errorf("invalid host operation entry %q: want addon-id=op-prefix, such as custom-addon=workspace.", item)
|
|
}
|
|
out[id] = append(out[id], prefix)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// WithAddonHostOps sets which add-ons may call which host operations.
|
|
// Nil and empty maps grant nothing. The run command resolves explicit host
|
|
// policy before passing its startup snapshot here.
|
|
func WithAddonHostOps(ops map[string][]string) AppOption {
|
|
snapshot := CloneAddonHostOps(ops)
|
|
return func(o *ApplicationConfig) { o.AddonHostOps = CloneAddonHostOps(snapshot) }
|
|
}
|
|
|
|
// validDenyEntry accepts what a deny rule may name: a prefix (workspace.),
|
|
// one exact operation (workspace.admin) or the single token "*".
|
|
func validDenyEntry(s string) bool {
|
|
return s == "*" || hostOpPrefix.MatchString(s) || hostOpExact.MatchString(s)
|
|
}
|
|
|
|
// ParseAddonHostDeny reads a comma list of addon-id=entry pairs, where addon-id
|
|
// may be "*" for every add-on and entry is a prefix, an exact operation name
|
|
// or "*". Duplicates are errors; "*" must be the only entry of its add-on.
|
|
func ParseAddonHostDeny(s string) (map[string][]string, error) {
|
|
out := map[string][]string{}
|
|
for _, item := range strings.Split(s, ",") {
|
|
item = strings.TrimSpace(item)
|
|
if item == "" {
|
|
continue
|
|
}
|
|
id, entry, ok := strings.Cut(item, "=")
|
|
id, entry = strings.TrimSpace(id), strings.TrimSpace(entry)
|
|
if !ok || !(id == "*" || addonIDForHostOps.MatchString(id)) || !validDenyEntry(entry) {
|
|
return nil, fmt.Errorf("invalid host deny entry %q: want addon-id=op-prefix or *=op-prefix, such as *=egress. or custom-addon=*", item)
|
|
}
|
|
for _, existing := range out[id] {
|
|
if existing == entry {
|
|
return nil, fmt.Errorf("duplicate host deny entry %q", item)
|
|
}
|
|
}
|
|
out[id] = append(out[id], entry)
|
|
}
|
|
if len(out) > 128 {
|
|
return nil, fmt.Errorf("host deny exceeds 128 addons")
|
|
}
|
|
for id, entries := range out {
|
|
if len(entries) > 32 {
|
|
return nil, fmt.Errorf("host deny exceeds 32 entries for %q", id)
|
|
}
|
|
for _, e := range entries {
|
|
if e == "*" && len(entries) != 1 {
|
|
return nil, fmt.Errorf(`host deny "*" must be the only entry of %q`, id)
|
|
}
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// MergeAddonHostDeny unions deny maps. Entries stay in first-seen order and a
|
|
// "*" absorbs every other entry of its key. The result is never nil.
|
|
func MergeAddonHostDeny(maps ...map[string][]string) map[string][]string {
|
|
out := map[string][]string{}
|
|
for _, m := range maps {
|
|
for id, entries := range m {
|
|
for _, e := range entries {
|
|
if !slices.Contains(out[id], e) {
|
|
out[id] = append(out[id], e)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for id, entries := range out {
|
|
if slices.Contains(entries, "*") {
|
|
out[id] = []string{"*"}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ParseAddonHostBlanket validates the deployment blanket mode. The empty
|
|
// string means the deployment does not set it and the UI decides.
|
|
func ParseAddonHostBlanket(s string) (string, error) {
|
|
switch s = strings.TrimSpace(s); s {
|
|
case "", "off", "official", "any":
|
|
return s, nil
|
|
}
|
|
return "", fmt.Errorf("invalid --addon-host-blanket %q: want off, official or any", s)
|
|
}
|
|
|
|
// WithAddonHostDeny sets the startup deny rules. The map is copied.
|
|
func WithAddonHostDeny(deny map[string][]string) AppOption {
|
|
snapshot := CloneAddonHostOps(deny)
|
|
return func(o *ApplicationConfig) { o.AddonHostDeny = CloneAddonHostOps(snapshot) }
|
|
}
|
|
|
|
// WithAddonHostBlanket sets the deployment blanket mode ("" leaves it to the UI).
|
|
func WithAddonHostBlanket(mode string) AppOption {
|
|
return func(o *ApplicationConfig) { o.AddonHostBlanket = mode }
|
|
}
|
|
|
|
// WithAddonHostUIGrantsLocked makes the UI grant store read-only.
|
|
func WithAddonHostUIGrantsLocked(locked bool) AppOption {
|
|
return func(o *ApplicationConfig) { o.AddonHostUIGrantsLocked = locked }
|
|
}
|