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 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 }