Add nasctl: Go NAS control plane with React frontend

This commit is contained in:
2026-07-05 17:37:19 -04:00
parent 359fd5a160
commit 4f0754ecc5
56 changed files with 6725 additions and 1 deletions
+138
View File
@@ -0,0 +1,138 @@
package system
import (
"context"
"fmt"
"os/exec"
"path/filepath"
"regexp"
"strings"
)
var (
usernamePattern = regexp.MustCompile(`^[a-z_][a-z0-9_-]*[$]?$`)
shareNamePattern = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
nfsOptionPattern = regexp.MustCompile(`^[a-z_]+$`)
nfsClientPattern = regexp.MustCompile(`^[a-zA-Z0-9_.:/\-\*]+$`)
allowedNFSOptions = map[string]bool{
"rw": true, "ro": true, "sync": true, "async": true,
"root_squash": true, "no_root_squash": true, "all_squash": true,
"no_all_squash": true, "subtree_check": true, "no_subtree_check": true,
"secure": true, "insecure": true, "wdelay": true, "no_wdelay": true,
"hide": true, "nohide": true, "crossmnt": true, "fsid": true,
}
)
// Run executes a command with explicit arguments (never via shell).
func Run(ctx context.Context, name string, args ...string) (stdout, stderr string, err error) {
cmd := exec.CommandContext(ctx, name, args...)
var outBuf, errBuf strings.Builder
cmd.Stdout = &outBuf
cmd.Stderr = &errBuf
err = cmd.Run()
return outBuf.String(), errBuf.String(), err
}
// RunWithInput executes a command feeding stdin from the provided string.
// Used for secrets (e.g. passwords) so they never appear in the argument list.
func RunWithInput(ctx context.Context, stdin, name string, args ...string) (stdout, stderr string, err error) {
cmd := exec.CommandContext(ctx, name, args...)
cmd.Stdin = strings.NewReader(stdin)
var outBuf, errBuf strings.Builder
cmd.Stdout = &outBuf
cmd.Stderr = &errBuf
err = cmd.Run()
return outBuf.String(), errBuf.String(), err
}
// UserExists reports whether a system user is present (via `id -u`).
func UserExists(ctx context.Context, username string) bool {
_, _, err := Run(ctx, "id", "-u", username)
return err == nil
}
// ValidateUsername checks Linux username constraints.
func ValidateUsername(username string) error {
if username == "" || len(username) > 32 {
return fmt.Errorf("invalid username length")
}
if !usernamePattern.MatchString(username) {
return fmt.Errorf("invalid username: %q", username)
}
return nil
}
// ValidatePath ensures path is absolute and does not contain traversal.
func ValidatePath(path string) error {
if path == "" {
return fmt.Errorf("path is required")
}
if !filepath.IsAbs(path) {
return fmt.Errorf("path must be absolute: %q", path)
}
clean := filepath.Clean(path)
if strings.Contains(clean, "..") {
return fmt.Errorf("path must not contain .. segments: %q", path)
}
return nil
}
// ValidateShareName checks Samba share name constraints.
func ValidateShareName(name string) error {
if name == "" || len(name) > 80 {
return fmt.Errorf("invalid share name length")
}
if !shareNamePattern.MatchString(name) {
return fmt.Errorf("invalid share name: %q", name)
}
return nil
}
// ValidatePathAllowed validates a path and, if allowedRoots is non-empty,
// ensures the path is contained within one of the configured roots.
func ValidatePathAllowed(path string, allowedRoots []string) error {
if err := ValidatePath(path); err != nil {
return err
}
if len(allowedRoots) == 0 {
return nil
}
clean := filepath.Clean(path)
for _, root := range allowedRoots {
root = filepath.Clean(root)
if clean == root || strings.HasPrefix(clean, root+string(filepath.Separator)) {
return nil
}
}
return fmt.Errorf("path %q is not within an allowed directory", path)
}
// ValidateNFSClient checks an NFS client/network specifier.
func ValidateNFSClient(client string) error {
if client == "" || len(client) > 255 {
return fmt.Errorf("invalid nfs client length")
}
if !nfsClientPattern.MatchString(client) {
return fmt.Errorf("invalid nfs client: %q", client)
}
return nil
}
// ValidateNFSOptions checks a comma-separated list of export options against
// a whitelist, rejecting anything that could break the exports file.
func ValidateNFSOptions(options string) error {
for _, opt := range strings.Split(options, ",") {
opt = strings.TrimSpace(opt)
if opt == "" {
continue
}
key := opt
if idx := strings.IndexByte(opt, '='); idx >= 0 {
key = opt[:idx]
}
if !nfsOptionPattern.MatchString(key) || !allowedNFSOptions[key] {
return fmt.Errorf("invalid nfs option: %q", opt)
}
}
return nil
}