mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-29 02:51:29 +02:00
Introduce a namespaced preference store owned by the profile manager, persisted next to the profile config as <id>.prefs.json and deleted with the profile. Sections are opaque JSON, so the profile manager stays free of any feature schema, and writes reuse the state file's atomic path. Migrate the Android SSH known-hosts store and session list onto it. Both previously lived outside the profile lifecycle: known hosts in a per-profile file under filesDir, the session list in Java SharedPreferences, each needing its own sweep against the live profile list to avoid outliving the profile they belonged to. A profile ID that got reused would have inherited the trusted keys of a deleted profile. Both now share the "ssh" and "ssh-sessions" namespaces of the profile's preferences, so deleting a profile takes them along and the Java-side pruning is gone. The known-hosts entries keep the OpenSSH line format, only the container changed, and host key verification keeps rejecting a changed key outright. SetKnownHostsPath becomes SetKnownHostsStore, taking the config dir and profile ID instead of a file path. Existing known-hosts files are not migrated: hosts trusted before this change prompt for confirmation once more, which errs towards safety.
131 lines
3.2 KiB
Go
131 lines
3.2 KiB
Go
package profilemanager
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
|
|
"github.com/netbirdio/netbird/util"
|
|
)
|
|
|
|
const prefsFileSuffix = ".prefs.json"
|
|
|
|
var prefsMu sync.Mutex
|
|
|
|
// Prefs is a namespaced per-profile preference store backed by a single JSON
|
|
// file next to the profile config; it is deleted together with the profile.
|
|
type Prefs struct {
|
|
path string
|
|
}
|
|
|
|
// ProfilePrefs returns the preference store of the profile identified by id.
|
|
func (s *ServiceManager) ProfilePrefs(id ID, username string) (*Prefs, error) {
|
|
if !IsValidProfileFilenameStem(id) {
|
|
return nil, fmt.Errorf("invalid profile ID: %q", id)
|
|
}
|
|
if id == defaultProfileName {
|
|
return &Prefs{path: filepath.Join(filepath.Dir(DefaultConfigPath), id.String()+prefsFileSuffix)}, nil
|
|
}
|
|
configDir, err := s.getConfigDir(username)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get config directory for user %s: %w", username, err)
|
|
}
|
|
return &Prefs{path: filepath.Join(configDir, id.String()+prefsFileSuffix)}, nil
|
|
}
|
|
|
|
// Get unmarshals the namespace section into v and reports whether it exists.
|
|
func (p *Prefs) Get(namespace string, v any) (bool, error) {
|
|
if namespace == "" {
|
|
return false, fmt.Errorf("empty prefs namespace")
|
|
}
|
|
|
|
prefsMu.Lock()
|
|
defer prefsMu.Unlock()
|
|
|
|
sections, err := readPrefsFile(p.path)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
raw, ok := sections[namespace]
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
if err := json.Unmarshal(raw, v); err != nil {
|
|
return false, fmt.Errorf("decode prefs namespace %q: %w", namespace, err)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// Put stores v as the namespace section, replacing any previous value.
|
|
func (p *Prefs) Put(namespace string, v any) error {
|
|
if namespace == "" {
|
|
return fmt.Errorf("empty prefs namespace")
|
|
}
|
|
raw, err := json.Marshal(v)
|
|
if err != nil {
|
|
return fmt.Errorf("encode prefs namespace %q: %w", namespace, err)
|
|
}
|
|
|
|
prefsMu.Lock()
|
|
defer prefsMu.Unlock()
|
|
|
|
sections, err := readPrefsFile(p.path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sections[namespace] = raw
|
|
return writePrefsFile(p.path, sections)
|
|
}
|
|
|
|
// Remove deletes the namespace section; a missing one is not an error.
|
|
func (p *Prefs) Remove(namespace string) error {
|
|
if namespace == "" {
|
|
return fmt.Errorf("empty prefs namespace")
|
|
}
|
|
|
|
prefsMu.Lock()
|
|
defer prefsMu.Unlock()
|
|
|
|
sections, err := readPrefsFile(p.path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, ok := sections[namespace]; !ok {
|
|
return nil
|
|
}
|
|
delete(sections, namespace)
|
|
return writePrefsFile(p.path, sections)
|
|
}
|
|
|
|
func removePrefsFile(path string) error {
|
|
prefsMu.Lock()
|
|
defer prefsMu.Unlock()
|
|
return os.Remove(path)
|
|
}
|
|
|
|
func readPrefsFile(path string) (map[string]json.RawMessage, error) {
|
|
data, err := os.ReadFile(path)
|
|
if os.IsNotExist(err) {
|
|
return map[string]json.RawMessage{}, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read prefs: %w", err)
|
|
}
|
|
|
|
sections := map[string]json.RawMessage{}
|
|
if err := json.Unmarshal(data, §ions); err != nil {
|
|
return nil, fmt.Errorf("decode prefs: %w", err)
|
|
}
|
|
return sections, nil
|
|
}
|
|
|
|
func writePrefsFile(path string, sections map[string]json.RawMessage) error {
|
|
if err := util.WriteJsonWithRestrictedPermission(context.Background(), path, sections); err != nil {
|
|
return fmt.Errorf("write prefs: %w", err)
|
|
}
|
|
return nil
|
|
}
|