mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-29 01:12:36 -04:00
## Describe your changes Expose RenameProfile in the Android profile manager binding ## Issue ticket number and link ## Stack <!-- branch-stack --> ### Checklist - [ ] Is it a bug fix - [ ] Is a typo/documentation fix - [x] Is a feature enhancement - [ ] It is a refactor - [ ] Created tests that fail without the change (if possible) - [ ] This change does **not** modify the public API, gRPC protocols, functionality behavior, CLI / service flags, or introduce a new feature — **OR** I have discussed it with the NetBird team beforehand (link the issue / Slack thread in the description). See [CONTRIBUTING.md](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#discuss-changes-with-the-netbird-team-first). > By submitting this pull request, you confirm that you have read and agree to the terms of the [Contributor License Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md). ## Documentation Select exactly one: - [ ] I added/updated documentation for this change - [x] Documentation is **not needed** for this change (explain why) ### Docs PR URL (required if "docs added" is checked) Paste the PR link from https://github.com/netbirdio/docs here: https://github.com/netbirdio/docs/pull/__ <!-- codesmith:footer --> --- <a href="https://app.blacksmith.sh/netbirdio/codesmith/netbird/pr/6926"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img alt="View with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a> <a href="https://backend.blacksmith.sh/track/enable-autofix?expires=1787768395&installation_model_id=427504&pr_number=6926&repository=netbirdio%2Fnetbird&return_to=https%3A%2F%2Fgithub.com%2Fnetbirdio%2Fnetbird%2Fpull%2F6926&signature=f4684572fa5b03325c89a2ace1e1eed03df3881365acebdc31066b74fa656bdb"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img alt="Autofix with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a> <sup>Need help on this PR? Tag <code>@codesmith-bot</code> with what you need. Autofix is disabled.</sup> <!-- codesmith:autofix:disabled --> <!-- /codesmith:footer --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added the ability to rename Android profiles. * Rename operations now provide clear success or failure feedback. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
273 lines
9.4 KiB
Go
273 lines
9.4 KiB
Go
//go:build android
|
|
|
|
package android
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
|
)
|
|
|
|
const (
|
|
// Android-specific config filename (different from desktop default.json)
|
|
defaultConfigFilename = "netbird.cfg"
|
|
// Subdirectory for non-default profiles (must match Java Preferences.java)
|
|
profilesSubdir = "profiles"
|
|
// Android uses a single user context per app (non-empty username required by ServiceManager)
|
|
androidUsername = "android"
|
|
)
|
|
|
|
// Profile represents a profile for gomobile
|
|
type Profile struct {
|
|
ID string
|
|
Name string
|
|
IsActive bool
|
|
}
|
|
|
|
// ProfileArray wraps profiles for gomobile compatibility
|
|
type ProfileArray struct {
|
|
items []*Profile
|
|
}
|
|
|
|
// Length returns the number of profiles
|
|
func (p *ProfileArray) Length() int {
|
|
return len(p.items)
|
|
}
|
|
|
|
// Get returns the profile at index i
|
|
func (p *ProfileArray) Get(i int) *Profile {
|
|
if i < 0 || i >= len(p.items) {
|
|
return nil
|
|
}
|
|
return p.items[i]
|
|
}
|
|
|
|
/*
|
|
|
|
/data/data/io.netbird.client/files/ ← configDir parameter
|
|
├── netbird.cfg ← Default profile config
|
|
├── state.json ← Default profile state
|
|
├── active_profile.json ← Active profile tracker (JSON with Name + Username)
|
|
└── profiles/ ← Subdirectory for non-default profiles
|
|
├── work.json ← Legacy work profile config
|
|
├── work.state.json ← Legacy work profile state
|
|
├── 4c5f5c8198c3989cffb5b5394f5a7ae0.json ← ID profile config
|
|
├── 4c5f5c8198c3989cffb5b5394f5a7ae0.state.json ← ID profile state
|
|
*/
|
|
|
|
// ProfileManager manages profiles for Android
|
|
// It wraps the internal profilemanager to provide Android-specific behavior
|
|
type ProfileManager struct {
|
|
configDir string
|
|
serviceMgr *profilemanager.ServiceManager
|
|
}
|
|
|
|
// NewProfileManager creates a new profile manager for Android
|
|
func NewProfileManager(configDir string) *ProfileManager {
|
|
// Set the default config path for Android (stored in root configDir, not profiles/)
|
|
defaultConfigPath := filepath.Join(configDir, defaultConfigFilename)
|
|
|
|
// Set global paths for Android
|
|
profilemanager.DefaultConfigPathDir = configDir
|
|
profilemanager.DefaultConfigPath = defaultConfigPath
|
|
profilemanager.ActiveProfileStatePath = filepath.Join(configDir, "active_profile.json")
|
|
|
|
// Create ServiceManager with profiles/ subdirectory
|
|
// This avoids modifying the global ConfigDirOverride for profile listing
|
|
profilesDir := filepath.Join(configDir, profilesSubdir)
|
|
serviceMgr := profilemanager.NewServiceManagerWithProfilesDir(defaultConfigPath, profilesDir)
|
|
|
|
return &ProfileManager{
|
|
configDir: configDir,
|
|
serviceMgr: serviceMgr,
|
|
}
|
|
}
|
|
|
|
// ListProfiles returns all available profiles
|
|
func (pm *ProfileManager) ListProfiles() (*ProfileArray, error) {
|
|
// Use ServiceManager (looks in profiles/ directory, checks active_profile.json for IsActive)
|
|
internalProfiles, err := pm.serviceMgr.ListProfiles(androidUsername)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list profiles: %w", err)
|
|
}
|
|
|
|
// Convert internal profiles to Android Profile type
|
|
var profiles []*Profile
|
|
for _, p := range internalProfiles {
|
|
profiles = append(profiles, &Profile{
|
|
ID: p.ID.String(),
|
|
Name: p.Name,
|
|
IsActive: p.IsActive,
|
|
})
|
|
}
|
|
|
|
return &ProfileArray{items: profiles}, nil
|
|
}
|
|
|
|
// GetActiveProfile returns the currently active profile name
|
|
func (pm *ProfileManager) GetActiveProfile() (*Profile, error) {
|
|
// Use ServiceManager to stay consistent with ListProfiles
|
|
// ServiceManager uses active_profile.json
|
|
activeState, err := pm.serviceMgr.GetActiveProfileState()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get active profile: %w", err)
|
|
}
|
|
|
|
// ActiveProfileState only stores the ID (and username), not the display
|
|
// name. Resolve the ID to the full profile so callers get the real Name.
|
|
prof, err := pm.serviceMgr.ResolveProfile(activeState.ID.String(), androidUsername)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to resolve active profile %q: %w", activeState.ID, err)
|
|
}
|
|
return &Profile{ID: prof.ID.String(), Name: prof.Name, IsActive: true}, nil
|
|
}
|
|
|
|
// SwitchProfile switches to a different profile
|
|
func (pm *ProfileManager) SwitchProfile(id string) error {
|
|
// Use ServiceManager to stay consistent with ListProfiles
|
|
// ServiceManager uses active_profile.json
|
|
err := pm.serviceMgr.SetActiveProfileState(&profilemanager.ActiveProfileState{
|
|
ID: profilemanager.ID(id),
|
|
Username: androidUsername,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to switch profile: %w", err)
|
|
}
|
|
|
|
log.Infof("switched to profile: %s", id)
|
|
return nil
|
|
}
|
|
|
|
// AddProfile creates a new profile
|
|
func (pm *ProfileManager) AddProfile(profileName string) error {
|
|
// Use ServiceManager (creates profile in profiles/ directory)
|
|
profile, err := pm.serviceMgr.AddProfile(profileName, androidUsername)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add profile: %w", err)
|
|
}
|
|
|
|
log.Infof("created new profile: %s", profile.ID)
|
|
return nil
|
|
}
|
|
|
|
// LogoutProfile logs out from a profile (clears authentication)
|
|
func (pm *ProfileManager) LogoutProfile(id string) error {
|
|
configPath, err := pm.getProfileConfigPath(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !profilemanager.IsValidProfileFilenameStem(profilemanager.ID(id)) {
|
|
return fmt.Errorf("id '%s' is not valid", id)
|
|
}
|
|
|
|
// Check if profile exists
|
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
|
return fmt.Errorf("profile '%s' does not exist", id)
|
|
}
|
|
|
|
// Read current config using internal profilemanager
|
|
config, err := profilemanager.ReadConfig(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read profile config: %w", err)
|
|
}
|
|
|
|
// Clear authentication by removing private key and SSH key
|
|
config.PrivateKey = ""
|
|
config.SSHKey = ""
|
|
|
|
// Save config using internal profilemanager
|
|
if err := profilemanager.WriteOutConfig(configPath, config); err != nil {
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
}
|
|
|
|
log.Infof("logged out from profile: %s", id)
|
|
return nil
|
|
}
|
|
|
|
// RenameProfile changes a profile's display name. The profile ID, and therefore
|
|
// its on-disk filename, is left untouched: only the "name" field of the config
|
|
// is rewritten. This works for the default profile too, whose config lives in
|
|
// netbird.cfg rather than under profiles/.
|
|
func (pm *ProfileManager) RenameProfile(id string, newName string) error {
|
|
if err := pm.serviceMgr.RenameProfile(profilemanager.ID(id), androidUsername, newName); err != nil {
|
|
return fmt.Errorf("failed to rename profile: %w", err)
|
|
}
|
|
|
|
log.Infof("renamed profile %s to: %s", id, newName)
|
|
return nil
|
|
}
|
|
|
|
// RemoveProfile deletes a profile
|
|
func (pm *ProfileManager) RemoveProfile(id string) error {
|
|
// Use ServiceManager (removes profile from profiles/ directory)
|
|
if err := pm.serviceMgr.RemoveProfile(profilemanager.ID(id), androidUsername); err != nil {
|
|
return fmt.Errorf("failed to remove profile: %w", err)
|
|
}
|
|
|
|
log.Infof("removed profile: %s", id)
|
|
return nil
|
|
}
|
|
|
|
// getProfileConfigPath returns the config file path for a profile
|
|
// This is needed for Android-specific path handling (netbird.cfg for default profile)
|
|
func (pm *ProfileManager) getProfileConfigPath(id string) (string, error) {
|
|
if !profilemanager.IsValidProfileFilenameStem(profilemanager.ID(id)) {
|
|
return "", fmt.Errorf("id %q is not valid", id)
|
|
}
|
|
|
|
if id == profilemanager.DefaultProfileName {
|
|
// Android uses netbird.cfg for default profile instead of default.json
|
|
// Default profile is stored in root configDir, not in profiles/
|
|
return filepath.Join(pm.configDir, defaultConfigFilename), nil
|
|
}
|
|
|
|
profilesDir := filepath.Join(pm.configDir, profilesSubdir)
|
|
return filepath.Join(profilesDir, id+".json"), nil
|
|
}
|
|
|
|
// GetConfigPath returns the config file path for a given profile id
|
|
// Java should call this instead of constructing paths with Preferences.configFile()
|
|
func (pm *ProfileManager) GetConfigPath(id string) (string, error) {
|
|
return pm.getProfileConfigPath(id)
|
|
}
|
|
|
|
// GetStateFilePath returns the state file path for a given profile
|
|
// Java should call this instead of constructing paths with Preferences.stateFile()
|
|
func (pm *ProfileManager) GetStateFilePath(id string) (string, error) {
|
|
if id == "" || id == profilemanager.DefaultProfileName {
|
|
return filepath.Join(pm.configDir, "state.json"), nil
|
|
}
|
|
|
|
if !profilemanager.IsValidProfileFilenameStem(profilemanager.ID(id)) {
|
|
return "", fmt.Errorf("id %q is not valid", id)
|
|
}
|
|
|
|
profilesDir := filepath.Join(pm.configDir, profilesSubdir)
|
|
return filepath.Join(profilesDir, id+".state.json"), nil
|
|
}
|
|
|
|
// GetActiveConfigPath returns the config file path for the currently active profile
|
|
// Java should call this instead of Preferences.getActiveProfileName() + Preferences.configFile()
|
|
func (pm *ProfileManager) GetActiveConfigPath() (string, error) {
|
|
activeProfile, err := pm.GetActiveProfile()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get active profile: %w", err)
|
|
}
|
|
return pm.GetConfigPath(activeProfile.ID)
|
|
}
|
|
|
|
// GetActiveStateFilePath returns the state file path for the currently active profile
|
|
// Java should call this instead of Preferences.getActiveProfileName() + Preferences.stateFile()
|
|
func (pm *ProfileManager) GetActiveStateFilePath() (string, error) {
|
|
activeProfile, err := pm.GetActiveProfile()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get active profile: %w", err)
|
|
}
|
|
return pm.GetStateFilePath(activeProfile.ID)
|
|
}
|