Files
ClusterCanvas/service/internal/auth/resolve_commands.go
T
Squid 7631591f30 Wire action-group variables, sudo, and command path resolution through the API and UI.
Completes run-if/set_variable and requires_sudo editing, sudoers path resolve helpers, and execute permission checks so node action groups can gate upgrades and elevate safely.
2026-07-19 22:00:14 +02:00

102 lines
2.7 KiB
Go

package auth
import (
"fmt"
"strings"
)
const MaxResolveCommandNames = 32
// ValidateCommandNames checks relative command basenames for resolve-commands.
func ValidateCommandNames(names []string) ([]string, error) {
if len(names) == 0 {
return nil, fmt.Errorf("names is required")
}
if len(names) > MaxResolveCommandNames {
return nil, fmt.Errorf("at most %d names allowed", MaxResolveCommandNames)
}
cleaned := make([]string, 0, len(names))
seen := make(map[string]struct{}, len(names))
for _, raw := range names {
name := strings.TrimSpace(raw)
if name == "" {
return nil, fmt.Errorf("command name must not be empty")
}
if strings.Contains(name, "/") {
return nil, fmt.Errorf("command name must be a basename without /")
}
if len(name) > 64 {
return nil, fmt.Errorf("command name too long")
}
for _, runeValue := range name {
if (runeValue >= 'a' && runeValue <= 'z') ||
(runeValue >= 'A' && runeValue <= 'Z') ||
(runeValue >= '0' && runeValue <= '9') ||
runeValue == '.' || runeValue == '_' || runeValue == '-' || runeValue == '+' {
continue
}
return nil, fmt.Errorf("command name contains invalid characters")
}
if _, exists := seen[name]; exists {
continue
}
seen[name] = struct{}{}
cleaned = append(cleaned, name)
}
if len(cleaned) == 0 {
return nil, fmt.Errorf("names is required")
}
return cleaned, nil
}
// BuildResolveCommandsRemote builds a shell snippet that prints name=path lines.
func BuildResolveCommandsRemote(names []string) string {
var builder strings.Builder
for _, name := range names {
quoted := shellQuote(name)
builder.WriteString("path=$(command -v -- ")
builder.WriteString(quoted)
builder.WriteString(" 2>/dev/null || true); printf '%s=%s\\n' ")
builder.WriteString(quoted)
builder.WriteString(" \"$path\"; ")
}
return builder.String()
}
// ParseResolveCommandsOutput parses name=path lines into found paths and missing names.
func ParseResolveCommandsOutput(names []string, stdout string) (paths map[string]string, missing []string) {
paths = make(map[string]string, len(names))
wanted := make(map[string]struct{}, len(names))
for _, name := range names {
wanted[name] = struct{}{}
}
for _, line := range strings.Split(stdout, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
name, path, ok := strings.Cut(line, "=")
if !ok {
continue
}
name = strings.TrimSpace(name)
path = strings.TrimSpace(path)
if _, ok := wanted[name]; !ok {
continue
}
if path == "" || !strings.HasPrefix(path, "/") {
continue
}
paths[name] = path
}
for _, name := range names {
if _, ok := paths[name]; !ok {
missing = append(missing, name)
}
}
return paths, missing
}