92 lines
3.0 KiB
Go
92 lines
3.0 KiB
Go
package user
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// SaveOwnerProfile writes p to path as YAML, atomically (temp file + rename) with
|
|
// mode 0600 and lenient parent-directory creation, matching the runtime config
|
|
// convention (SaveChannels). The whole struct is marshaled, so curated fields
|
|
// (notes, identityFile, displayName, ...) survive a round-trip. YAML comments in
|
|
// the existing file are not part of the struct and are dropped by the encode,
|
|
// consistent with the project's other config writers.
|
|
func SaveOwnerProfile(path string, p *OwnerProfile) error {
|
|
if p == nil {
|
|
return fmt.Errorf("user: save owner profile: empty profile")
|
|
}
|
|
if strings.TrimSpace(path) == "" {
|
|
return fmt.Errorf("user: save owner profile: empty path")
|
|
}
|
|
data, err := yaml.Marshal(p)
|
|
if err != nil {
|
|
return fmt.Errorf("user: encode %s: %w", path, err)
|
|
}
|
|
dir := filepath.Dir(path)
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return fmt.Errorf("user: create %s: %w", dir, err)
|
|
}
|
|
tmp, err := os.CreateTemp(dir, ".owner-*.yaml")
|
|
if err != nil {
|
|
return fmt.Errorf("user: temp file in %s: %w", dir, err)
|
|
}
|
|
tmpName := tmp.Name()
|
|
closed := false
|
|
defer func() {
|
|
_ = tmp.Close()
|
|
if !closed {
|
|
_ = os.Remove(tmpName)
|
|
}
|
|
}()
|
|
// CreateTemp opens with 0600; chmod explicitly so the on-disk file never
|
|
// carries a broader mode even if the umask changes.
|
|
if err := tmp.Chmod(0o600); err != nil {
|
|
return fmt.Errorf("user: chmod %s: %w", tmpName, err)
|
|
}
|
|
if _, err := tmp.Write(data); err != nil {
|
|
return fmt.Errorf("user: write %s: %w", tmpName, err)
|
|
}
|
|
if err := tmp.Sync(); err != nil {
|
|
return fmt.Errorf("user: sync %s: %w", tmpName, err)
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
return fmt.Errorf("user: close %s: %w", tmpName, err)
|
|
}
|
|
closed = true
|
|
if err := os.Rename(tmpName, path); err != nil {
|
|
return fmt.Errorf("user: rename to %s: %w", path, err)
|
|
}
|
|
// Best-effort dir fsync so the rename is durable across a crash.
|
|
if d, derr := os.Open(dir); derr == nil {
|
|
_ = d.Sync()
|
|
_ = d.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RegisterOwnerAccount loads the owner whitelist at path, appends (platform,
|
|
// accountID) when it is not already present (idempotent), and writes the updated
|
|
// profile back. It returns an error when the profile cannot be loaded or saved.
|
|
// The on-disk replace is atomic, so a concurrent reader never sees a partial
|
|
// file. Hot-reload of a running owner profile is the caller's concern (see
|
|
// cmd/cyrene's ownerRegistrar), which applies RegisterAccount to the live profile
|
|
// the channel bridge already holds.
|
|
func RegisterOwnerAccount(path, platform, accountID string) error {
|
|
if strings.TrimSpace(platform) == "" {
|
|
return fmt.Errorf("user: register owner account: empty platform")
|
|
}
|
|
if strings.TrimSpace(accountID) == "" {
|
|
return fmt.Errorf("user: register owner account: empty account id")
|
|
}
|
|
p, err := LoadOwnerProfile(path)
|
|
if err != nil {
|
|
return fmt.Errorf("user: load %s: %w", path, err)
|
|
}
|
|
p.RegisterAccount(platform, accountID)
|
|
return SaveOwnerProfile(path, p)
|
|
}
|