mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
With authentication off every caller that reaches the port is the same local owner, and the checks that tell a person from a script are headers the caller chooses. Any process or agent that could reach the port could grant host operations, set deny rules and turn the blanket approval on. UI host grants are now locked by default in that mode. The policy file and flags still grant, and every write route answers 403 locked_by_deployment with a sentence that says why. The overview and the preview carry the lock and its reason for the page. The new flag --addon-host-ui-grants-without-auth turns the UI grants on knowingly and logs a startup warning. With sign-in on nothing changes. Assisted-by: Claude:claude-sonnet-5-5 go gofmt
147 lines
5.0 KiB
Go
147 lines
5.0 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) }
|
|
}
|
|
|
|
// WithAddonHostOpsSource records where the startup grants came from, for the
|
|
// startup log. A path after a colon is dropped.
|
|
func WithAddonHostOpsSource(source string) AppOption {
|
|
kind, _, _ := strings.Cut(source, ":")
|
|
return func(o *ApplicationConfig) { o.AddonHostOpsSource = kind }
|
|
}
|
|
|
|
// WithAddonHostBlanket sets the deployment blanket mode ("" leaves it to the UI).
|
|
func WithAddonHostBlanket(mode string) AppOption {
|
|
return func(o *ApplicationConfig) { o.AddonHostBlanket = mode }
|
|
}
|
|
|
|
// WithAddonHostUIGrantsWithoutAuth lets the UI change host grants although
|
|
// authentication is off.
|
|
func WithAddonHostUIGrantsWithoutAuth(on bool) AppOption {
|
|
return func(o *ApplicationConfig) { o.AddonHostUIGrantsWithoutAuth = on }
|
|
}
|
|
|
|
// WithAddonHostUIGrantsLocked makes the UI grant store read-only.
|
|
func WithAddonHostUIGrantsLocked(locked bool) AppOption {
|
|
return func(o *ApplicationConfig) { o.AddonHostUIGrantsLocked = locked }
|
|
}
|