[client] Resolve the merge conflicts left in the tree

262ce8c3b landed with the conflict markers still in it, so client/server and
the iOS SDK did not compile. Four regions, resolved as follows.

client/server/mdm.go — main moved the MDM conflict-check machinery into the
mdm package (mdm.ResolveConflicts, mdm.ConflictBool, mdm.ConflictURL, ...).
This branch had edited the local copies, which are now dead: dropped, along
with the profilemanager import that only the local conflictURL needed.

client/server/server.go, Login gate — this branch's value-aware gate stays
(the point of the PR: refuse a real divergence, let a restatement through),
so main's presence-based `loginRequestHasConfigOverrides` block goes; that
helper no longer exists here anyway. Main's other change in the same lines
is real and kept: the MDM policy now comes from the daemon-owned
s.mdmLoader.Load() instead of the package-level loadMDMPolicy, which main
removed. The stale call right below the conflict was the reason the file
would not have compiled even with the markers gone.

client/server/server.go, getConfig — both sides add something and both are
needed. The identity is provisioned and persisted first, then the MDM
overlay is applied, so what reaches disk stays the profile's own config: the
overlay is runtime-only and re-derived on every load.

client/ios/NetBirdSDK/client.go — main reworked SetConfigFromJSON to store
the JSON and re-parse it on each load, which is the shape kept; the parse is
now only a validity check, and this branch's reason for it (a document with
no peer identity is refused, not just an unparseable one) moves into that
comment.

client/server/update_settings_gate_test.go — follows the sentinel constant
to its new home, mdm.PreSharedKeyRedactedSentinel.
This commit is contained in:
riccardom
2026-09-08 14:03:15 +02:00
parent 262ce8c3b2
commit 97b9a18ef6
4 changed files with 15 additions and 149 deletions
+6 -10
View File
@@ -130,17 +130,13 @@ func NewClient(cfgFile, stateFile, cacheDir, logFilePath, deviceName string, osV
// SetConfigFromJSON stores the JSON config that later loads resolve instead of the config file (tvOS).
func (c *Client) SetConfigFromJSON(jsonStr string) error {
<<<<<<< HEAD
cfg, err := profilemanager.ConfigFromJSON(jsonStr)
if err != nil {
// Not only a parse error any more: a document with no peer identity is
// refused, because Run() would otherwise connect as a peer whose key
// this SDK has no way to hand back to the caller's store.
log.Errorf("SetConfigFromJSON: failed to load config JSON: %v", err)
=======
// Parsed only to reject a bad document early; the JSON itself is what is
// stored, and every load re-parses it. Not only a parse error any more: a
// document with no peer identity is refused too, because Run() would
// otherwise connect as a peer whose key this SDK has no way to hand back
// to the caller's store.
if _, err := profilemanager.ConfigFromJSON(jsonStr); err != nil {
log.Errorf("SetConfigFromJSON: failed to parse config JSON: %v", err)
>>>>>>> origin/main
log.Errorf("SetConfigFromJSON: failed to load config JSON: %v", err)
return err
}
c.preloadedConfigJSON.Store(&jsonStr)
-119
View File
@@ -9,7 +9,6 @@ import (
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/mdm"
"github.com/netbirdio/netbird/client/proto"
)
@@ -146,124 +145,6 @@ func (s *Server) restartEngineForMDMLocked() error {
return nil
}
<<<<<<< HEAD
// conflictBool builds a conflictCheck for a boolean MDM key. If p is nil
// the field is treated as matching (no override requested); otherwise the
// check returns true only when the policy contains the key and its
// boolean value equals *p.
func conflictBool(key string, p *bool) conflictCheck {
return conflictCheck{
key: key,
check: func(pol *mdm.Policy) bool {
if p == nil {
return true // absent → match by definition
}
want, ok := pol.GetBool(key)
return ok && want == *p
},
}
}
// conflictURL is conflictString for URL-typed keys: both sides are compared as
// endpoints (profilemanager.SameServiceURL), so an implicit default port, a
// trailing slash or a different host case is not read as a divergence from the
// policy. A value that does not parse as a URL falls back to string equality,
// which is the strictest thing left to do with it.
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)
if !ok {
return false
}
wantURL, wantErr := profilemanager.ParseServiceURL(key, want)
gotURL, gotErr := profilemanager.ParseServiceURL(key, got)
if wantErr != nil || gotErr != nil {
return want == got
}
return profilemanager.SameServiceURL(wantURL, gotURL)
},
}
}
// 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
// value equals got.
func conflictString(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 && want == got
},
}
}
// conflictStringPtr is conflictString for optional proto fields, where an
// explicit empty value is still a request to change the setting. If p is
// nil the field is treated as matching (no override requested); otherwise
// the check returns true only when the policy contains the key and its
// value equals *p.
func conflictStringPtr(key string, p *string) conflictCheck {
return conflictCheck{
key: key,
check: func(pol *mdm.Policy) bool {
if p == nil {
return true
}
want, ok := pol.GetString(key)
return ok && want == *p
},
}
}
// conflictInt64 builds a conflictCheck for an integer MDM key. If p is
// nil the field is treated as matching; otherwise the check returns
// true only when the policy contains the key and its int value equals *p.
func conflictInt64(key string, p *int64) conflictCheck {
return conflictCheck{
key: key,
check: func(pol *mdm.Policy) bool {
if p == nil {
return true
}
want, ok := pol.GetInt(key)
return ok && want == *p
},
}
}
// resolveConflicts walks the per-field checks against the active MDM
// policy and returns the names of keys whose requested value diverges
// from the policy-enforced value. Keys not present in the policy are
// skipped silently (the gate fires only for keys the admin has
// actually pushed). Returns nil for an empty policy.
func resolveConflicts(policy *mdm.Policy, checks []conflictCheck) []string {
if policy.IsEmpty() {
return nil
}
var conflicts []string
for _, c := range checks {
if !policy.HasKey(c.key) {
continue
}
if !c.check(policy) {
conflicts = append(conflicts, c.key)
}
}
return conflicts
}
=======
>>>>>>> origin/main
// mdmManagedFieldConflicts returns the names of MDM-managed keys whose
// requested value in the SetConfigRequest differs from the MDM-enforced
// value. A field set to the same value the policy already enforces is
+7 -19
View File
@@ -669,7 +669,6 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
// command `netbird up --management-url=X` (which falls through to
// Login when SetConfig is rejected — see cmd/up.go) would silently
// bypass `--disable-update-settings` and any MDM policy.
<<<<<<< HEAD
//
// The update-settings gate is value-aware, as in SetConfig: it looks at
// what a login would actually persist (loginOverridesInput) and refuses
@@ -679,19 +678,9 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
// NB_MANAGEMENT_URL, working with the kill switch on.
if s.checkUpdateSettingsDisabled() && configChangeRequested(stored, loginOverridesInput(msg)) {
return nil, gstatus.Errorf(codes.FailedPrecondition, errUpdateSettingsDisabled)
=======
if loginRequestHasConfigOverrides(msg) {
if s.checkUpdateSettingsDisabled() {
return nil, gstatus.Errorf(codes.Unavailable, errUpdateSettingsDisabled)
}
policy := s.mdmLoader.Load()
if err := rejectMDMManagedFieldConflicts(loginRequestMDMConflicts(msg, policy)); err != nil {
return nil, err
}
>>>>>>> origin/main
}
policy := loadMDMPolicy()
policy := s.mdmLoader.Load()
if err := rejectMDMManagedFieldConflicts(loginRequestMDMConflicts(msg, policy)); err != nil {
return nil, err
}
@@ -1533,7 +1522,6 @@ func (s *Server) getConfig(activeProf *profilemanager.ActiveProfileState) (*prof
return nil, false, fmt.Errorf("failed to get config: %w", err)
}
<<<<<<< HEAD
// This is the daemon's provisioning point: the config resolved here is the
// one the peer runs with, so it needs the keys that identify it, and those
// have to reach disk — a key that stays in memory would come back different
@@ -1549,13 +1537,13 @@ func (s *Server) getConfig(activeProf *profilemanager.ActiveProfileState) (*prof
return nil, false, fmt.Errorf("write out profile 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.
// 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.
// After the write above, on purpose: the overlay is runtime-only and
// re-derived on every load, so the file keeps the profile's own values.
config.ApplyMDMPolicy(s.mdmLoader.Load())
>>>>>>> origin/main
return config, configExisted, nil
}
+2 -1
View File
@@ -12,6 +12,7 @@ import (
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/mdm"
"github.com/netbirdio/netbird/client/proto"
)
@@ -156,7 +157,7 @@ func TestLoginGateDecision(t *testing.T) {
stored, err := profilemanager.GetExistingConfig(seedProfileConfig(t, storedManagementURL, "stored-key"))
require.NoError(t, err)
redacted := preSharedKeyRedactedSentinel
redacted := mdm.PreSharedKeyRedactedSentinel
empty := ""
sameKey := "stored-key"
otherKey := "other-key"