-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
feat: add MDM managed app configuration support for Android and iOS #5986
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dbrieck
wants to merge
4
commits into
netbirdio:main
Choose a base branch
from
dbrieck:feat/mdm-managed-config
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
5d8c87a
feat: add MDM managed app configuration support for Android and iOS
dbrieck-pengate f216e7a
fix: address CodeRabbit review feedback on MDM managed config
dbrieck-pengate b517419
fix: add GetManagementURL getter and pass mgmt URL to NewAuth
dbrieck-pengate 5b92c09
test: add iOS managed config tests to match Android parity
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| package android | ||
|
|
||
| import ( | ||
| "fmt" | ||
|
|
||
| log "github.com/sirupsen/logrus" | ||
|
|
||
| "github.com/netbirdio/netbird/client/internal/profilemanager" | ||
| ) | ||
|
|
||
| // Managed configuration key names for Android Enterprise managed configurations. | ||
| // These match the keys defined in app_restrictions.xml on the Android native app side. | ||
| const ( | ||
| managedConfigKeyManagementURL = "managementUrl" | ||
| managedConfigKeySetupKey = "setupKey" | ||
| managedConfigKeyAdminURL = "adminUrl" | ||
| managedConfigKeyPreSharedKey = "preSharedKey" | ||
| managedConfigKeyRosenpassEnabled = "rosenpassEnabled" | ||
| managedConfigKeyRosenpassPerm = "rosenpassPermissive" | ||
| managedConfigKeyDisableAutoConn = "disableAutoConnect" | ||
| ) | ||
|
|
||
| // Key name getters for the Android Java client to reference the same key constants. | ||
|
|
||
| // GetManagedConfigKeyManagementURL returns the key name for management URL | ||
| func GetManagedConfigKeyManagementURL() string { return managedConfigKeyManagementURL } | ||
|
|
||
| // GetManagedConfigKeySetupKey returns the key name for setup key | ||
| func GetManagedConfigKeySetupKey() string { return managedConfigKeySetupKey } | ||
|
|
||
| // GetManagedConfigKeyAdminURL returns the key name for admin URL | ||
| func GetManagedConfigKeyAdminURL() string { return managedConfigKeyAdminURL } | ||
|
|
||
| // GetManagedConfigKeyPreSharedKey returns the key name for pre-shared key | ||
| func GetManagedConfigKeyPreSharedKey() string { return managedConfigKeyPreSharedKey } | ||
|
|
||
| // GetManagedConfigKeyRosenpassEnabled returns the key name for Rosenpass enabled | ||
| func GetManagedConfigKeyRosenpassEnabled() string { return managedConfigKeyRosenpassEnabled } | ||
|
|
||
| // GetManagedConfigKeyRosenpassPermissive returns the key name for Rosenpass permissive | ||
| func GetManagedConfigKeyRosenpassPermissive() string { return managedConfigKeyRosenpassPerm } | ||
|
|
||
| // GetManagedConfigKeyDisableAutoConnect returns the key name for disable auto-connect | ||
| func GetManagedConfigKeyDisableAutoConnect() string { return managedConfigKeyDisableAutoConn } | ||
|
|
||
| // ManagedConfig holds configuration values pushed by an MDM/EMM via Android Enterprise | ||
| // managed configurations (app restrictions). Values set here override user preferences | ||
| // on every app launch. | ||
| // | ||
| // The native Android app reads from RestrictionsManager and populates this struct | ||
| // via the setter methods, then calls Apply() to write the values to the config file. | ||
| type ManagedConfig struct { | ||
| managementURL string | ||
| setupKey string | ||
| adminURL string | ||
| preSharedKey *string | ||
| rosenpassEnabled *bool | ||
| rosenpassPerm *bool | ||
| disableAutoConn *bool | ||
| } | ||
|
|
||
| // NewManagedConfig creates a new empty ManagedConfig | ||
| func NewManagedConfig() *ManagedConfig { | ||
| return &ManagedConfig{} | ||
| } | ||
|
|
||
| // SetManagementURL sets the management server URL from MDM config | ||
| func (m *ManagedConfig) SetManagementURL(url string) { | ||
| m.managementURL = url | ||
| } | ||
|
|
||
| // SetSetupKey sets the setup key for silent device registration from MDM config | ||
| func (m *ManagedConfig) SetSetupKey(key string) { | ||
| m.setupKey = key | ||
| } | ||
|
|
||
| // SetAdminURL sets the admin dashboard URL from MDM config | ||
| func (m *ManagedConfig) SetAdminURL(url string) { | ||
| m.adminURL = url | ||
| } | ||
|
|
||
| // SetPreSharedKey sets the WireGuard pre-shared key from MDM config | ||
| func (m *ManagedConfig) SetPreSharedKey(key string) { | ||
| m.preSharedKey = &key | ||
| } | ||
|
|
||
| // SetRosenpassEnabled sets whether Rosenpass post-quantum encryption is enabled | ||
| func (m *ManagedConfig) SetRosenpassEnabled(enabled bool) { | ||
| m.rosenpassEnabled = &enabled | ||
| } | ||
|
|
||
| // SetRosenpassPermissive sets whether Rosenpass permissive mode is enabled | ||
| func (m *ManagedConfig) SetRosenpassPermissive(permissive bool) { | ||
| m.rosenpassPerm = &permissive | ||
| } | ||
|
|
||
| // SetDisableAutoConnect sets whether auto-connect on launch is disabled | ||
| func (m *ManagedConfig) SetDisableAutoConnect(disable bool) { | ||
| m.disableAutoConn = &disable | ||
| } | ||
|
|
||
| // HasSetupKey returns true if a setup key was provided by MDM | ||
| func (m *ManagedConfig) HasSetupKey() bool { | ||
| return m.setupKey != "" | ||
| } | ||
|
|
||
| // GetSetupKey returns the MDM-provided setup key | ||
| func (m *ManagedConfig) GetSetupKey() string { | ||
| return m.setupKey | ||
| } | ||
|
|
||
| // HasConfig returns true if any configuration value was set by MDM | ||
| func (m *ManagedConfig) HasConfig() bool { | ||
| return m.managementURL != "" || | ||
| m.setupKey != "" || | ||
| m.adminURL != "" || | ||
| m.preSharedKey != nil || | ||
| m.rosenpassEnabled != nil || | ||
| m.rosenpassPerm != nil || | ||
| m.disableAutoConn != nil | ||
| } | ||
|
|
||
| // Apply writes the MDM-managed configuration values to the config file at configPath. | ||
| // Values provided by MDM override any existing user-set values. | ||
| // The setup key is NOT written to the config file — it is used separately for registration. | ||
| func (m *ManagedConfig) Apply(configPath string) error { | ||
| if !m.HasConfig() { | ||
| return nil | ||
| } | ||
|
|
||
| log.Info("Applying MDM managed configuration") | ||
|
|
||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| input := profilemanager.ConfigInput{ | ||
| ConfigPath: configPath, | ||
| } | ||
|
|
||
| if m.managementURL != "" { | ||
| input.ManagementURL = m.managementURL | ||
| log.Infof("MDM: setting management URL") | ||
| } | ||
|
|
||
| if m.adminURL != "" { | ||
| input.AdminURL = m.adminURL | ||
| log.Infof("MDM: setting admin URL") | ||
| } | ||
|
|
||
| if m.preSharedKey != nil { | ||
| input.PreSharedKey = m.preSharedKey | ||
| log.Infof("MDM: setting pre-shared key") | ||
| } | ||
|
|
||
| if m.rosenpassEnabled != nil { | ||
| input.RosenpassEnabled = m.rosenpassEnabled | ||
| log.Infof("MDM: setting Rosenpass enabled=%v", *m.rosenpassEnabled) | ||
| } | ||
|
|
||
| if m.rosenpassPerm != nil { | ||
| input.RosenpassPermissive = m.rosenpassPerm | ||
| log.Infof("MDM: setting Rosenpass permissive=%v", *m.rosenpassPerm) | ||
| } | ||
|
|
||
| if m.disableAutoConn != nil { | ||
| input.DisableAutoConnect = m.disableAutoConn | ||
| log.Infof("MDM: setting disable auto-connect=%v", *m.disableAutoConn) | ||
| } | ||
|
|
||
| _, err := profilemanager.UpdateOrCreateConfig(input) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to apply MDM config: %w", err) | ||
| } | ||
|
|
||
| log.Info("MDM managed configuration applied successfully") | ||
| return nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| package android | ||
|
|
||
| import ( | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
| "github.com/netbirdio/netbird/client/internal/profilemanager" | ||
| ) | ||
|
|
||
| func TestManagedConfig_NewIsEmpty(t *testing.T) { | ||
| m := NewManagedConfig() | ||
| if m.HasConfig() { | ||
| t.Error("new ManagedConfig should not have config") | ||
| } | ||
| if m.HasSetupKey() { | ||
| t.Error("new ManagedConfig should not have setup key") | ||
| } | ||
| if m.GetSetupKey() != "" { | ||
| t.Error("new ManagedConfig setup key should be empty") | ||
| } | ||
| } | ||
|
|
||
| func TestManagedConfig_SettersMarkHasConfig(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| setter func(*ManagedConfig) | ||
| }{ | ||
| {"managementURL", func(m *ManagedConfig) { m.SetManagementURL("https://example.com") }}, | ||
| {"setupKey", func(m *ManagedConfig) { m.SetSetupKey("test-key") }}, | ||
| {"adminURL", func(m *ManagedConfig) { m.SetAdminURL("https://admin.example.com") }}, | ||
| {"preSharedKey", func(m *ManagedConfig) { m.SetPreSharedKey("psk123") }}, | ||
| {"rosenpassEnabled", func(m *ManagedConfig) { m.SetRosenpassEnabled(true) }}, | ||
| {"rosenpassPermissive", func(m *ManagedConfig) { m.SetRosenpassPermissive(true) }}, | ||
| {"disableAutoConnect", func(m *ManagedConfig) { m.SetDisableAutoConnect(true) }}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| m := NewManagedConfig() | ||
| tt.setter(m) | ||
| if !m.HasConfig() { | ||
| t.Errorf("HasConfig() should be true after setting %s", tt.name) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestManagedConfig_SetupKey(t *testing.T) { | ||
| m := NewManagedConfig() | ||
| m.SetSetupKey("my-setup-key") | ||
| if !m.HasSetupKey() { | ||
| t.Error("HasSetupKey() should be true") | ||
| } | ||
| if m.GetSetupKey() != "my-setup-key" { | ||
| t.Errorf("GetSetupKey() = %q, want %q", m.GetSetupKey(), "my-setup-key") | ||
| } | ||
| } | ||
|
|
||
| func TestManagedConfig_ApplyEmpty(t *testing.T) { | ||
| m := NewManagedConfig() | ||
| cfgFile := filepath.Join(t.TempDir(), "netbird.json") | ||
| err := m.Apply(cfgFile) | ||
| if err != nil { | ||
| t.Fatalf("Apply on empty config should not error: %v", err) | ||
| } | ||
| } | ||
|
|
||
| func TestManagedConfig_ApplyManagementURL(t *testing.T) { | ||
| cfgFile := filepath.Join(t.TempDir(), "netbird.json") | ||
|
|
||
| m := NewManagedConfig() | ||
| m.SetManagementURL("https://custom.mgmt.example.com:443") | ||
| err := m.Apply(cfgFile) | ||
| if err != nil { | ||
| t.Fatalf("Apply failed: %v", err) | ||
| } | ||
|
|
||
| cfg, err := profilemanager.ReadConfig(cfgFile) | ||
| if err != nil { | ||
| t.Fatalf("ReadConfig failed: %v", err) | ||
| } | ||
| if cfg.ManagementURL.String() != "https://custom.mgmt.example.com:443" { | ||
| t.Errorf("ManagementURL = %q, want %q", cfg.ManagementURL.String(), "https://custom.mgmt.example.com:443") | ||
| } | ||
| } | ||
|
|
||
| func TestManagedConfig_ApplyOverridesExisting(t *testing.T) { | ||
| cfgFile := filepath.Join(t.TempDir(), "netbird.json") | ||
|
|
||
| // Create initial config with default URL | ||
| p := NewPreferences(cfgFile) | ||
| p.SetManagementURL("https://original.example.com:443") | ||
| if err := p.Commit(); err != nil { | ||
| t.Fatalf("initial Commit failed: %v", err) | ||
| } | ||
|
|
||
| // Apply MDM config that overrides the URL | ||
| m := NewManagedConfig() | ||
| m.SetManagementURL("https://mdm-managed.example.com:443") | ||
| m.SetRosenpassEnabled(true) | ||
| err := m.Apply(cfgFile) | ||
| if err != nil { | ||
| t.Fatalf("Apply failed: %v", err) | ||
| } | ||
|
|
||
| cfg, err := profilemanager.ReadConfig(cfgFile) | ||
| if err != nil { | ||
| t.Fatalf("ReadConfig failed: %v", err) | ||
| } | ||
| if cfg.ManagementURL.String() != "https://mdm-managed.example.com:443" { | ||
| t.Errorf("ManagementURL = %q, want %q", cfg.ManagementURL.String(), "https://mdm-managed.example.com:443") | ||
| } | ||
| if !cfg.RosenpassEnabled { | ||
| t.Error("RosenpassEnabled should be true after MDM apply") | ||
| } | ||
| } | ||
|
|
||
| func TestManagedConfig_ApplyPreSharedKey(t *testing.T) { | ||
| cfgFile := filepath.Join(t.TempDir(), "netbird.json") | ||
|
|
||
| m := NewManagedConfig() | ||
| m.SetPreSharedKey("mdm-psk-value") | ||
| err := m.Apply(cfgFile) | ||
| if err != nil { | ||
| t.Fatalf("Apply failed: %v", err) | ||
| } | ||
|
|
||
| cfg, err := profilemanager.ReadConfig(cfgFile) | ||
| if err != nil { | ||
| t.Fatalf("ReadConfig failed: %v", err) | ||
| } | ||
| if cfg.PreSharedKey != "mdm-psk-value" { | ||
| t.Errorf("PreSharedKey = %q, want %q", cfg.PreSharedKey, "mdm-psk-value") | ||
| } | ||
| } | ||
|
|
||
| func TestManagedConfig_SetupKeyNotWrittenToConfig(t *testing.T) { | ||
| cfgFile := filepath.Join(t.TempDir(), "netbird.json") | ||
|
|
||
| m := NewManagedConfig() | ||
| m.SetSetupKey("secret-setup-key") | ||
| m.SetManagementURL("https://example.com:443") | ||
| err := m.Apply(cfgFile) | ||
| if err != nil { | ||
| t.Fatalf("Apply failed: %v", err) | ||
| } | ||
|
|
||
| // The setup key should NOT be in the config file — it's only used for registration | ||
| cfg, err := profilemanager.ReadConfig(cfgFile) | ||
| if err != nil { | ||
| t.Fatalf("ReadConfig failed: %v", err) | ||
| } | ||
| // Config has no SetupKey field, so if we got here without error, the key was correctly not written | ||
| if cfg.ManagementURL.String() != "https://example.com:443" { | ||
| t.Errorf("ManagementURL = %q, want %q", cfg.ManagementURL.String(), "https://example.com:443") | ||
| } | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| func TestManagedConfig_KeyConstants(t *testing.T) { | ||
| if GetManagedConfigKeyManagementURL() != "managementUrl" { | ||
| t.Errorf("unexpected key: %s", GetManagedConfigKeyManagementURL()) | ||
| } | ||
| if GetManagedConfigKeySetupKey() != "setupKey" { | ||
| t.Errorf("unexpected key: %s", GetManagedConfigKeySetupKey()) | ||
| } | ||
| if GetManagedConfigKeyAdminURL() != "adminUrl" { | ||
| t.Errorf("unexpected key: %s", GetManagedConfigKeyAdminURL()) | ||
| } | ||
| if GetManagedConfigKeyPreSharedKey() != "preSharedKey" { | ||
| t.Errorf("unexpected key: %s", GetManagedConfigKeyPreSharedKey()) | ||
| } | ||
| if GetManagedConfigKeyRosenpassEnabled() != "rosenpassEnabled" { | ||
| t.Errorf("unexpected key: %s", GetManagedConfigKeyRosenpassEnabled()) | ||
| } | ||
| if GetManagedConfigKeyRosenpassPermissive() != "rosenpassPermissive" { | ||
| t.Errorf("unexpected key: %s", GetManagedConfigKeyRosenpassPermissive()) | ||
| } | ||
| if GetManagedConfigKeyDisableAutoConnect() != "disableAutoConnect" { | ||
| t.Errorf("unexpected key: %s", GetManagedConfigKeyDisableAutoConnect()) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.