package user import ( "errors" "fmt" "os" "strings" "gopkg.in/yaml.v3" ) // ErrRequiredField is wrapped by validation errors so callers can detect an // owner profile that is missing required fields. var ErrRequiredField = errors.New("user: required field missing") // LoadOwnerProfile reads and validates the owner profile YAML at path. func LoadOwnerProfile(path string) (*OwnerProfile, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("user: read %s: %w", path, err) } p := &OwnerProfile{} if err := yaml.Unmarshal(data, p); err != nil { return nil, fmt.Errorf("user: parse %s: %w", path, err) } if err := p.validate(); err != nil { return nil, err } return p, nil } // Validate 用与 LoadOwnerProfile 完全相同的一套规则校验一份**内存中**的档案, // 使管理面板的写路径能在落盘前就拒掉坏档案,而不是先写进磁盘、等下次启动才 // 报错。它只校验必填字段与半空的 accounts 项,不校验平台名是否已知 // (适配器可以后装)。 func (p *OwnerProfile) Validate() error { if p == nil { return fmt.Errorf("%w: empty profile", ErrRequiredField) } return p.validate() } // validate checks the owner profile for its required fields and for partial // whitelist entries. An empty whitelist is allowed: on a fresh install nobody // has been configured as owner, so the policy engine defaults to deny until the // operator fills in account ids (fail-safe, per design/11). func (p *OwnerProfile) validate() error { var missing []string if strings.TrimSpace(p.ID) == "" { missing = append(missing, "id") } if strings.TrimSpace(p.DisplayName) == "" { missing = append(missing, "displayName") } if len(missing) > 0 { return fmt.Errorf("%w: missing %s", ErrRequiredField, strings.Join(missing, ", ")) } for i, a := range p.Accounts { pe := strings.TrimSpace(a.Platform) == "" ae := strings.TrimSpace(a.AccountID) == "" switch { case pe && ae: // Fully-empty entry: an unconfigured placeholder, skip it. continue case pe || ae: return fmt.Errorf("%w: accounts[%d] requires both platform and accountId", ErrRequiredField, i) } } return nil }