|
|
@@ -9,9 +9,13 @@ import (
|
|
|
"io"
|
|
|
"os"
|
|
|
"os/exec"
|
|
|
+ "os/user"
|
|
|
"path/filepath"
|
|
|
+ "strconv"
|
|
|
+ "strings"
|
|
|
"sync"
|
|
|
"sync/atomic"
|
|
|
+ "syscall"
|
|
|
|
|
|
"fwh-toolbox-server/utils/filestore"
|
|
|
|
|
|
@@ -247,6 +251,121 @@ type ClientConfig struct {
|
|
|
Args []string // 命令参数(如 ["acp"])
|
|
|
WorkingDir string // 工作目录
|
|
|
Env []string // 额外环境变量(如 "HOME=/home/user")
|
|
|
+ RunAsUser string // 降权运行的目标系统用户名,为空则不降权
|
|
|
+}
|
|
|
+
|
|
|
+// ApplyRunAsUser 让 cmd 以指定系统用户身份运行(降权)。
|
|
|
+// runAsUser 为空时不做任何处理(沿用当前进程用户)。
|
|
|
+// 该函数会:
|
|
|
+// 1. 解析目标用户的 uid/gid,并设置 cmd.SysProcAttr.Credential;
|
|
|
+// 2. 附带解析该用户的补充组(supplementary groups);
|
|
|
+// 3. 修正 HOME/USER/LOGNAME 环境变量为目标用户的值,避免 kiro-cli
|
|
|
+// 仍读取 root 的家目录导致配置/凭证路径错误。
|
|
|
+//
|
|
|
+// 注意:设置 Credential 需要当前进程具备相应权限(通常为 root)。
|
|
|
+func ApplyRunAsUser(cmd *exec.Cmd, runAsUser string) error {
|
|
|
+ runAsUser = strings.TrimSpace(runAsUser)
|
|
|
+ if runAsUser == "" {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ u, err := user.Lookup(runAsUser)
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("查找降权用户 %q 失败: %w", runAsUser, err)
|
|
|
+ }
|
|
|
+
|
|
|
+ uid, err := strconv.ParseUint(u.Uid, 10, 32)
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("解析用户 %q 的 uid(%s) 失败: %w", runAsUser, u.Uid, err)
|
|
|
+ }
|
|
|
+ gid, err := strconv.ParseUint(u.Gid, 10, 32)
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("解析用户 %q 的 gid(%s) 失败: %w", runAsUser, u.Gid, err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析补充组(失败不致命,仅记录告警)
|
|
|
+ var groups []uint32
|
|
|
+ if gidStrs, gErr := u.GroupIds(); gErr == nil {
|
|
|
+ for _, gs := range gidStrs {
|
|
|
+ if g, pErr := strconv.ParseUint(gs, 10, 32); pErr == nil {
|
|
|
+ groups = append(groups, uint32(g))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ zap.L().Warn("[ACP] 读取降权用户补充组失败", zap.String("user", runAsUser), zap.Error(gErr))
|
|
|
+ }
|
|
|
+
|
|
|
+ if cmd.SysProcAttr == nil {
|
|
|
+ cmd.SysProcAttr = &syscall.SysProcAttr{}
|
|
|
+ }
|
|
|
+ cmd.SysProcAttr.Credential = &syscall.Credential{
|
|
|
+ Uid: uint32(uid),
|
|
|
+ Gid: uint32(gid),
|
|
|
+ Groups: groups,
|
|
|
+ }
|
|
|
+
|
|
|
+ // 修正环境变量,确保子进程使用目标用户的身份。
|
|
|
+ // USER/LOGNAME 强制设为目标用户;HOME 尊重上层(配置)已传入的值,
|
|
|
+ // 仅在未提供时才回退到用户家目录。
|
|
|
+ if cmd.Env == nil {
|
|
|
+ cmd.Env = os.Environ()
|
|
|
+ }
|
|
|
+ cmd.Env = overrideEnv(cmd.Env, map[string]string{
|
|
|
+ "USER": u.Username,
|
|
|
+ "LOGNAME": u.Username,
|
|
|
+ })
|
|
|
+ cmd.Env = fillEnvIfAbsent(cmd.Env, "HOME", u.HomeDir)
|
|
|
+
|
|
|
+ effectiveHome := u.HomeDir
|
|
|
+ for _, e := range cmd.Env {
|
|
|
+ if strings.HasPrefix(e, "HOME=") {
|
|
|
+ effectiveHome = strings.TrimPrefix(e, "HOME=")
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ zap.L().Info("[ACP] 子进程降权运行",
|
|
|
+ zap.String("user", u.Username),
|
|
|
+ zap.String("uid", u.Uid),
|
|
|
+ zap.String("gid", u.Gid),
|
|
|
+ zap.String("home", effectiveHome),
|
|
|
+ )
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// fillEnvIfAbsent 仅当 env 中不存在 key 时才追加 key=value,否则原样返回。
|
|
|
+func fillEnvIfAbsent(env []string, key, value string) []string {
|
|
|
+ prefix := key + "="
|
|
|
+ for _, e := range env {
|
|
|
+ if strings.HasPrefix(e, prefix) {
|
|
|
+ return env
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return append(env, prefix+value)
|
|
|
+}
|
|
|
+
|
|
|
+// overrideEnv 用 kv 覆盖 env 中已存在的同名变量,不存在则追加。
|
|
|
+func overrideEnv(env []string, kv map[string]string) []string {
|
|
|
+ out := make([]string, 0, len(env)+len(kv))
|
|
|
+ seen := make(map[string]bool, len(kv))
|
|
|
+ for _, e := range env {
|
|
|
+ key := e
|
|
|
+ if idx := strings.IndexByte(e, '='); idx >= 0 {
|
|
|
+ key = e[:idx]
|
|
|
+ }
|
|
|
+ if v, ok := kv[key]; ok {
|
|
|
+ out = append(out, key+"="+v)
|
|
|
+ seen[key] = true
|
|
|
+ } else {
|
|
|
+ out = append(out, e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for k, v := range kv {
|
|
|
+ if !seen[k] {
|
|
|
+ out = append(out, k+"="+v)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return out
|
|
|
}
|
|
|
|
|
|
// NewClient 创建并启动 ACP Client
|
|
|
@@ -263,6 +382,11 @@ func NewClient(cfg ClientConfig) (*Client, error) {
|
|
|
// 设置环境变量
|
|
|
cmd.Env = append(os.Environ(), cfg.Env...)
|
|
|
|
|
|
+ // 若配置了降权用户,则以该用户身份拉起子进程
|
|
|
+ if err := ApplyRunAsUser(cmd, cfg.RunAsUser); err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
stdin, err := cmd.StdinPipe()
|
|
|
if err != nil {
|
|
|
return nil, fmt.Errorf("创建 stdin pipe 失败: %w", err)
|