Skip to content

Conversation

@pappz
Copy link
Contributor

@pappz pappz commented Dec 1, 2025

Describe your changes

Expose the profile-manager service for Android. Logout was not part of the manager service implementation. In the future, I recommend moving this logic there.

Issue ticket number and link

Stack

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • 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/__

Summary by CodeRabbit

  • New Features

    • Added Android profile management: list, switch, create, remove, logout profiles, and view active profile; exposes config/state path utilities for mobile.
  • Refactor

    • Android client startup now uses platform-aware config/state handling and logs the active config and state files.
    • Service manager can use an alternate profiles directory to support mobile/embedded paths.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 1, 2025

Walkthrough

Platform-specific config/state file handling moved out of the Android Client struct into method parameters; an Android ProfileManager wrapper was added; ServiceManager gained an optional profiles directory override; the engine now uses mobile-provided state paths on iOS/Android while preserving desktop behavior.

Changes

Cohort / File(s) Summary
Client parameter refactor
client/android/client.go
Removed Client.cfgFile and Client.stateFile fields. NewClient signature changed to remove platformFiles. Run and RunWithoutLogin now accept platformFiles, derive local cfgFile/stateFile, log active config/state, and pass stateFile to downstream functions; all references to former struct fields removed.
Android profile manager (new)
client/android/profile_manager.go
Added Android-focused ProfileManager wrapper types (Profile, ProfileArray, ProfileManager) and public methods: ListProfiles, GetActiveProfile, SwitchProfile, AddProfile, LogoutProfile, RemoveProfile, plus config/state path getters and Android-specific pathing/sanitization.
Mobile state path change
client/internal/engine.go
On mobile (iOS/Android) use mobileDep.StateFilePath (creating the file if missing) for state path; non-mobile platforms continue using ServiceManager-derived state path.
ServiceManager override capability
client/internal/profilemanager/service.go
Added profilesDir field and NewServiceManagerWithProfilesDir constructor. Introduced getConfigDir helper and updated Add/Remove/List/GetStatePath to honor an optional profiles directory override.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Check all call sites for updated NewClient, Run, and RunWithoutLogin signatures.
  • Verify ProfileManager methods correctly map to internal profilemanager behavior and path conventions (especially active_profile handling and logout clearing of keys).
  • Confirm ServiceManager profilesDir override doesn't regress non-mobile path resolution.
  • Validate engine mobile path creation error handling and that it preserves previous desktop behavior.

Poem

🐰 I hopped through code with whiskers bright,
Moved configs from burrows into daylight,
A ProfileManager nested snug and small,
Paths now tidy under Android's hall,
I nibble bugs and bounce — rejoice, one and all!

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title '[client] Android profile switch' clearly and concisely describes the main change: exposing profile-manager functionality for Android. It is specific, directly related to the changeset, and avoids vague terms.
Description check ✅ Passed The description covers the main change (exposing profile-manager for Android) and checklist items. However, it lacks an issue ticket reference and leaves the Docs PR URL field partially unfilled despite checking documentation-related items.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch android-profile-switch

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4cee5be and 5b69abb.

📒 Files selected for processing (1)
  • client/android/profile_manager.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • client/android/profile_manager.go
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: Management / Unit (amd64, postgres)
  • GitHub Check: Client / Unit (amd64)
  • GitHub Check: Relay / Unit (amd64, -race)
  • GitHub Check: Management / Unit (amd64, sqlite)
  • GitHub Check: Management / Unit (amd64, mysql)
  • GitHub Check: Client (Docker) / Unit
  • GitHub Check: Client / Unit (386)
  • GitHub Check: Relay / Unit (386)
  • GitHub Check: Darwin
  • GitHub Check: Linux
  • GitHub Check: Windows
  • GitHub Check: Client / Unit
  • GitHub Check: Client / Unit
  • GitHub Check: release_ui_darwin
  • GitHub Check: release
  • GitHub Check: Client / Unit
  • GitHub Check: release_ui
  • GitHub Check: JS / Lint
  • GitHub Check: Android / Build
  • GitHub Check: iOS / Build

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
client/internal/engine.go (1)

2108-2111: fileExists returns true on permission errors.

The current implementation returns true if the error is anything other than os.ErrNotExist. This includes permission errors, which could lead to skipping file creation when the file doesn't actually exist but is inaccessible.

Consider using errors.Is for more precise checking:

 func fileExists(path string) bool {
 	_, err := os.Stat(path)
-	return !os.IsNotExist(err)
+	return err == nil
 }

This returns true only when the file definitively exists and is accessible.

client/android/profile_manager.go (2)

121-135: Consider validating profile existence before switching.

SwitchProfile sets the active profile without verifying that the target profile actually exists. This could lead to runtime errors when the client attempts to use a non-existent profile.

 func (pm *ProfileManager) SwitchProfile(profileName string) error {
+	// Validate profile exists before switching
+	configPath, err := pm.getProfileConfigPath(profileName)
+	if err != nil {
+		return fmt.Errorf("failed to get profile path: %w", err)
+	}
+	if _, err := os.Stat(configPath); os.IsNotExist(err) {
+		return fmt.Errorf("profile '%s' does not exist", profileName)
+	}
+
 	err := pm.serviceMgr.SetActiveProfileState(&profilemanager.ActiveProfileState{
 		Name:     profileName,
 		Username: androidUsername,
 	})

245-256: sanitizeProfileName can return empty string.

If the input contains only invalid characters (e.g., "!!!" or "..."), the function returns an empty string. This could cause unexpected behavior when the sanitized name is used in file paths.

Consider either returning an error or providing a fallback:

-func sanitizeProfileName(name string) string {
+func sanitizeProfileName(name string) (string, error) {
 	var result strings.Builder
 	for _, r := range name {
 		if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
 			(r >= '0' && r <= '9') || r == '_' || r == '-' {
 			result.WriteRune(r)
 		}
 	}
-	return result.String()
+	sanitized := result.String()
+	if sanitized == "" {
+		return "", fmt.Errorf("profile name '%s' contains no valid characters", name)
+	}
+	return sanitized, nil
 }

Note: This would require updating callers to handle the error.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 387d43b and 49cd864.

📒 Files selected for processing (4)
  • client/android/client.go (3 hunks)
  • client/android/profile_manager.go (1 hunks)
  • client/internal/engine.go (1 hunks)
  • client/internal/profilemanager/service.go (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
client/internal/engine.go (1)
client/internal/profilemanager/service.go (1)
  • NewServiceManager (82-87)
client/android/profile_manager.go (3)
client/internal/profilemanager/service.go (6)
  • ServiceManager (78-80)
  • DefaultConfigPathDir (23-23)
  • DefaultConfigPath (24-24)
  • ActiveProfileStatePath (25-25)
  • NewServiceManagerWithProfilesDir (91-98)
  • ActiveProfileState (56-59)
client/internal/profilemanager/config.go (2)
  • ReadConfig (772-774)
  • WriteOutConfig (812-814)
client/internal/profilemanager/profilemanager.go (1)
  • DefaultProfileName (16-16)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: Management / Unit (amd64, sqlite)
  • GitHub Check: Management / Unit (amd64, postgres)
  • GitHub Check: Management / Unit (amd64, mysql)
  • GitHub Check: Relay / Unit (386)
  • GitHub Check: Relay / Unit (amd64, -race)
  • GitHub Check: Client / Unit (amd64)
  • GitHub Check: Client (Docker) / Unit
  • GitHub Check: Client / Unit (386)
  • GitHub Check: JS / Lint
  • GitHub Check: Client / Unit
  • GitHub Check: Client / Unit
  • GitHub Check: Client / Unit
  • GitHub Check: release_ui_darwin
  • GitHub Check: release_ui
  • GitHub Check: release
  • GitHub Check: Android / Build
  • GitHub Check: iOS / Build
  • GitHub Check: Linux
  • GitHub Check: Darwin
  • GitHub Check: Windows
🔇 Additional comments (7)
client/internal/engine.go (1)

255-270: LGTM - Platform-specific state path resolution looks correct.

The logic correctly differentiates between mobile (iOS/Android) and desktop platforms. On mobile, the state file path is provided via mobileDep.StateFilePath and created if missing. On desktop, the ServiceManager is used to resolve the profile-aware state path.

One minor observation: the error from createFile is logged but doesn't prevent engine creation, which is the intended behavior per the comment. This is appropriate for resilience.

client/internal/profilemanager/service.go (2)

385-392: LGTM - Clean override pattern for profiles directory.

The getConfigDir method provides a clean abstraction that allows the Android-specific profilesDir to override the default behavior while maintaining backward compatibility with existing code paths.


89-98: Platform separation negates global state mutation risk.

The //go:build android tag at line 1 of client/android/profile_manager.go restricts this code to Android-only builds, and client/server/server.go has no build tags, meaning they never coexist in the same binary. The global DefaultConfigPath mutation is not a practical concern since each platform receives its own isolated binary with its own initialization sequence.

However, the pattern of mutating globals in constructors remains poor design practice. If this pattern is intentional for the Android use case, consider documenting why platform separation makes this safe or refactor to avoid the mutation entirely.

client/android/client.go (2)

91-128: LGTM - Clean refactor to accept paths per-call.

Moving PlatformFiles from the constructor to the Run method parameters enables profile switching by allowing different config/state paths for each run. The added logging at lines 97-98 improves debuggability.


133-161: LGTM - Consistent with Run method changes.

The RunWithoutLogin method follows the same pattern as Run, accepting PlatformFiles as a parameter and extracting config/state paths locally. This maintains consistency across the API.

client/android/profile_manager.go (2)

69-88: LGTM - Android-specific ProfileManager initialization.

The constructor properly sets up Android-specific paths:

  • Default config at configDir/netbird.cfg
  • Profiles in configDir/profiles/ subdirectory
  • Active profile state at configDir/active_profile.json

The use of NewServiceManagerWithProfilesDir correctly configures the profiles directory override.


138-146: No duplicate sanitization in this code path.

AddProfile passes the profile name directly to serviceMgr.AddProfile, which handles sanitization internally. There is no redundant sanitization occurring.

Likely an incorrect or invalid review comment.

Comment on lines 148 to 178
// LogoutProfile logs out from a profile (clears authentication)
func (pm *ProfileManager) LogoutProfile(profileName string) error {
profileName = sanitizeProfileName(profileName)

configPath, err := pm.getProfileConfigPath(profileName)
if err != nil {
return err
}

// Check if profile exists
if _, err := os.Stat(configPath); os.IsNotExist(err) {
return fmt.Errorf("profile '%s' does not exist", profileName)
}

// 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
config.PrivateKey = ""

// 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", profileName)
return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

ast-grep --pattern $'type Config struct {
  $$$
}'

Repository: netbirdio/netbird

Length of output: 12551


Clear all authentication credentials on logout, not just PrivateKey.

The LogoutProfile method only clears the PrivateKey field. The Config struct contains multiple authentication-related fields that persist after logout:

  • PreSharedKey (line 92)
  • SSHKey (line 122, private SSH key in PEM format)
  • ClientCertPath and ClientCertKeyPath (lines 150-153, mTLS certificate paths)
  • ClientCertKeyPair (line 155, mTLS certificate object)

These should be cleared to ensure a complete logout.

🤖 Prompt for AI Agents
In client/android/profile_manager.go around lines 148 to 178, LogoutProfile
currently only clears config.PrivateKey; update it to clear all
authentication-related fields (set to empty string or nil as appropriate):
PreSharedKey, SSHKey, ClientCertPath, ClientCertKeyPath, ClientCertKeyPair (and
any other auth fields on the Config struct), then write the config back with
profilemanager.WriteOutConfig and preserve existing logging/return behavior.

@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants