diff --git a/client/server/mdm.go b/client/server/mdm.go index db7db2759..1e1005e05 100644 --- a/client/server/mdm.go +++ b/client/server/mdm.go @@ -3,6 +3,7 @@ package server import ( "context" "fmt" + "net/url" "time" log "github.com/sirupsen/logrus" @@ -181,6 +182,37 @@ func conflictBool(key string, p *bool) conflictCheck { } } +func canonicalURL(s string) string { + u, err := url.ParseRequestURI(s) + if err != nil { + return s + } + if u.Port() == "" { + switch u.Scheme { + case "https": + u.Host += ":443" + case "http": + u.Host += ":80" + } + } + return u.String() +} + +// conflictURL is conflictString for URL-typed keys: both sides are +// normalized via canonicalURL before comparison. +func conflictURL(key, got string) conflictCheck { + return conflictCheck{ + key: key, + check: func(pol *mdm.Policy) bool { + if got == "" { + return true + } + want, ok := pol.GetString(key) + return ok && canonicalURL(want) == canonicalURL(got) + }, + } +} + // conflictString builds a conflictCheck for a string MDM key. An empty // `got` is treated as "field not set" (no override requested); otherwise // the check returns true only when the policy contains the key and its @@ -256,7 +288,7 @@ func mdmManagedFieldConflicts(msg *proto.SetConfigRequest, policy *mdm.Policy) [ } return resolveConflicts(policy, []conflictCheck{ - conflictString(mdm.KeyManagementURL, msg.ManagementUrl), + conflictURL(mdm.KeyManagementURL, msg.ManagementUrl), conflictString(mdm.KeyPreSharedKey, pskGot), conflictBool(mdm.KeyRosenpassEnabled, msg.RosenpassEnabled), conflictBool(mdm.KeyRosenpassPermissive, msg.RosenpassPermissive), @@ -377,7 +409,7 @@ func loginRequestMDMConflicts(msg *proto.LoginRequest, policy *mdm.Policy) []str } return resolveConflicts(policy, []conflictCheck{ - conflictString(mdm.KeyManagementURL, msg.ManagementUrl), + conflictURL(mdm.KeyManagementURL, msg.ManagementUrl), conflictString(mdm.KeyPreSharedKey, pskGot), conflictBool(mdm.KeyRosenpassEnabled, msg.RosenpassEnabled), conflictBool(mdm.KeyRosenpassPermissive, msg.RosenpassPermissive), diff --git a/client/server/setconfig_mdm_test.go b/client/server/setconfig_mdm_test.go index 9818f9fdf..9baf16136 100644 --- a/client/server/setconfig_mdm_test.go +++ b/client/server/setconfig_mdm_test.go @@ -181,6 +181,43 @@ func TestSetConfig_MDMAllow_NonManagedFields(t *testing.T) { require.NotNil(t, resp) } +// TestSetConfig_MDMAllow_ManagementURLPortNormalized covers the +// regression from discussion #6483: MDM URL without explicit port vs +// UI echo with the parseURL-appended default port must be treated as +// a no-op echo, not a conflict. +func TestSetConfig_MDMAllow_ManagementURLPortNormalized(t *testing.T) { + tests := []struct { + name string + mdmURL string + submitURL string + }{ + {"policy_no_port_submit_with_443", "https://netbird.corp.example", "https://netbird.corp.example:443"}, + {"policy_with_443_submit_no_port", "https://netbird.corp.example:443", "https://netbird.corp.example"}, + {"http_policy_no_port_submit_with_80", "http://netbird.corp.example", "http://netbird.corp.example:80"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + withMDMPolicy(t, mdm.NewPolicy(map[string]any{ + mdm.KeyManagementURL: tc.mdmURL, + })) + + s, ctx, profName, username, _ := setupServerWithProfile(t) + + rosenpassEnabled := true + resp, err := s.SetConfig(ctx, &proto.SetConfigRequest{ + ProfileName: profName, + Username: username, + ManagementUrl: tc.submitURL, + RosenpassEnabled: &rosenpassEnabled, + }) + + require.NoError(t, err, "port-normalized URL echo must not trip MDM conflict gate") + require.NotNil(t, resp) + }) + } +} + func TestSetConfig_MDMEmpty_NoEnforcement(t *testing.T) { // No MDM policy active: any field can be written. withMDMPolicy(t, mdm.NewPolicy(nil))