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.
102 lines
2.7 KiB
Go
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
|
|
}
|