package config import ( "bufio" "bytes" "errors" "fmt" "os" "regexp" "strings" "unicode" ) // 超级管理员启动凭据(R1):进程环境优先,未设置的键从受保护的 dotenv 文件补齐。 const ( EnvSuperAdminOwner = "SUPER_ADMIN_OWNER" EnvSuperAdminPassword = "SUPER_ADMIN_PASSWORD" EnvFileKey = "AGENT_SYSTEM_ENV_FILE" ) // ErrSuperAdminConfig 标记缺失、不完整、非法或不可读的超级管理员配置; // 错误文本只含键名与文件路径,绝不含取值。 var ErrSuperAdminConfig = errors.New("super admin config") // SuperAdmin 持有启动期超级管理员身份与口令。口令只在进程内存中, // 格式化输出与 JSON 序列化一律脱敏。 type SuperAdmin struct { Owner string password string } // NewSuperAdmin 供测试与组合根直接构造(不做校验,校验见 Validate)。 func NewSuperAdmin(owner, password string) SuperAdmin { return SuperAdmin{Owner: owner, password: password} } // Password 返回配置口令;调用方不得记录或回传。 func (s SuperAdmin) Password() string { return s.password } func (s SuperAdmin) String() string { if s.password == "" { return fmt.Sprintf("SuperAdmin{owner=%q password=}", s.Owner) } return fmt.Sprintf("SuperAdmin{owner=%q password=}", s.Owner) } func (s SuperAdmin) GoString() string { return s.String() } func (s SuperAdmin) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf(`{"owner":%q,"passwordConfigured":%t}`, s.Owner, s.password != "")), nil } // Validate 检查身份与口令的结构有效性;passwordPolicy 为认证边界的口令策略 // (由调用方注入,避免在配置层复制策略)。 func (s SuperAdmin) Validate(passwordPolicy func(string) error) error { switch { case s.Owner == "" && s.password == "": return fmt.Errorf("%w: %s and %s are required when authentication is enabled", ErrSuperAdminConfig, EnvSuperAdminOwner, EnvSuperAdminPassword) case s.Owner == "": return fmt.Errorf("%w: %s is missing (partial configuration)", ErrSuperAdminConfig, EnvSuperAdminOwner) case s.password == "": return fmt.Errorf("%w: %s is missing (partial configuration)", ErrSuperAdminConfig, EnvSuperAdminPassword) } if strings.TrimSpace(s.Owner) != s.Owner { return fmt.Errorf("%w: %s must not have surrounding whitespace", ErrSuperAdminConfig, EnvSuperAdminOwner) } for _, r := range s.Owner { if unicode.IsControl(r) { return fmt.Errorf("%w: %s contains control characters", ErrSuperAdminConfig, EnvSuperAdminOwner) } } if passwordPolicy != nil { if err := passwordPolicy(s.password); err != nil { return fmt.Errorf("%w: %s violates password policy: %v", ErrSuperAdminConfig, EnvSuperAdminPassword, err) } } return nil } // LoadSuperAdmin 读取超级管理员凭据:envLookup 中非空的键优先;未设置或空的键从 // envFile(为空时取 AGENT_SYSTEM_ENV_FILE)补齐。文件必须存在且不得对组/其他 // 用户开放任何权限。未开认证时不读取也不要求(嵌入式单用户档不暴露账号管理)。 func LoadSuperAdmin(envLookup func(string) (string, bool), envFile string, authEnabled bool) (SuperAdmin, error) { if !authEnabled { return SuperAdmin{}, nil } owner, _ := envLookup(EnvSuperAdminOwner) password, _ := envLookup(EnvSuperAdminPassword) ownerSet, passwordSet := owner != "", password != "" if envFile == "" { envFile, _ = envLookup(EnvFileKey) } if (!ownerSet || !passwordSet) && envFile != "" { values, err := ReadEnvFile(envFile) if err != nil { return SuperAdmin{}, err } if !ownerSet { owner = values[EnvSuperAdminOwner] } if !passwordSet { password = values[EnvSuperAdminPassword] } } return SuperAdmin{Owner: owner, password: password}, nil } var envFileLine = regexp.MustCompile(`^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$`) // ReadEnvFile 按 svc.sh 使用的 dotenv 格式安全解析: // KEY=VALUE、# 注释、export 前缀、剥成对引号;非法行忽略;不执行内容。 // 文件对组/其他用户有任何权限位即拒绝(受保护 .env 约定)。 func ReadEnvFile(path string) (map[string]string, error) { info, err := os.Stat(path) if err != nil { return nil, fmt.Errorf("%w: env file: %v", ErrSuperAdminConfig, err) } if !info.Mode().IsRegular() { return nil, fmt.Errorf("%w: env file %s is not a regular file", ErrSuperAdminConfig, path) } if perm := info.Mode().Perm(); perm&0o077 != 0 { return nil, fmt.Errorf("%w: env file %s is accessible by group/other (mode %04o; chmod 600)", ErrSuperAdminConfig, path, perm) } raw, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("%w: env file: %v", ErrSuperAdminConfig, err) } values := map[string]string{} scanner := bufio.NewScanner(bytes.NewReader(raw)) scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) for scanner.Scan() { line := strings.TrimSuffix(scanner.Text(), "\r") if line == "" || strings.HasPrefix(line, "#") { continue } m := envFileLine.FindStringSubmatch(line) if m == nil { continue } values[m[1]] = unquoteEnvValue(strings.TrimLeft(m[2], " \t")) } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("%w: env file %s: unreadable line", ErrSuperAdminConfig, path) } return values, nil } func unquoteEnvValue(v string) string { if len(v) >= 2 && (v[0] == '"' || v[0] == '\'') && v[len(v)-1] == v[0] { return v[1 : len(v)-1] } return v }