Compare commits

...

5 Commits

Author SHA1 Message Date
riccardom
b2c5732847 You now need to explicitly call these around 2026-06-16 10:42:14 +02:00
riccardom
0340893854 Now we need to apply MDM in the GetConfig 2026-06-15 18:36:38 +02:00
riccardom
874195440c Removes static vars 2026-06-15 17:32:46 +02:00
riccardom
bec26d5a14 Removes dead code 2026-06-15 13:01:04 +02:00
riccardom
db2c9b6f49 MDM Android mobile wiring 2026-06-15 12:04:26 +02:00
18 changed files with 414 additions and 171 deletions

View File

@@ -23,6 +23,7 @@ import (
"github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/internal/routemanager" "github.com/netbirdio/netbird/client/internal/routemanager"
"github.com/netbirdio/netbird/client/internal/stdnet" "github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/client/mdm"
"github.com/netbirdio/netbird/client/net" "github.com/netbirdio/netbird/client/net"
"github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/formatter" "github.com/netbirdio/netbird/formatter"
@@ -75,6 +76,13 @@ type Client struct {
connectClient *internal.ConnectClient connectClient *internal.ConnectClient
config *profilemanager.Config config *profilemanager.Config
cacheDir string cacheDir string
// mdmLoader holds the per-Client MDM policy source. Set by
// SetMDMPolicyFetcher (called from the Kotlin side). Each Run
// passes this loader to the resolved Config so applyMDMPolicy
// picks up the active overlay. Nil means "MDM enforcement off
// for this Client".
mdmLoader *mdm.Loader
} }
func (c *Client) setState(cfg *profilemanager.Config, cacheDir string, cc *internal.ConnectClient) { func (c *Client) setState(cfg *profilemanager.Config, cacheDir string, cc *internal.ConnectClient) {
@@ -129,6 +137,7 @@ func (c *Client) Run(platformFiles PlatformFiles, urlOpener URLOpener, isAndroid
if err != nil { if err != nil {
return err return err
} }
c.applyMDMOverlay(cfg)
c.recorder.UpdateManagementAddress(cfg.ManagementURL.String()) c.recorder.UpdateManagementAddress(cfg.ManagementURL.String())
c.recorder.UpdateRosenpass(cfg.RosenpassEnabled, cfg.RosenpassPermissive) c.recorder.UpdateRosenpass(cfg.RosenpassEnabled, cfg.RosenpassPermissive)
@@ -173,6 +182,7 @@ func (c *Client) RunWithoutLogin(platformFiles PlatformFiles, dns *DNSList, dnsR
if err != nil { if err != nil {
return err return err
} }
c.applyMDMOverlay(cfg)
c.recorder.UpdateManagementAddress(cfg.ManagementURL.String()) c.recorder.UpdateManagementAddress(cfg.ManagementURL.String())
c.recorder.UpdateRosenpass(cfg.RosenpassEnabled, cfg.RosenpassPermissive) c.recorder.UpdateRosenpass(cfg.RosenpassEnabled, cfg.RosenpassPermissive)
@@ -230,6 +240,7 @@ func (c *Client) DebugBundle(platformFiles PlatformFiles, anonymize bool) (strin
if err != nil { if err != nil {
return "", fmt.Errorf("load config: %w", err) return "", fmt.Errorf("load config: %w", err)
} }
c.applyMDMOverlay(cfg)
cacheDir = platformFiles.CacheDir() cacheDir = platformFiles.CacheDir()
} }

80
client/android/mdm.go Normal file
View File

@@ -0,0 +1,80 @@
//go:build android
package android
import (
"encoding/json"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/mdm"
)
// PolicyFetcher is the mobile-side bridge for the MDM managed-config
// snapshot. The native layer (Kotlin) implements this and registers
// the instance per Client via Client.SetMDMPolicyFetcher. Every
// invocation of fetchJSON must read the current RestrictionsManager
// state and return the result as a JSON-encoded map[string]any string.
//
// JSON is used because gomobile does not support map[string]any
// crossing the JNI boundary — the adapter on the Go side parses the
// string back into the map[string]any expected by mdm.Loader.
//
// Return value contract:
// - "" (empty) : interpreted as "no MDM source / no managed keys"
// - "{}" : managed config explicitly empty
// - "{...}" : JSON object with key/value pairs
// - malformed JSON : logged and treated as empty
type PolicyFetcher interface {
FetchJSON() string
}
// jsonFetcherAdapter wraps a gomobile-exposed PolicyFetcher into the
// internal mdm.PolicyFetcher interface, taking care of JSON decoding
// on every Fetch.
type jsonFetcherAdapter struct {
inner PolicyFetcher
}
func (a *jsonFetcherAdapter) Fetch() map[string]any {
raw := a.inner.FetchJSON()
if raw == "" {
return nil
}
var out map[string]any
if err := json.Unmarshal([]byte(raw), &out); err != nil {
log.Warnf("MDM mobile fetcher: invalid JSON payload from native: %v", err)
return nil
}
return out
}
// SetMDMPolicyFetcher registers the native-provided MDM policy fetcher
// on this Client. Call once from the gomobile-init code (Kotlin
// Application.onCreate or Service onCreate) before invoking Run /
// RunWithoutLogin. Passing nil disables MDM enforcement on this
// Client.
//
// The fetcher is held as a *mdm.Loader instance on the Client (no
// package-level state) — multiple Clients in the same process get
// independent Loaders, and tests can inject fakes per Client.
func (c *Client) SetMDMPolicyFetcher(p PolicyFetcher) {
if p == nil {
c.mdmLoader = mdm.NewLoader(nil)
return
}
c.mdmLoader = mdm.NewLoader(&jsonFetcherAdapter{inner: p})
}
// applyMDMOverlay applies the Client-held MDM Loader's current policy
// on top of the just-read Config. Called immediately after every
// UpdateOrCreateConfig — profilemanager's apply() initialises the
// policy to empty and leaves overlay responsibility to the lifecycle
// owner. No-op when no fetcher was registered.
func (c *Client) applyMDMOverlay(cfg *profilemanager.Config) {
if cfg == nil || c.mdmLoader == nil {
return
}
cfg.ApplyMDMPolicy(c.mdmLoader.Load())
}

View File

@@ -17,6 +17,7 @@ import (
"github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/auth" "github.com/netbirdio/netbird/client/internal/auth"
"github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/mdm"
"github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/util" "github.com/netbirdio/netbird/util"
@@ -248,6 +249,11 @@ func doForegroundLogin(ctx context.Context, cmd *cobra.Command, setupKey string,
if err != nil { if err != nil {
return fmt.Errorf("read config file %s: %v", configFilePath, err) return fmt.Errorf("read config file %s: %v", configFilePath, err)
} }
// CLI standalone login: profilemanager no longer auto-applies MDM,
// so layer in the OS-native policy here. Desktop builds construct
// a Loader with no fetcher — the build-tagged loadPlatform reads
// the registry/plist directly.
config.ApplyMDMPolicy(mdm.NewLoader(nil).Load())
err = foregroundLogin(ctx, cmd, config, setupKey, activeProf.Name) err = foregroundLogin(ctx, cmd, config, setupKey, activeProf.Name)
if err != nil { if err != nil {

View File

@@ -21,6 +21,7 @@ import (
"github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/mdm"
"github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/shared/management/domain"
@@ -187,6 +188,10 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command, activeProf *pr
if err != nil { if err != nil {
return fmt.Errorf("get config file: %v", err) return fmt.Errorf("get config file: %v", err)
} }
// CLI foreground path runs without the daemon Server: layer in the
// active MDM policy explicitly so a forced ManagementURL / PSK /
// other managed key actually takes effect on this run.
config.ApplyMDMPolicy(mdm.NewLoader(nil).Load())
_, _ = profilemanager.UpdateOldManagementURL(ctx, config, configFilePath) _, _ = profilemanager.UpdateOldManagementURL(ctx, config, configFilePath)

View File

@@ -21,6 +21,7 @@ import (
"github.com/netbirdio/netbird/client/internal/auth" "github.com/netbirdio/netbird/client/internal/auth"
"github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/mdm"
sshcommon "github.com/netbirdio/netbird/client/ssh" sshcommon "github.com/netbirdio/netbird/client/ssh"
"github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/shared/management/domain"
@@ -215,6 +216,10 @@ func New(opts Options) (*Client, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("create config: %w", err) return nil, fmt.Errorf("create config: %w", err)
} }
// Embedded path runs without the daemon Server: apply the active
// MDM policy explicitly so a forced ManagementURL / PSK / other
// managed key takes effect on this embedded engine instance.
config.ApplyMDMPolicy(mdm.NewLoader(nil).Load())
if opts.PrivateKey != "" { if opts.PrivateKey != "" {
config.PrivateKey = opts.PrivateKey config.PrivateKey = opts.PrivateKey

View File

@@ -58,10 +58,6 @@ var DefaultInterfaceBlacklist = []string{
"Tailscale", "tailscale", "docker", "veth", "br-", "lo", "Tailscale", "tailscale", "docker", "veth", "br-", "lo",
} }
// loadMDMPolicy is the package-level indirection used by apply() to read the
// active MDM policy. Tests override this to inject a fake policy.
var loadMDMPolicy = mdm.LoadPolicy
// ConfigInput carries configuration changes to the client // ConfigInput carries configuration changes to the client
type ConfigInput struct { type ConfigInput struct {
ManagementURL string ManagementURL string
@@ -180,14 +176,27 @@ type Config struct {
MTU uint16 MTU uint16
// policy is the MDM policy that produced the currently-set values for // policy is the MDM policy that produced the currently-set values
// any MDM-enforced fields. Set by applyMDMPolicy at the tail of apply() // for any MDM-enforced fields. Set by ApplyMDMPolicy on every
// and reset on every apply() invocation. Never persisted to disk. // invocation. Never persisted to disk. Callers query enforcement
// Callers query enforcement state via Policy() and the mdm.Policy API // state via Policy() and the mdm.Policy API (HasKey, ManagedKeys,
// (HasKey, ManagedKeys, IsEmpty). // IsEmpty).
policy *mdm.Policy `json:"-"` policy *mdm.Policy `json:"-"`
} }
// ApplyMDMPolicy overlays the supplied MDM Policy on top of the
// currently resolved Config values. Idempotent — pass an empty Policy
// to clear any prior overlay. The lifecycle owner (Server.getConfig
// on desktop, the Client.Run path on mobile) calls this with
// loader.Load() once the per-process Loader is known; the Config
// itself holds no reference to the Loader.
func (config *Config) ApplyMDMPolicy(policy *mdm.Policy) {
if config == nil {
return
}
config.applyMDMPolicy(policy)
}
// Policy returns the MDM policy applied to this Config. Returns a non-nil // Policy returns the MDM policy applied to this Config. Returns a non-nil
// empty Policy when MDM enforcement is inactive; callers can always invoke // empty Policy when MDM enforcement is inactive; callers can always invoke
// HasKey / ManagedKeys / IsEmpty without a nil check. // HasKey / ManagedKeys / IsEmpty without a nil check.
@@ -634,9 +643,11 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) {
updated = true updated = true
} }
// MDM is the last override layer: any key present in the policy // Initialise the MDM overlay to "no enforcement" so Config.Policy()
// supersedes defaults, on-disk config, env vars and CLI input. // never returns a stale or nil policy on a freshly applied Config.
config.applyMDMPolicy(loadMDMPolicy()) // Lifecycle owners that want to enforce a real MDM policy invoke
// Config.ApplyMDMPolicy(loader.Load()) after this returns.
config.applyMDMPolicy(mdm.NewPolicy(nil))
return updated, nil return updated, nil
} }

View File

@@ -10,24 +10,58 @@ import (
"github.com/netbirdio/netbird/client/mdm" "github.com/netbirdio/netbird/client/mdm"
) )
// withMDMPolicy temporarily overrides the package-level loadMDMPolicy hook so // fakeFetcher implements mdm.PolicyFetcher returning a pre-set policy
// apply() observes the supplied Policy. The original loader is restored at // map. Test helper used to construct a Loader without touching the OS
// test cleanup. // or any package-level state.
func withMDMPolicy(t *testing.T, policy *mdm.Policy) { type fakeFetcher struct{ values map[string]any }
func (f *fakeFetcher) Fetch() map[string]any { return f.values }
// loaderFor builds an mdm.Loader whose loadPlatform returns the
// supplied Policy's underlying values.
func loaderFor(policy *mdm.Policy) *mdm.Loader {
if policy == nil || policy.IsEmpty() {
return mdm.NewLoader(&fakeFetcher{values: nil})
}
values := make(map[string]any)
for _, k := range policy.ManagedKeys() {
if v, ok := policy.GetString(k); ok {
values[k] = v
continue
}
if v, ok := policy.GetBool(k); ok {
values[k] = v
continue
}
if v, ok := policy.GetInt(k); ok {
values[k] = v
continue
}
if v, ok := policy.GetStringSlice(k); ok {
values[k] = v
}
}
return mdm.NewLoader(&fakeFetcher{values: values})
}
// configWithMDM is the test convenience that builds a Config via
// UpdateOrCreateConfig and overlays the supplied MDM policy on top —
// mirrors the production pattern (Server.getConfig / Client.applyMDMOverlay)
// where the Loader lives outside Config and the apply step is driven
// by the lifecycle owner.
func configWithMDM(t *testing.T, input ConfigInput, policy *mdm.Policy) *Config {
t.Helper() t.Helper()
prev := loadMDMPolicy cfg, err := UpdateOrCreateConfig(input)
loadMDMPolicy = func() *mdm.Policy { return policy } require.NoError(t, err)
t.Cleanup(func() { loadMDMPolicy = prev }) require.NotNil(t, cfg)
cfg.ApplyMDMPolicy(loaderFor(policy).Load())
return cfg
} }
func TestApply_MDMEmpty_NoEnforcement(t *testing.T) { func TestApply_MDMEmpty_NoEnforcement(t *testing.T) {
withMDMPolicy(t, mdm.NewPolicy(nil)) cfg := configWithMDM(t, ConfigInput{
cfg, err := UpdateOrCreateConfig(ConfigInput{
ConfigPath: filepath.Join(t.TempDir(), "config.json"), ConfigPath: filepath.Join(t.TempDir(), "config.json"),
}) }, mdm.NewPolicy(nil))
require.NoError(t, err)
require.NotNil(t, cfg)
assert.True(t, cfg.Policy().IsEmpty(), "no MDM source ⇒ empty Policy") assert.True(t, cfg.Policy().IsEmpty(), "no MDM source ⇒ empty Policy")
assert.False(t, cfg.Policy().HasKey(mdm.KeyManagementURL)) assert.False(t, cfg.Policy().HasKey(mdm.KeyManagementURL))
@@ -39,18 +73,15 @@ func TestApply_MDMEmpty_NoEnforcement(t *testing.T) {
func TestApply_MDMOnly_OverridesDefaults(t *testing.T) { func TestApply_MDMOnly_OverridesDefaults(t *testing.T) {
const mdmURL = "https://corp.mdm.example.com:443" const mdmURL = "https://corp.mdm.example.com:443"
withMDMPolicy(t, mdm.NewPolicy(map[string]any{
cfg := configWithMDM(t, ConfigInput{
ConfigPath: filepath.Join(t.TempDir(), "config.json"),
}, mdm.NewPolicy(map[string]any{
mdm.KeyManagementURL: mdmURL, mdm.KeyManagementURL: mdmURL,
mdm.KeyDisableClientRoutes: true, mdm.KeyDisableClientRoutes: true,
mdm.KeyBlockInbound: true, mdm.KeyBlockInbound: true,
})) }))
cfg, err := UpdateOrCreateConfig(ConfigInput{
ConfigPath: filepath.Join(t.TempDir(), "config.json"),
})
require.NoError(t, err)
require.NotNil(t, cfg)
assert.Equal(t, mdmURL, cfg.ManagementURL.String()) assert.Equal(t, mdmURL, cfg.ManagementURL.String())
assert.True(t, cfg.DisableClientRoutes) assert.True(t, cfg.DisableClientRoutes)
assert.True(t, cfg.BlockInbound) assert.True(t, cfg.BlockInbound)
@@ -65,16 +96,12 @@ func TestApply_MDMBeatsCLIInput(t *testing.T) {
const mdmURL = "https://mdm.example.com:443" const mdmURL = "https://mdm.example.com:443"
const cliURL = "https://cli.example.com:443" const cliURL = "https://cli.example.com:443"
withMDMPolicy(t, mdm.NewPolicy(map[string]any{ cfg := configWithMDM(t, ConfigInput{
mdm.KeyManagementURL: mdmURL,
}))
cfg, err := UpdateOrCreateConfig(ConfigInput{
ConfigPath: filepath.Join(t.TempDir(), "config.json"), ConfigPath: filepath.Join(t.TempDir(), "config.json"),
ManagementURL: cliURL, ManagementURL: cliURL,
}) }, mdm.NewPolicy(map[string]any{
require.NoError(t, err) mdm.KeyManagementURL: mdmURL,
require.NotNil(t, cfg) }))
// MDM wins over CLI-supplied management URL. // MDM wins over CLI-supplied management URL.
assert.Equal(t, mdmURL, cfg.ManagementURL.String()) assert.Equal(t, mdmURL, cfg.ManagementURL.String())
@@ -82,16 +109,12 @@ func TestApply_MDMBeatsCLIInput(t *testing.T) {
} }
func TestApply_MDMInvalidURL_KeepsPreviousValue(t *testing.T) { func TestApply_MDMInvalidURL_KeepsPreviousValue(t *testing.T) {
withMDMPolicy(t, mdm.NewPolicy(map[string]any{ cfg := configWithMDM(t, ConfigInput{
ConfigPath: filepath.Join(t.TempDir(), "config.json"),
}, mdm.NewPolicy(map[string]any{
mdm.KeyManagementURL: "not-a-url", mdm.KeyManagementURL: "not-a-url",
})) }))
cfg, err := UpdateOrCreateConfig(ConfigInput{
ConfigPath: filepath.Join(t.TempDir(), "config.json"),
})
require.NoError(t, err)
require.NotNil(t, cfg)
// Invalid MDM URL is logged and skipped: default URL stays in place // Invalid MDM URL is logged and skipped: default URL stays in place
// to keep the client functional. // to keep the client functional.
assert.Equal(t, DefaultManagementURL, cfg.ManagementURL.String()) assert.Equal(t, DefaultManagementURL, cfg.ManagementURL.String())
@@ -106,24 +129,20 @@ func TestApply_MDMBoolKeysOverrideOnDiskValue(t *testing.T) {
tmp := filepath.Join(t.TempDir(), "config.json") tmp := filepath.Join(t.TempDir(), "config.json")
// Seed without MDM. // Seed without MDM.
withMDMPolicy(t, mdm.NewPolicy(nil)) configWithMDM(t, ConfigInput{
_, err := UpdateOrCreateConfig(ConfigInput{
ConfigPath: tmp, ConfigPath: tmp,
DisableClientRoutes: boolPtr(false), DisableClientRoutes: boolPtr(false),
RosenpassEnabled: boolPtr(false), RosenpassEnabled: boolPtr(false),
}) }, mdm.NewPolicy(nil))
require.NoError(t, err)
// Now enable MDM enforcement for these keys. // Now enable MDM enforcement for these keys.
withMDMPolicy(t, mdm.NewPolicy(map[string]any{ cfg := configWithMDM(t, ConfigInput{
ConfigPath: tmp,
}, mdm.NewPolicy(map[string]any{
mdm.KeyDisableClientRoutes: true, mdm.KeyDisableClientRoutes: true,
mdm.KeyRosenpassEnabled: true, mdm.KeyRosenpassEnabled: true,
})) }))
cfg, err := UpdateOrCreateConfig(ConfigInput{ConfigPath: tmp})
require.NoError(t, err)
require.NotNil(t, cfg)
assert.True(t, cfg.DisableClientRoutes, "MDM override should flip on-disk false to true") assert.True(t, cfg.DisableClientRoutes, "MDM override should flip on-disk false to true")
assert.True(t, cfg.RosenpassEnabled) assert.True(t, cfg.RosenpassEnabled)
assert.True(t, cfg.Policy().HasKey(mdm.KeyDisableClientRoutes)) assert.True(t, cfg.Policy().HasKey(mdm.KeyDisableClientRoutes))
@@ -133,16 +152,12 @@ func TestApply_MDMBoolKeysOverrideOnDiskValue(t *testing.T) {
func TestApply_MDMPreSharedKeyRedactionSentinelRejected(t *testing.T) { func TestApply_MDMPreSharedKeyRedactionSentinelRejected(t *testing.T) {
const maskSentinel = "**********" const maskSentinel = "**********"
withMDMPolicy(t, mdm.NewPolicy(map[string]any{ cfg := configWithMDM(t, ConfigInput{
ConfigPath: filepath.Join(t.TempDir(), "config.json"),
}, mdm.NewPolicy(map[string]any{
mdm.KeyPreSharedKey: maskSentinel, mdm.KeyPreSharedKey: maskSentinel,
})) }))
cfg, err := UpdateOrCreateConfig(ConfigInput{
ConfigPath: filepath.Join(t.TempDir(), "config.json"),
})
require.NoError(t, err)
require.NotNil(t, cfg)
// Mask sentinel must not be persisted as the actual PSK. // Mask sentinel must not be persisted as the actual PSK.
assert.NotEqual(t, maskSentinel, cfg.PreSharedKey) assert.NotEqual(t, maskSentinel, cfg.PreSharedKey)
// Key still marked managed so user writes are still rejected. // Key still marked managed so user writes are still rejected.

View File

@@ -84,16 +84,48 @@ func NewPolicy(values map[string]any) *Policy {
return &Policy{values: values} return &Policy{values: values}
} }
// LoadPolicy reads the platform-native MDM configuration. Returns an // PolicyFetcher is implemented by mobile platforms (Android / iOS) that
// empty (but non-nil) Policy when no source is present, the source is // push the OS-managed configuration into the Go runtime instead of
// empty, or the platform is unsupported. // having Go read an on-disk source directly. Desktop platforms ignore
// this interface — Loader.loadPlatform on windows/darwin reads the
// registry / plist on its own. A Loader constructed with a non-nil
// fetcher delegates to it on mobile; passing nil disables MDM
// enforcement (loadPlatform returns nil values).
type PolicyFetcher interface {
Fetch() map[string]any
}
// Loader is the DI-friendly entry point for reading the active MDM
// policy. Construct one at the daemon's lifecycle owner (Server on
// desktop, gomobile-exposed bridge on mobile) and pass it to anything
// that needs to read MDM state (the reload ticker, profilemanager's
// Config). Each callsite has the Loader handed in instead of looking
// up package-level state.
type Loader struct {
fetcher PolicyFetcher
}
// NewLoader constructs a Loader. The fetcher is consulted only on
// mobile builds (ios || android); on desktop it is unused but accepted
// to keep a single constructor signature across platforms — pass nil
// on desktop.
func NewLoader(f PolicyFetcher) *Loader {
return &Loader{fetcher: f}
}
// Load reads the platform-native MDM configuration and returns a
// Policy. Returns an empty (but non-nil) Policy when no source is
// present, the source is empty, or the platform is unsupported.
// //
// Diagnostic logging differentiates the three states: // Diagnostic logging differentiates the three states:
// - source absent / unsupported platform: trace log only // - source absent / unsupported platform: trace log only
// - source present, zero keys: info "MDM enrolled (no managed keys)" // - source present, zero keys: info "MDM enrolled (no managed keys)"
// - source present, N keys: info "MDM enrolled with N managed keys: [...]" // - source present, N keys: info "MDM enrolled with N managed keys: [...]"
func LoadPolicy() *Policy { func (l *Loader) Load() *Policy {
values, err := loadPlatformPolicy() if l == nil {
return &Policy{values: map[string]any{}}
}
values, err := l.loadPlatform()
if err != nil { if err != nil {
log.Tracef("MDM policy load: %v", err) log.Tracef("MDM policy load: %v", err)
return &Policy{values: map[string]any{}} return &Policy{values: map[string]any{}}

View File

@@ -25,8 +25,10 @@ import (
// writable plist, as a defense against tampered installs. // writable plist, as a defense against tampered installs.
const policyPlistPath = "/Library/Managed Preferences/io.netbird.client.plist" const policyPlistPath = "/Library/Managed Preferences/io.netbird.client.plist"
// loadPlatformPolicy reads the MDM-managed configuration from the macOS // loadPlatform reads the MDM-managed configuration from the macOS
// managed-preferences plist at policyPlistPath. Returns: // managed-preferences plist at policyPlistPath. The Loader's fetcher
// field is unused on this platform — the plist is the authoritative
// source. Returns:
// - (nil, nil) when the plist is absent (device not MDM-enrolled for // - (nil, nil) when the plist is absent (device not MDM-enrolled for
// NetBird, or admin has not yet pushed a payload) // NetBird, or admin has not yet pushed a payload)
// - (map, nil) with N entries when N managed values are present // - (map, nil) with N entries when N managed values are present
@@ -39,7 +41,13 @@ const policyPlistPath = "/Library/Managed Preferences/io.netbird.client.plist"
// skipped so a stray entry in the payload does not block startup. // skipped so a stray entry in the payload does not block startup.
// Native plist value types map naturally onto the Policy accessor // Native plist value types map naturally onto the Policy accessor
// expectations (GetString / GetBool / GetInt / GetStringSlice). // expectations (GetString / GetBool / GetInt / GetStringSlice).
func loadPlatformPolicy() (map[string]any, error) { func (l *Loader) loadPlatform() (map[string]any, error) {
// Honour the injected fetcher when present so tests (and any
// future non-macOS MDM channel) can short-circuit the plist read
// with a scripted policy.
if l != nil && l.fetcher != nil {
return l.fetcher.Fetch(), nil
}
f, err := os.Open(policyPlistPath) f, err := os.Open(policyPlistPath)
if err != nil { if err != nil {
if errors.Is(err, fs.ErrNotExist) { if errors.Is(err, fs.ErrNotExist) {

View File

@@ -2,13 +2,14 @@
package mdm package mdm
// loadPlatformPolicy is unused on mobile: the native layer (Swift on iOS, // loadPlatform reads the OS-managed configuration via the native
// Kotlin/Java on Android) reads the OS managed-config store and pushes the // PolicyFetcher injected at Loader construction. Returns
// resulting dictionary in-process via a gomobile entry point that lands in // (nil, nil) — the platform-absent sentinel that Loader.Load treats as
// Phase 5 / Phase 6. The stub keeps the package compilable for mobile // "no MDM source present" — when no fetcher was provided.
// builds and returns (nil, nil) — the platform-absent sentinel that func (l *Loader) loadPlatform() (map[string]any, error) {
// LoadPolicy in policy.go treats as "no MDM source present". if l == nil || l.fetcher == nil {
func loadPlatformPolicy() (map[string]any, error) { //nolint:nilnil // (nil, nil) is the documented platform-absent sentinel; see Loader.Load.
//nolint:nilnil // (nil, nil) is the documented platform-absent sentinel; see LoadPolicy. return nil, nil
return nil, nil }
return l.fetcher.Fetch(), nil
} }

View File

@@ -2,13 +2,17 @@
package mdm package mdm
// loadPlatformPolicy returns no policy on platforms without an MDM channel // loadPlatform reads the MDM policy on platforms without a native MDM
// (Linux, FreeBSD). MDM enforcement is off and the client behaves as if // channel (Linux, FreeBSD). When no fetcher was injected the policy is
// the feature did not exist. Returns (nil, nil) — the platform-absent // (nil, nil) — the platform-absent sentinel that Loader.Load treats as
// sentinel the caller (LoadPolicy in policy.go) treats as "no MDM // "MDM enforcement disabled". A non-nil fetcher takes precedence: it
// source present"; an error here would just translate to the same // is the test-seam used by unit tests to inject a scripted policy
// outcome with an extra log line. // without touching the OS, and the same hook supports any future
func loadPlatformPolicy() (map[string]any, error) { // non-mobile OS that grows an out-of-band MDM channel.
//nolint:nilnil // (nil, nil) is the documented platform-absent sentinel; see LoadPolicy. func (l *Loader) loadPlatform() (map[string]any, error) {
if l != nil && l.fetcher != nil {
return l.fetcher.Fetch(), nil
}
//nolint:nilnil // (nil, nil) is the documented platform-absent sentinel; see Loader.Load.
return nil, nil return nil, nil
} }

View File

@@ -150,10 +150,12 @@ func TestPolicy_GetStringSlice(t *testing.T) {
}) })
} }
func TestLoadPolicy_PlatformStubReturnsEmpty(t *testing.T) { func TestLoader_NilFetcherReturnsEmpty(t *testing.T) {
// loadPlatformPolicy is a stub on every OS for Phase 1. LoadPolicy must // Loader.Load with no fetcher (desktop construction) must degrade
// degrade gracefully and never return nil. // gracefully and never return nil; on linux loadPlatform is a stub
p := LoadPolicy() // returning (nil, nil), and Load is expected to translate that
// into a non-nil empty Policy.
p := NewLoader(nil).Load()
require.NotNil(t, p) require.NotNil(t, p)
assert.True(t, p.IsEmpty()) assert.True(t, p.IsEmpty())
assert.Empty(t, p.ManagedKeys()) assert.Empty(t, p.ManagedKeys())

View File

@@ -61,8 +61,10 @@ func readRegistryValue(k registry.Key, name, canonical string, out map[string]an
} }
} }
// loadPlatformPolicy reads the MDM-managed configuration from the // loadPlatform reads the MDM-managed configuration from the Windows
// Windows registry under HKLM\Software\Policies\NetBird. Returns: // registry under HKLM\Software\Policies\NetBird. The Loader's fetcher
// field is unused on this platform — the registry is the
// authoritative source. Returns:
// - (nil, nil) when the key is absent (device not MDM-enrolled for NetBird) // - (nil, nil) when the key is absent (device not MDM-enrolled for NetBird)
// - (map, nil) with N entries when N managed values are set (N may be 0) // - (map, nil) with N entries when N managed values are set (N may be 0)
// - (nil, err) on open / enumerate registry errors // - (nil, err) on open / enumerate registry errors
@@ -70,7 +72,13 @@ func readRegistryValue(k registry.Key, name, canonical string, out map[string]an
// Per-value type coercion + skip-on-error is delegated to // Per-value type coercion + skip-on-error is delegated to
// readRegistryValue. Unknown value names are logged and skipped so a // readRegistryValue. Unknown value names are logged and skipped so a
// malformed deployment does not block startup. // malformed deployment does not block startup.
func loadPlatformPolicy() (map[string]any, error) { func (l *Loader) loadPlatform() (map[string]any, error) {
// Honour the injected fetcher when present so tests (and any
// future non-Windows MDM channel) can short-circuit the registry
// read with a scripted policy.
if l != nil && l.fetcher != nil {
return l.fetcher.Fetch(), nil
}
k, err := registry.OpenKey(registry.LOCAL_MACHINE, policyRegistryPath, registry.QUERY_VALUE) k, err := registry.OpenKey(registry.LOCAL_MACHINE, policyRegistryPath, registry.QUERY_VALUE)
if err != nil { if err != nil {
if errors.Is(err, registry.ErrNotExist) { if errors.Is(err, registry.ErrNotExist) {

View File

@@ -15,33 +15,33 @@ import (
// instead, hence anticipating the ticker mechanism entirely. // instead, hence anticipating the ticker mechanism entirely.
const DefaultReloadInterval = 1 * time.Minute const DefaultReloadInterval = 1 * time.Minute
// policyLoader is the indirection through which the ticker reads the // Ticker periodically re-reads the OS-native MDM policy via the
// OS-native policy, both for the initial observation and on every tick. // injected Loader and invokes the onChange callback (supplied to Run)
// Production points it at LoadPolicy; tests in this package override it to // whenever the observed Policy diverges from the last observation
// feed a scripted sequence of policies without touching the real OS store. // (added / removed / changed keys). Launch with Run from a goroutine;
var policyLoader = LoadPolicy // cancel the supplied context to stop.
// Ticker periodically re-reads the OS-native MDM policy via LoadPolicy and
// invokes the onChange callback (supplied to Run) whenever the observed
// Policy diverges from the last observation (added / removed / changed
// keys). Launch with Run from a goroutine; cancel the supplied context
// to stop.
type Ticker struct { type Ticker struct {
interval time.Duration interval time.Duration
loader *Loader
prev *Policy prev *Policy
} }
// NewTicker constructs a Ticker that will re-read the OS-native policy // NewTicker constructs a Ticker that will re-read the OS-native policy
// every reloadInterval once Run is called. // every reloadInterval once Run is called. The Loader is injected so
// The initial snapshot is populated by calling policyLoader at // the ticker doesn't depend on any package-level state — production
// passes the daemon-owned Loader, tests pass a fake Loader (built with
// a fake PolicyFetcher).
//
// The initial snapshot is populated by calling loader.Load() at
// construction time so the first tick only fires // construction time so the first tick only fires
// onChange when the policy actually changed since boot — without // onChange when the policy actually changed since boot — without
// this baseline the first tick would report every currently-managed // this baseline the first tick would report every currently-managed
// key as "added" and trigger a spurious engine restart. // key as "added" and trigger a spurious engine restart.
func NewTicker(reloadInterval time.Duration) *Ticker { func NewTicker(reloadInterval time.Duration, loader *Loader) *Ticker {
return &Ticker{ return &Ticker{
interval: reloadInterval, interval: reloadInterval,
prev: policyLoader(), loader: loader,
prev: loader.Load(),
} }
} }
@@ -58,7 +58,7 @@ func (t *Ticker) Run(ctx context.Context, onChange func(prev, curr *Policy) erro
log.Info("MDM policy reload ticker stopped") log.Info("MDM policy reload ticker stopped")
return return
case <-tk.C: case <-tk.C:
curr := policyLoader() curr := t.loader.Load()
if policiesEqual(t.prev, curr) { if policiesEqual(t.prev, curr) {
continue continue
} }

View File

@@ -13,28 +13,40 @@ import (
// testReloadInterval for speeding up the ticker cadence under `go test` // testReloadInterval for speeding up the ticker cadence under `go test`
const testReloadInterval = 1 * time.Second const testReloadInterval = 1 * time.Second
// withPolicyLoader overrides the package-level policyLoader for the duration // fakePolicyFetcher implements PolicyFetcher returning a scripted
// of the test so the ticker observes a scripted policy instead of the real // policy map. Goroutine-safe so the test can mutate the script while
// OS-native store. The original loader is restored on cleanup. // the ticker is observing it.
func withPolicyLoader(t *testing.T, fn func() *Policy) { type fakePolicyFetcher struct {
t.Helper() mu sync.Mutex
prev := policyLoader values map[string]any
policyLoader = fn }
t.Cleanup(func() { policyLoader = prev })
func (f *fakePolicyFetcher) Fetch() map[string]any {
f.mu.Lock()
defer f.mu.Unlock()
if f.values == nil {
return nil
}
out := make(map[string]any, len(f.values))
for k, v := range f.values {
out[k] = v
}
return out
}
func (f *fakePolicyFetcher) set(values map[string]any) {
f.mu.Lock()
defer f.mu.Unlock()
f.values = values
} }
func TestTicker_FiresOnChangeWithDelta(t *testing.T) { func TestTicker_FiresOnChangeWithDelta(t *testing.T) {
var mu sync.Mutex fetcher := &fakePolicyFetcher{} // initial observation: empty (no enforcement)
current := NewPolicy(nil) // initial observation: empty (no enforcement) loader := NewLoader(fetcher)
withPolicyLoader(t, func() *Policy {
mu.Lock()
defer mu.Unlock()
return current
})
type change struct{ prev, curr *Policy } type change struct{ prev, curr *Policy }
changes := make(chan change, 1) changes := make(chan change, 1)
tk := NewTicker(testReloadInterval) tk := NewTicker(testReloadInterval, loader)
require.Equal(t, testReloadInterval, tk.interval) require.Equal(t, testReloadInterval, tk.interval)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
@@ -49,15 +61,13 @@ func TestTicker_FiresOnChangeWithDelta(t *testing.T) {
}) })
close(done) close(done)
}() }()
// Stop Run and wait for it to exit before returning, so the policyLoader // Stop Run and wait for it to exit before returning, so the test
// restore in t.Cleanup can't race the ticker goroutine still reading it. // goroutine doesn't race the still-running ticker.
defer func() { cancel(); <-done }() defer func() { cancel(); <-done }()
// Flip the OS-observed policy from empty to one managed key. The next // Flip the OS-observed policy from empty to one managed key. The
// tick must detect the diff and invoke onChange. // next tick must detect the diff and invoke onChange.
mu.Lock() fetcher.set(map[string]any{KeyManagementURL: "https://mdm.example.com:443"})
current = NewPolicy(map[string]any{KeyManagementURL: "https://mdm.example.com:443"})
mu.Unlock()
select { select {
case c := <-changes: case c := <-changes:
@@ -69,12 +79,11 @@ func TestTicker_FiresOnChangeWithDelta(t *testing.T) {
} }
func TestTicker_NoCallbackWhenPolicyUnchanged(t *testing.T) { func TestTicker_NoCallbackWhenPolicyUnchanged(t *testing.T) {
withPolicyLoader(t, func() *Policy { fetcher := &fakePolicyFetcher{values: map[string]any{KeyBlockInbound: true}}
return NewPolicy(map[string]any{KeyBlockInbound: true}) loader := NewLoader(fetcher)
})
fired := make(chan struct{}, 1) fired := make(chan struct{}, 1)
tk := NewTicker(testReloadInterval) tk := NewTicker(testReloadInterval, loader)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{}) done := make(chan struct{})
@@ -90,8 +99,8 @@ func TestTicker_NoCallbackWhenPolicyUnchanged(t *testing.T) {
}() }()
defer func() { cancel(); <-done }() defer func() { cancel(); <-done }()
// Over ~2 ticks at the 1s test cadence the policy never changes, so the // Over ~2 ticks at the 1s test cadence the policy never changes,
// diff guard must suppress the callback entirely. // so the diff guard must suppress the callback entirely.
select { select {
case <-fired: case <-fired:
t.Fatal("onChange fired despite an unchanged policy") t.Fatal("onChange fired despite an unchanged policy")

View File

@@ -20,10 +20,6 @@ import (
// a no-op echo, never as a conflict with the policy. // a no-op echo, never as a conflict with the policy.
const preSharedKeyRedactedSentinel = "**********" const preSharedKeyRedactedSentinel = "**********"
// loadMDMPolicy is the indirection used by server handlers to read the
// active MDM policy. Tests override this to inject a fake policy.
var loadMDMPolicy = mdm.LoadPolicy
// conflictCheck is a value-aware comparison between a single field in // conflictCheck is a value-aware comparison between a single field in
// the incoming request and the corresponding MDM-enforced value. It // the incoming request and the corresponding MDM-enforced value. It
// runs only when the field was actually set in the request (presence // runs only when the field was actually set in the request (presence

View File

@@ -110,6 +110,15 @@ type Server struct {
// stopped by the rootCtx cancellation. // stopped by the rootCtx cancellation.
mdmTicker *mdm.Ticker mdmTicker *mdm.Ticker
// mdmLoader is the daemon-owned source of the active MDM policy.
// Constructed once during Server.Start (with a nil PolicyFetcher on
// desktop — the build-tagged Loader.loadPlatform reads the OS
// registry / plist directly) and injected into every consumer:
// mdmTicker for its periodic reload, the SetConfig / Login MDM
// gates for conflict detection, and every Config produced via
// getConfig() so its apply() picks up the same overlay.
mdmLoader *mdm.Loader
updateManager *updater.Manager updateManager *updater.Manager
jwtCache *jwtCache jwtCache *jwtCache
@@ -173,8 +182,14 @@ func (s *Server) Start() error {
// Runs re-resolves Config (re-running profilemanager.Config.apply which // Runs re-resolves Config (re-running profilemanager.Config.apply which
// applies the freshly-read MDM policy as the last layer) and brings // applies the freshly-read MDM policy as the last layer) and brings
// the engine back with the new values. // the engine back with the new values.
if s.mdmLoader == nil {
// Desktop builds pass a nil PolicyFetcher: the Loader's
// build-tagged loadPlatform reads the OS source directly
// (registry on Windows, plist on macOS, no-op elsewhere).
s.mdmLoader = mdm.NewLoader(nil)
}
if s.mdmTicker == nil { if s.mdmTicker == nil {
s.mdmTicker = mdm.NewTicker(mdm.DefaultReloadInterval) s.mdmTicker = mdm.NewTicker(mdm.DefaultReloadInterval, s.mdmLoader)
go s.mdmTicker.Run(s.rootCtx, s.onMDMPolicyChange) go s.mdmTicker.Run(s.rootCtx, s.onMDMPolicyChange)
} }
@@ -370,7 +385,7 @@ func (s *Server) SetConfig(callerCtx context.Context, msg *proto.SetConfigReques
// by the active MDM policy. The error carries an MDMManagedFields- // by the active MDM policy. The error carries an MDMManagedFields-
// Violation detail listing the offending key names. Non-conflicting // Violation detail listing the offending key names. Non-conflicting
// fields in the same request are not applied either. // fields in the same request are not applied either.
policy := loadMDMPolicy() policy := s.mdmLoader.Load()
if err := rejectMDMManagedFieldConflicts(mdmManagedFieldConflicts(msg, policy)); err != nil { if err := rejectMDMManagedFieldConflicts(mdmManagedFieldConflicts(msg, policy)); err != nil {
return nil, err return nil, err
} }
@@ -496,7 +511,7 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
if s.checkUpdateSettingsDisabled() { if s.checkUpdateSettingsDisabled() {
return nil, gstatus.Errorf(codes.Unavailable, errUpdateSettingsDisabled) return nil, gstatus.Errorf(codes.Unavailable, errUpdateSettingsDisabled)
} }
policy := loadMDMPolicy() policy := s.mdmLoader.Load()
if err := rejectMDMManagedFieldConflicts(loginRequestMDMConflicts(msg, policy)); err != nil { if err := rejectMDMManagedFieldConflicts(loginRequestMDMConflicts(msg, policy)); err != nil {
return nil, err return nil, err
} }
@@ -1088,6 +1103,12 @@ func (s *Server) getConfig(activeProf *profilemanager.ActiveProfileState) (*prof
return nil, false, fmt.Errorf("failed to get config: %w", err) return nil, false, fmt.Errorf("failed to get config: %w", err)
} }
// Apply the daemon-owned MDM policy on top of the just-resolved
// Config. profilemanager's apply() initialises the policy to
// empty — the Loader lives outside Config, so this overlay step
// is driven externally here.
config.ApplyMDMPolicy(s.mdmLoader.Load())
return config, configExisted, nil return config, configExisted, nil
} }
@@ -1142,6 +1163,9 @@ func (s *Server) logoutFromProfile(ctx context.Context, profileName, username st
if err != nil { if err != nil {
return fmt.Errorf("profile '%s' not found", profileName) return fmt.Errorf("profile '%s' not found", profileName)
} }
// Honour any MDM-enforced ManagementURL when issuing the logout
// RPC: the user-stored value may have been overridden by policy.
config.ApplyMDMPolicy(s.mdmLoader.Load())
return s.sendLogoutRequestWithConfig(ctx, config) return s.sendLogoutRequestWithConfig(ctx, config)
} }
@@ -1574,6 +1598,11 @@ func (s *Server) GetConfig(ctx context.Context, req *proto.GetConfigRequest) (*p
log.Errorf("failed to get active profile config: %v", err) log.Errorf("failed to get active profile config: %v", err)
return nil, fmt.Errorf("failed to get active profile config: %w", err) return nil, fmt.Errorf("failed to get active profile config: %w", err)
} }
// Overlay the active MDM policy so the response's MDMManagedFields
// list reflects what the GUI / CLI must render as read-only.
// profilemanager.GetConfig itself returns a Config without the
// overlay (Loader lives outside profilemanager).
cfg.ApplyMDMPolicy(s.mdmLoader.Load())
managementURL := cfg.ManagementURL managementURL := cfg.ManagementURL
adminURL := cfg.AdminURL adminURL := cfg.AdminURL

View File

@@ -16,14 +16,40 @@ import (
"github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/proto"
) )
// withMDMPolicy temporarily overrides the server-package loadMDMPolicy hook // fakeMDMFetcher implements mdm.PolicyFetcher returning a pre-set
// so SetConfig observes the supplied Policy. Restores the original loader // policy map. Tests build one per Server instance to inject a
// at test cleanup. // scripted MDM overlay via a Loader rather than via package-level state.
func withMDMPolicy(t *testing.T, policy *mdm.Policy) { type fakeMDMFetcher struct{ values map[string]any }
func (f *fakeMDMFetcher) Fetch() map[string]any { return f.values }
// withMDMPolicy installs an mdm.Loader on the given Server whose
// loadPlatform returns the supplied Policy's underlying values. Use
// after setupServerWithProfile to inject the scripted policy the
// SetConfig / Login MDM gates will observe.
func withMDMPolicy(t *testing.T, s *Server, policy *mdm.Policy) {
t.Helper() t.Helper()
prev := loadMDMPolicy values := map[string]any{}
loadMDMPolicy = func() *mdm.Policy { return policy } if policy != nil {
t.Cleanup(func() { loadMDMPolicy = prev }) for _, k := range policy.ManagedKeys() {
if v, ok := policy.GetString(k); ok {
values[k] = v
continue
}
if v, ok := policy.GetBool(k); ok {
values[k] = v
continue
}
if v, ok := policy.GetInt(k); ok {
values[k] = v
continue
}
if v, ok := policy.GetStringSlice(k); ok {
values[k] = v
}
}
}
s.mdmLoader = mdm.NewLoader(&fakeMDMFetcher{values: values})
} }
// setupServerWithProfile mirrors the boilerplate of TestSetConfig_AllFieldsSaved: // setupServerWithProfile mirrors the boilerplate of TestSetConfig_AllFieldsSaved:
@@ -89,12 +115,11 @@ func extractViolation(t *testing.T, err error) *proto.MDMManagedFieldsViolation
} }
func TestSetConfig_MDMReject_SingleField(t *testing.T) { func TestSetConfig_MDMReject_SingleField(t *testing.T) {
withMDMPolicy(t, mdm.NewPolicy(map[string]any{ s, ctx, profName, username, _ := setupServerWithProfile(t)
withMDMPolicy(t, s, mdm.NewPolicy(map[string]any{
mdm.KeyManagementURL: "https://mdm.example.com:443", mdm.KeyManagementURL: "https://mdm.example.com:443",
})) }))
s, ctx, profName, username, _ := setupServerWithProfile(t)
_, err := s.SetConfig(ctx, &proto.SetConfigRequest{ _, err := s.SetConfig(ctx, &proto.SetConfigRequest{
ProfileName: profName, ProfileName: profName,
Username: username, Username: username,
@@ -106,13 +131,12 @@ func TestSetConfig_MDMReject_SingleField(t *testing.T) {
} }
func TestSetConfig_MDMReject_MultipleFields(t *testing.T) { func TestSetConfig_MDMReject_MultipleFields(t *testing.T) {
withMDMPolicy(t, mdm.NewPolicy(map[string]any{
mdm.KeyManagementURL: "https://mdm.example.com:443",
mdm.KeyBlockInbound: true,
mdm.KeyRosenpassEnabled: true,
}))
s, ctx, profName, username, _ := setupServerWithProfile(t) s, ctx, profName, username, _ := setupServerWithProfile(t)
withMDMPolicy(t, s, mdm.NewPolicy(map[string]any{
mdm.KeyManagementURL: "https://mdm.example.com:443",
mdm.KeyBlockInbound: true,
mdm.KeyRosenpassEnabled: true,
}))
blockInbound := false blockInbound := false
rosenpassEnabled := false rosenpassEnabled := false
@@ -137,12 +161,11 @@ func TestSetConfig_MDMReject_AllOrNothing(t *testing.T) {
// enforced field AND a non-enforced field (RosenpassEnabled). // enforced field AND a non-enforced field (RosenpassEnabled).
// The whole request must be rejected — non-conflicting fields are not // The whole request must be rejected — non-conflicting fields are not
// applied either. // applied either.
withMDMPolicy(t, mdm.NewPolicy(map[string]any{ s, ctx, profName, username, cfgPath := setupServerWithProfile(t)
withMDMPolicy(t, s, mdm.NewPolicy(map[string]any{
mdm.KeyManagementURL: "https://mdm.example.com:443", mdm.KeyManagementURL: "https://mdm.example.com:443",
})) }))
s, ctx, profName, username, cfgPath := setupServerWithProfile(t)
rosenpassEnabled := true rosenpassEnabled := true
_, err := s.SetConfig(ctx, &proto.SetConfigRequest{ _, err := s.SetConfig(ctx, &proto.SetConfigRequest{
ProfileName: profName, ProfileName: profName,
@@ -164,12 +187,11 @@ func TestSetConfig_MDMReject_AllOrNothing(t *testing.T) {
func TestSetConfig_MDMAllow_NonManagedFields(t *testing.T) { func TestSetConfig_MDMAllow_NonManagedFields(t *testing.T) {
// MDM enforces ManagementURL but the user only writes RosenpassEnabled. // MDM enforces ManagementURL but the user only writes RosenpassEnabled.
// Request must succeed. // Request must succeed.
withMDMPolicy(t, mdm.NewPolicy(map[string]any{ s, ctx, profName, username, _ := setupServerWithProfile(t)
withMDMPolicy(t, s, mdm.NewPolicy(map[string]any{
mdm.KeyManagementURL: "https://mdm.example.com:443", mdm.KeyManagementURL: "https://mdm.example.com:443",
})) }))
s, ctx, profName, username, _ := setupServerWithProfile(t)
rosenpassEnabled := true rosenpassEnabled := true
resp, err := s.SetConfig(ctx, &proto.SetConfigRequest{ resp, err := s.SetConfig(ctx, &proto.SetConfigRequest{
ProfileName: profName, ProfileName: profName,
@@ -183,9 +205,8 @@ func TestSetConfig_MDMAllow_NonManagedFields(t *testing.T) {
func TestSetConfig_MDMEmpty_NoEnforcement(t *testing.T) { func TestSetConfig_MDMEmpty_NoEnforcement(t *testing.T) {
// No MDM policy active: any field can be written. // No MDM policy active: any field can be written.
withMDMPolicy(t, mdm.NewPolicy(nil))
s, ctx, profName, username, _ := setupServerWithProfile(t) s, ctx, profName, username, _ := setupServerWithProfile(t)
withMDMPolicy(t, s, mdm.NewPolicy(nil))
resp, err := s.SetConfig(ctx, &proto.SetConfigRequest{ resp, err := s.SetConfig(ctx, &proto.SetConfigRequest{
ProfileName: profName, ProfileName: profName,