mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-20 13:49:07 +02:00
Merge branch 'main' into profile-ownership
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
package mobile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||
"github.com/netbirdio/netbird/client/mdm"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -22,6 +24,9 @@ const (
|
||||
profilesSubdir = "profiles"
|
||||
)
|
||||
|
||||
// ErrProfilesDisabled marks a profile mutation rejected by MDM policy.
|
||||
var ErrProfilesDisabled = errors.New("profile management is disabled by MDM policy")
|
||||
|
||||
/*
|
||||
|
||||
<configDir>/ ← app-writable config root
|
||||
@@ -55,6 +60,7 @@ type ProfileManager struct {
|
||||
configDir string
|
||||
username string
|
||||
serviceMgr *profilemanager.ServiceManager
|
||||
mdmLoader *mdm.Loader
|
||||
}
|
||||
|
||||
// NewProfileManager creates a profile manager rooted at configDir, the
|
||||
@@ -127,6 +133,9 @@ func (pm *ProfileManager) GetActiveProfile() (*Profile, error) {
|
||||
// SwitchProfile records the given profile ID as the active profile. The caller
|
||||
// must stop the VPN tunnel before switching.
|
||||
func (pm *ProfileManager) SwitchProfile(id string) error {
|
||||
if err := pm.checkProfilesAllowed(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := pm.serviceMgr.SetActiveProfileState(&profilemanager.ActiveProfileState{
|
||||
ID: profilemanager.ID(id),
|
||||
Username: pm.username,
|
||||
@@ -141,6 +150,9 @@ func (pm *ProfileManager) SwitchProfile(id string) error {
|
||||
// AddProfile creates a new profile with the given display name and a
|
||||
// generated ID. It returns the created profile so the caller learns the ID.
|
||||
func (pm *ProfileManager) AddProfile(displayName string) (*Profile, error) {
|
||||
if err := pm.checkProfilesAllowed(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
profile, err := pm.serviceMgr.AddProfile(displayName, pm.username, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("add profile: %w", err)
|
||||
@@ -153,6 +165,9 @@ func (pm *ProfileManager) AddProfile(displayName string) (*Profile, error) {
|
||||
// RenameProfile changes the display name of the profile identified by id. The
|
||||
// on-disk filename (the ID) is left unchanged.
|
||||
func (pm *ProfileManager) RenameProfile(id string, newName string) error {
|
||||
if err := pm.checkProfilesAllowed(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := pm.serviceMgr.RenameProfile(profilemanager.ID(id), pm.username, newName); err != nil {
|
||||
return fmt.Errorf("rename profile: %w", err)
|
||||
}
|
||||
@@ -165,6 +180,9 @@ func (pm *ProfileManager) RenameProfile(id string, newName string) error {
|
||||
// private key and SSH key from the config, forcing a re-login. The management
|
||||
// URL and other settings are preserved.
|
||||
func (pm *ProfileManager) LogoutProfile(id string) error {
|
||||
if err := pm.checkProfileLogoutAllowed(id); err != nil {
|
||||
return err
|
||||
}
|
||||
configPath, err := pm.getProfileConfigPath(id)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -196,6 +214,9 @@ func (pm *ProfileManager) LogoutProfile(id string) error {
|
||||
// RemoveProfile deletes a profile. The default profile and the active profile
|
||||
// cannot be removed.
|
||||
func (pm *ProfileManager) RemoveProfile(id string) error {
|
||||
if err := pm.checkProfilesAllowed(); err != nil {
|
||||
return err
|
||||
}
|
||||
configPath, err := pm.getProfileConfigPath(id)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -267,6 +288,27 @@ func (pm *ProfileManager) GetActiveStateFilePath() (string, error) {
|
||||
return pm.GetStateFilePath(activeProfile.ID)
|
||||
}
|
||||
|
||||
// SetMDMLoader registers the MDM policy source consulted before profile
|
||||
// mutations; a nil loader disables enforcement.
|
||||
func (pm *ProfileManager) SetMDMLoader(loader *mdm.Loader) {
|
||||
pm.mdmLoader = loader
|
||||
}
|
||||
|
||||
func (pm *ProfileManager) checkProfilesAllowed() error {
|
||||
if v, ok := pm.mdmLoader.Load().GetBool(mdm.KeyDisableProfiles); ok && v {
|
||||
return ErrProfilesDisabled
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pm *ProfileManager) checkProfileLogoutAllowed(id string) error {
|
||||
active, err := pm.serviceMgr.GetActiveProfileState()
|
||||
if err == nil && active.ID.String() == id {
|
||||
return nil
|
||||
}
|
||||
return pm.checkProfilesAllowed()
|
||||
}
|
||||
|
||||
// profileEmail returns the account email recorded for a profile. Display-only,
|
||||
// so an unresolvable path degrades to "" rather than an error.
|
||||
func (pm *ProfileManager) profileEmail(id string) string {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package mobile
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||
"github.com/netbirdio/netbird/client/mdm"
|
||||
)
|
||||
|
||||
type fakeFetcher struct{ values map[string]any }
|
||||
|
||||
func (f *fakeFetcher) Fetch() map[string]any { return f.values }
|
||||
|
||||
func newTestProfileManager(t *testing.T) *ProfileManager {
|
||||
t.Helper()
|
||||
origDir := profilemanager.DefaultConfigPathDir
|
||||
origPath := profilemanager.DefaultConfigPath
|
||||
origActive := profilemanager.ActiveProfileStatePath
|
||||
t.Cleanup(func() {
|
||||
profilemanager.DefaultConfigPathDir = origDir
|
||||
profilemanager.DefaultConfigPath = origPath
|
||||
profilemanager.ActiveProfileStatePath = origActive
|
||||
})
|
||||
|
||||
configDir := t.TempDir()
|
||||
pm := NewProfileManager(configDir, "mobile")
|
||||
_, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
|
||||
ConfigPath: filepath.Join(configDir, defaultConfigFilename),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return pm
|
||||
}
|
||||
|
||||
func privateKeyOf(t *testing.T, pm *ProfileManager, id string) string {
|
||||
t.Helper()
|
||||
path, err := pm.getProfileConfigPath(id)
|
||||
require.NoError(t, err)
|
||||
raw, err := os.ReadFile(path)
|
||||
require.NoError(t, err)
|
||||
var cfg struct{ PrivateKey string }
|
||||
require.NoError(t, json.Unmarshal(raw, &cfg))
|
||||
return cfg.PrivateKey
|
||||
}
|
||||
|
||||
func TestLogoutProfile_DisableProfiles(t *testing.T) {
|
||||
pm := newTestProfileManager(t)
|
||||
other, err := pm.AddProfile("work")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, pm.SwitchProfile(profilemanager.DefaultProfileName))
|
||||
require.NotEmpty(t, privateKeyOf(t, pm, profilemanager.DefaultProfileName))
|
||||
require.NotEmpty(t, privateKeyOf(t, pm, other.ID))
|
||||
|
||||
pm.SetMDMLoader(mdm.NewLoader(&fakeFetcher{values: map[string]any{
|
||||
mdm.KeyDisableProfiles: true,
|
||||
}}))
|
||||
|
||||
err = pm.LogoutProfile(other.ID)
|
||||
assert.ErrorIs(t, err, ErrProfilesDisabled)
|
||||
assert.NotEmpty(t, privateKeyOf(t, pm, other.ID))
|
||||
|
||||
require.NoError(t, pm.LogoutProfile(profilemanager.DefaultProfileName))
|
||||
assert.Empty(t, privateKeyOf(t, pm, profilemanager.DefaultProfileName))
|
||||
}
|
||||
|
||||
func TestLogoutProfile_ProfilesAllowed(t *testing.T) {
|
||||
pm := newTestProfileManager(t)
|
||||
other, err := pm.AddProfile("work")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, pm.SwitchProfile(profilemanager.DefaultProfileName))
|
||||
|
||||
pm.SetMDMLoader(mdm.NewLoader(&fakeFetcher{values: map[string]any{
|
||||
mdm.KeyDisableProfiles: false,
|
||||
}}))
|
||||
|
||||
require.NoError(t, pm.LogoutProfile(other.ID))
|
||||
assert.Empty(t, privateKeyOf(t, pm, other.ID))
|
||||
}
|
||||
Reference in New Issue
Block a user