mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-19 21:29:09 +02:00
313 lines
9.9 KiB
Go
313 lines
9.9 KiB
Go
package profilemanager
|
|
|
|
import (
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/netbirdio/netbird/client/mdm"
|
|
)
|
|
|
|
// fakeFetcher implements mdm.PolicyFetcher returning a pre-set policy
|
|
// map. Test helper used to construct a Loader without touching the OS
|
|
// or any package-level state.
|
|
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.GetInt(k); ok {
|
|
values[k] = v
|
|
continue
|
|
}
|
|
if v, ok := policy.GetBool(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()
|
|
cfg, err := UpdateOrCreateConfig(input)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, cfg)
|
|
cfg.ApplyMDMPolicy(loaderFor(policy).Load())
|
|
return cfg
|
|
}
|
|
|
|
func TestApply_MDMEmpty_NoEnforcement(t *testing.T) {
|
|
cfg := configWithMDM(t, ConfigInput{
|
|
ConfigPath: filepath.Join(t.TempDir(), "config.json"),
|
|
}, mdm.NewPolicy(nil))
|
|
|
|
assert.True(t, cfg.Policy().IsEmpty(), "no MDM source ⇒ empty Policy")
|
|
assert.False(t, cfg.Policy().HasKey(mdm.KeyManagementURL))
|
|
assert.Empty(t, cfg.Policy().ManagedKeys())
|
|
|
|
// Default management URL still resolves.
|
|
assert.Equal(t, DefaultManagementURL, cfg.ManagementURL.String())
|
|
}
|
|
|
|
func TestApply_MDMOnly_OverridesDefaults(t *testing.T) {
|
|
const mdmURL = "https://corp.mdm.example.com:443"
|
|
|
|
cfg := configWithMDM(t, ConfigInput{
|
|
ConfigPath: filepath.Join(t.TempDir(), "config.json"),
|
|
}, mdm.NewPolicy(map[string]any{
|
|
mdm.KeyManagementURL: mdmURL,
|
|
mdm.KeyDisableClientRoutes: true,
|
|
mdm.KeyBlockInbound: true,
|
|
}))
|
|
|
|
assert.Equal(t, mdmURL, cfg.ManagementURL.String())
|
|
assert.True(t, cfg.DisableClientRoutes)
|
|
assert.True(t, cfg.BlockInbound)
|
|
|
|
assert.True(t, cfg.Policy().HasKey(mdm.KeyManagementURL))
|
|
assert.True(t, cfg.Policy().HasKey(mdm.KeyDisableClientRoutes))
|
|
assert.True(t, cfg.Policy().HasKey(mdm.KeyBlockInbound))
|
|
assert.False(t, cfg.Policy().HasKey(mdm.KeyAllowServerSSH))
|
|
}
|
|
|
|
func TestApply_MDMBeatsCLIInput(t *testing.T) {
|
|
const mdmURL = "https://mdm.example.com:443"
|
|
const cliURL = "https://cli.example.com:443"
|
|
|
|
cfg := configWithMDM(t, ConfigInput{
|
|
ConfigPath: filepath.Join(t.TempDir(), "config.json"),
|
|
ManagementURL: cliURL,
|
|
}, mdm.NewPolicy(map[string]any{
|
|
mdm.KeyManagementURL: mdmURL,
|
|
}))
|
|
|
|
// MDM wins over CLI-supplied management URL.
|
|
assert.Equal(t, mdmURL, cfg.ManagementURL.String())
|
|
assert.True(t, cfg.Policy().HasKey(mdm.KeyManagementURL))
|
|
}
|
|
|
|
func TestApply_MDMInvalidURL_KeepsPreviousValue(t *testing.T) {
|
|
cfg := configWithMDM(t, ConfigInput{
|
|
ConfigPath: filepath.Join(t.TempDir(), "config.json"),
|
|
}, mdm.NewPolicy(map[string]any{
|
|
mdm.KeyManagementURL: "not-a-url",
|
|
}))
|
|
|
|
// Invalid MDM URL is logged and skipped: default URL stays in place
|
|
// to keep the client functional.
|
|
assert.Equal(t, DefaultManagementURL, cfg.ManagementURL.String())
|
|
|
|
// But the key is still considered MDM-managed (admin intent is to
|
|
// enforce, daemon rejects user writes to this field — phase-1 scaffolding
|
|
// reflects this by keeping Policy.HasKey true even on parse failure).
|
|
assert.True(t, cfg.Policy().HasKey(mdm.KeyManagementURL))
|
|
}
|
|
|
|
func TestApply_MDMBoolKeysOverrideOnDiskValue(t *testing.T) {
|
|
tmp := filepath.Join(t.TempDir(), "config.json")
|
|
|
|
// Seed without MDM.
|
|
configWithMDM(t, ConfigInput{
|
|
ConfigPath: tmp,
|
|
DisableClientRoutes: boolPtr(false),
|
|
RosenpassEnabled: boolPtr(false),
|
|
}, mdm.NewPolicy(nil))
|
|
|
|
// Now enable MDM enforcement for these keys.
|
|
cfg := configWithMDM(t, ConfigInput{
|
|
ConfigPath: tmp,
|
|
}, mdm.NewPolicy(map[string]any{
|
|
mdm.KeyDisableClientRoutes: true,
|
|
mdm.KeyRosenpassEnabled: true,
|
|
}))
|
|
|
|
assert.True(t, cfg.DisableClientRoutes, "MDM override should flip on-disk false to true")
|
|
assert.True(t, cfg.RosenpassEnabled)
|
|
assert.True(t, cfg.Policy().HasKey(mdm.KeyDisableClientRoutes))
|
|
assert.True(t, cfg.Policy().HasKey(mdm.KeyRosenpassEnabled))
|
|
}
|
|
|
|
func TestApply_MDMVNCKeys(t *testing.T) {
|
|
tmp := filepath.Join(t.TempDir(), "config.json")
|
|
|
|
// Seed without MDM: VNC off, approval prompt on.
|
|
withMDMPolicy(t, mdm.NewPolicy(nil))
|
|
_, err := UpdateOrCreateConfig(ConfigInput{
|
|
ConfigPath: tmp,
|
|
ServerVNCAllowed: boolPtr(false),
|
|
DisableVNCApproval: boolPtr(false),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// MDM enforces VNC on and disables the approval prompt.
|
|
withMDMPolicy(t, mdm.NewPolicy(map[string]any{
|
|
mdm.KeyAllowServerVNC: true,
|
|
mdm.KeyDisableVNCApproval: true,
|
|
}))
|
|
|
|
cfg, err := UpdateOrCreateConfig(ConfigInput{ConfigPath: tmp})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, cfg)
|
|
|
|
require.NotNil(t, cfg.ServerVNCAllowed)
|
|
assert.True(t, *cfg.ServerVNCAllowed, "MDM override should flip on-disk false to true")
|
|
require.NotNil(t, cfg.DisableVNCApproval)
|
|
assert.True(t, *cfg.DisableVNCApproval)
|
|
assert.True(t, cfg.Policy().HasKey(mdm.KeyAllowServerVNC))
|
|
assert.True(t, cfg.Policy().HasKey(mdm.KeyDisableVNCApproval))
|
|
}
|
|
|
|
func TestApply_MDMLocalMetrics(t *testing.T) {
|
|
tmp := filepath.Join(t.TempDir(), "config.json")
|
|
|
|
// Seed without MDM.
|
|
configWithMDM(t, ConfigInput{
|
|
ConfigPath: tmp,
|
|
LocalMetricsEnabled: boolPtr(false),
|
|
}, mdm.NewPolicy(nil))
|
|
|
|
// Now enable MDM enforcement for these keys.
|
|
cfg := configWithMDM(t, ConfigInput{
|
|
ConfigPath: tmp,
|
|
}, mdm.NewPolicy(map[string]any{
|
|
mdm.KeyEnableLocalMetrics: true,
|
|
mdm.KeyLocalMetricsAddress: "127.0.0.1:9292",
|
|
}))
|
|
|
|
assert.True(t, cfg.LocalMetricsEnabled, "MDM override should flip on-disk false to true")
|
|
assert.Equal(t, "127.0.0.1:9292", cfg.LocalMetricsAddress)
|
|
assert.True(t, cfg.Policy().HasKey(mdm.KeyEnableLocalMetrics))
|
|
assert.True(t, cfg.Policy().HasKey(mdm.KeyLocalMetricsAddress))
|
|
}
|
|
|
|
func TestApply_MDMLazyConnection(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
raw any
|
|
want string
|
|
}{
|
|
{"native true", true, "on"},
|
|
{"native false", false, "off"},
|
|
{"string on", "on", "on"},
|
|
{"string off", "off", "off"},
|
|
{"string yes", "yes", "on"},
|
|
{"string no", "no", "off"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
cfg := configWithMDM(t, ConfigInput{
|
|
ConfigPath: filepath.Join(t.TempDir(), "config.json"),
|
|
}, mdm.NewPolicy(map[string]any{
|
|
mdm.KeyLazyConnection: c.raw,
|
|
}))
|
|
|
|
assert.Equal(t, c.want, cfg.LazyConnection)
|
|
assert.True(t, cfg.Policy().HasKey(mdm.KeyLazyConnection))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApply_MDMPreSharedKeyRedactionSentinelRejected(t *testing.T) {
|
|
const maskSentinel = mdm.PreSharedKeyRedactedSentinel
|
|
|
|
cfg := configWithMDM(t, ConfigInput{
|
|
ConfigPath: filepath.Join(t.TempDir(), "config.json"),
|
|
}, mdm.NewPolicy(map[string]any{
|
|
mdm.KeyPreSharedKey: maskSentinel,
|
|
}))
|
|
|
|
// Mask sentinel must not be persisted as the actual PSK.
|
|
assert.NotEqual(t, maskSentinel, cfg.PreSharedKey)
|
|
// Key still marked managed so user writes are still rejected.
|
|
assert.True(t, cfg.Policy().HasKey(mdm.KeyPreSharedKey))
|
|
}
|
|
|
|
func TestMDMConflicts_PreSharedKey(t *testing.T) {
|
|
policy := mdm.NewPolicy(map[string]any{
|
|
mdm.KeyPreSharedKey: "mdm-enforced-psk",
|
|
})
|
|
empty := ""
|
|
sentinel := mdm.PreSharedKeyRedactedSentinel
|
|
same := "mdm-enforced-psk"
|
|
other := "user-psk"
|
|
|
|
tests := []struct {
|
|
name string
|
|
psk *string
|
|
want []string
|
|
}{
|
|
{name: "unset", psk: nil, want: nil},
|
|
{name: "explicit empty", psk: &empty, want: []string{mdm.KeyPreSharedKey}},
|
|
{name: "sentinel echo", psk: &sentinel, want: nil},
|
|
{name: "same value", psk: &same, want: nil},
|
|
{name: "divergent", psk: &other, want: []string{mdm.KeyPreSharedKey}},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
assert.Equal(t, tc.want, MDMConflicts(ConfigInput{PreSharedKey: tc.psk}, policy))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMDMConflicts_RemoteJobsAndLocalMetrics(t *testing.T) {
|
|
policy := mdm.NewPolicy(map[string]any{
|
|
mdm.KeyRemoteJobsAllowed: false,
|
|
mdm.KeyEnableLocalMetrics: true,
|
|
mdm.KeyLocalMetricsAddress: "127.0.0.1:9999",
|
|
})
|
|
sameAddr := "127.0.0.1:9999"
|
|
otherAddr := "0.0.0.0:9999"
|
|
emptyAddr := ""
|
|
|
|
tests := []struct {
|
|
name string
|
|
input ConfigInput
|
|
want []string
|
|
}{
|
|
{name: "unset", input: ConfigInput{}, want: nil},
|
|
{name: "echo", input: ConfigInput{
|
|
RemoteJobsAllowed: boolPtr(false),
|
|
LocalMetricsEnabled: boolPtr(true),
|
|
LocalMetricsAddress: &sameAddr,
|
|
}, want: nil},
|
|
{name: "remote jobs divergent", input: ConfigInput{RemoteJobsAllowed: boolPtr(true)}, want: []string{mdm.KeyRemoteJobsAllowed}},
|
|
{name: "metrics disabled", input: ConfigInput{LocalMetricsEnabled: boolPtr(false)}, want: []string{mdm.KeyEnableLocalMetrics}},
|
|
{name: "metrics address divergent", input: ConfigInput{LocalMetricsAddress: &otherAddr}, want: []string{mdm.KeyLocalMetricsAddress}},
|
|
{name: "metrics address explicit empty", input: ConfigInput{LocalMetricsAddress: &emptyAddr}, want: []string{mdm.KeyLocalMetricsAddress}},
|
|
{name: "all divergent", input: ConfigInput{
|
|
RemoteJobsAllowed: boolPtr(true),
|
|
LocalMetricsEnabled: boolPtr(false),
|
|
LocalMetricsAddress: &otherAddr,
|
|
}, want: []string{mdm.KeyRemoteJobsAllowed, mdm.KeyEnableLocalMetrics, mdm.KeyLocalMetricsAddress}},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
assert.Equal(t, tc.want, MDMConflicts(tc.input, policy))
|
|
})
|
|
}
|
|
}
|
|
|
|
func boolPtr(b bool) *bool { return &b }
|