mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-11 17:29:07 +02:00
Merge branch 'main' into embedded-vnc
This commit is contained in:
+40
-191
@@ -3,7 +3,6 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -14,28 +13,6 @@ import (
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
)
|
||||
|
||||
// preSharedKeyRedactedSentinel is the value GetConfig returns in place
|
||||
// of an actual PSK, so a UI that round-trips the field back to the
|
||||
// daemon (via SetConfig / Login) can be distinguished from a deliberate
|
||||
// override. Any incoming PSK that equals this sentinel is treated as
|
||||
// a no-op echo, never as a conflict with the policy.
|
||||
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
|
||||
// the incoming request and the corresponding MDM-enforced value. It
|
||||
// runs only when the field was actually set in the request (presence
|
||||
// already filtered upstream); ok=true reports the policy value, ok=false
|
||||
// means the policy is silent on the key — both are treated as conflicts
|
||||
// to be safe (an MDM key declared as managed must hold a value).
|
||||
type conflictCheck struct {
|
||||
key string
|
||||
check func(*mdm.Policy) (match bool)
|
||||
}
|
||||
|
||||
// onMDMPolicyChange is invoked by the MDM reload ticker every time the
|
||||
// OS-native managed-config store reports a diff vs the last observation.
|
||||
//
|
||||
@@ -168,126 +145,6 @@ func (s *Server) restartEngineForMDMLocked() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -301,29 +158,27 @@ func mdmManagedFieldConflicts(msg *proto.SetConfigRequest, policy *mdm.Policy) [
|
||||
return nil
|
||||
}
|
||||
|
||||
// PSK round-trip echo: collapse the sentinel to empty so the
|
||||
// shared check treats it as "field not set".
|
||||
pskGot := ""
|
||||
if msg.OptionalPreSharedKey != nil && *msg.OptionalPreSharedKey != preSharedKeyRedactedSentinel {
|
||||
pskGot = *msg.OptionalPreSharedKey
|
||||
pskGot := msg.OptionalPreSharedKey
|
||||
if pskGot != nil && *pskGot == mdm.PreSharedKeyRedactedSentinel {
|
||||
pskGot = nil
|
||||
}
|
||||
|
||||
return resolveConflicts(policy, []conflictCheck{
|
||||
conflictURL(mdm.KeyManagementURL, msg.ManagementUrl),
|
||||
conflictString(mdm.KeyPreSharedKey, pskGot),
|
||||
conflictBool(mdm.KeyRosenpassEnabled, msg.RosenpassEnabled),
|
||||
conflictBool(mdm.KeyRosenpassPermissive, msg.RosenpassPermissive),
|
||||
conflictBool(mdm.KeyDisableAutoConnect, msg.DisableAutoConnect),
|
||||
conflictBool(mdm.KeyAllowServerSSH, msg.ServerSSHAllowed),
|
||||
conflictBool(mdm.KeyRemoteJobsAllowed, msg.RemoteJobsAllowed),
|
||||
conflictBool(mdm.KeyAllowServerVNC, msg.ServerVNCAllowed),
|
||||
conflictBool(mdm.KeyDisableVNCApproval, msg.DisableVNCApproval),
|
||||
conflictBool(mdm.KeyDisableClientRoutes, msg.DisableClientRoutes),
|
||||
conflictBool(mdm.KeyDisableServerRoutes, msg.DisableServerRoutes),
|
||||
conflictBool(mdm.KeyBlockInbound, msg.BlockInbound),
|
||||
conflictInt64(mdm.KeyWireguardPort, msg.WireguardPort),
|
||||
conflictBool(mdm.KeyEnableLocalMetrics, msg.EnableLocalMetrics),
|
||||
conflictStringPtr(mdm.KeyLocalMetricsAddress, msg.LocalMetricsAddress),
|
||||
return mdm.ResolveConflicts(policy, []mdm.ConflictCheck{
|
||||
mdm.ConflictURL(mdm.KeyManagementURL, msg.ManagementUrl),
|
||||
mdm.ConflictStringPtr(mdm.KeyPreSharedKey, pskGot),
|
||||
mdm.ConflictBool(mdm.KeyRosenpassEnabled, msg.RosenpassEnabled),
|
||||
mdm.ConflictBool(mdm.KeyRosenpassPermissive, msg.RosenpassPermissive),
|
||||
mdm.ConflictBool(mdm.KeyDisableAutoConnect, msg.DisableAutoConnect),
|
||||
mdm.ConflictBool(mdm.KeyAllowServerSSH, msg.ServerSSHAllowed),
|
||||
mdm.ConflictBool(mdm.KeyRemoteJobsAllowed, msg.RemoteJobsAllowed),
|
||||
mdm.ConflictBool(mdm.KeyAllowServerVNC, msg.ServerVNCAllowed),
|
||||
mdm.ConflictBool(mdm.KeyDisableVNCApproval, msg.DisableVNCApproval),
|
||||
mdm.ConflictBool(mdm.KeyDisableClientRoutes, msg.DisableClientRoutes),
|
||||
mdm.ConflictBool(mdm.KeyDisableServerRoutes, msg.DisableServerRoutes),
|
||||
mdm.ConflictBool(mdm.KeyBlockInbound, msg.BlockInbound),
|
||||
mdm.ConflictInt64(mdm.KeyWireguardPort, msg.WireguardPort),
|
||||
mdm.ConflictBool(mdm.KeyEnableLocalMetrics, msg.EnableLocalMetrics),
|
||||
mdm.ConflictStringPtr(mdm.KeyLocalMetricsAddress, msg.LocalMetricsAddress),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -430,36 +285,30 @@ func loginRequestMDMConflicts(msg *proto.LoginRequest, policy *mdm.Policy) []str
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collapse the two PSK fields + the redaction sentinel down to a
|
||||
// single "got" string the shared check can compare against the
|
||||
// policy: OptionalPreSharedKey wins if set; PreSharedKey (deprecated)
|
||||
// is the fallback; sentinel echo is treated as "field not set".
|
||||
pskGot := ""
|
||||
if msg.OptionalPreSharedKey != nil {
|
||||
pskGot = *msg.OptionalPreSharedKey
|
||||
} else if msg.PreSharedKey != "" { //nolint:staticcheck // SA1019: legacy proto field still accepted by Login
|
||||
pskGot = msg.PreSharedKey //nolint:staticcheck // SA1019
|
||||
pskGot := msg.OptionalPreSharedKey
|
||||
if pskGot == nil && msg.PreSharedKey != "" { //nolint:staticcheck // SA1019: legacy proto field still accepted by Login
|
||||
pskGot = &msg.PreSharedKey //nolint:staticcheck // SA1019
|
||||
}
|
||||
if pskGot == preSharedKeyRedactedSentinel {
|
||||
pskGot = ""
|
||||
if pskGot != nil && *pskGot == mdm.PreSharedKeyRedactedSentinel {
|
||||
pskGot = nil
|
||||
}
|
||||
|
||||
return resolveConflicts(policy, []conflictCheck{
|
||||
conflictURL(mdm.KeyManagementURL, msg.ManagementUrl),
|
||||
conflictString(mdm.KeyPreSharedKey, pskGot),
|
||||
conflictBool(mdm.KeyRosenpassEnabled, msg.RosenpassEnabled),
|
||||
conflictBool(mdm.KeyRosenpassPermissive, msg.RosenpassPermissive),
|
||||
conflictBool(mdm.KeyDisableAutoConnect, msg.DisableAutoConnect),
|
||||
conflictBool(mdm.KeyAllowServerSSH, msg.ServerSSHAllowed),
|
||||
conflictBool(mdm.KeyRemoteJobsAllowed, msg.RemoteJobsAllowed),
|
||||
conflictBool(mdm.KeyAllowServerVNC, msg.ServerVNCAllowed),
|
||||
conflictBool(mdm.KeyDisableVNCApproval, msg.DisableVNCApproval),
|
||||
conflictBool(mdm.KeyDisableClientRoutes, msg.DisableClientRoutes),
|
||||
conflictBool(mdm.KeyDisableServerRoutes, msg.DisableServerRoutes),
|
||||
conflictBool(mdm.KeyBlockInbound, msg.BlockInbound),
|
||||
conflictInt64(mdm.KeyWireguardPort, msg.WireguardPort),
|
||||
conflictBool(mdm.KeyEnableLocalMetrics, msg.EnableLocalMetrics),
|
||||
conflictStringPtr(mdm.KeyLocalMetricsAddress, msg.LocalMetricsAddress),
|
||||
return mdm.ResolveConflicts(policy, []mdm.ConflictCheck{
|
||||
mdm.ConflictURL(mdm.KeyManagementURL, msg.ManagementUrl),
|
||||
mdm.ConflictStringPtr(mdm.KeyPreSharedKey, pskGot),
|
||||
mdm.ConflictBool(mdm.KeyRosenpassEnabled, msg.RosenpassEnabled),
|
||||
mdm.ConflictBool(mdm.KeyRosenpassPermissive, msg.RosenpassPermissive),
|
||||
mdm.ConflictBool(mdm.KeyDisableAutoConnect, msg.DisableAutoConnect),
|
||||
mdm.ConflictBool(mdm.KeyAllowServerSSH, msg.ServerSSHAllowed),
|
||||
mdm.ConflictBool(mdm.KeyRemoteJobsAllowed, msg.RemoteJobsAllowed),
|
||||
mdm.ConflictBool(mdm.KeyAllowServerVNC, msg.ServerVNCAllowed),
|
||||
mdm.ConflictBool(mdm.KeyDisableVNCApproval, msg.DisableVNCApproval),
|
||||
mdm.ConflictBool(mdm.KeyDisableClientRoutes, msg.DisableClientRoutes),
|
||||
mdm.ConflictBool(mdm.KeyDisableServerRoutes, msg.DisableServerRoutes),
|
||||
mdm.ConflictBool(mdm.KeyBlockInbound, msg.BlockInbound),
|
||||
mdm.ConflictInt64(mdm.KeyWireguardPort, msg.WireguardPort),
|
||||
mdm.ConflictBool(mdm.KeyEnableLocalMetrics, msg.EnableLocalMetrics),
|
||||
mdm.ConflictStringPtr(mdm.KeyLocalMetricsAddress, msg.LocalMetricsAddress),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+32
-3
@@ -142,6 +142,15 @@ type Server struct {
|
||||
// stopped by the rootCtx cancellation.
|
||||
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
|
||||
|
||||
jwtCache *jwtCache
|
||||
@@ -250,8 +259,14 @@ func (s *Server) Start() error {
|
||||
// Runs re-resolves Config (re-running profilemanager.Config.apply which
|
||||
// applies the freshly-read MDM policy as the last layer) and brings
|
||||
// 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 {
|
||||
s.mdmTicker = mdm.NewTicker(mdm.DefaultReloadInterval)
|
||||
s.mdmTicker = mdm.NewTicker(mdm.DefaultReloadInterval, s.mdmLoader)
|
||||
go s.mdmTicker.Run(s.rootCtx, s.onMDMPolicyChange)
|
||||
}
|
||||
|
||||
@@ -497,7 +512,7 @@ func (s *Server) SetConfig(callerCtx context.Context, msg *proto.SetConfigReques
|
||||
// by the active MDM policy. The error carries an MDMManagedFields-
|
||||
// Violation detail listing the offending key names. Non-conflicting
|
||||
// fields in the same request are not applied either.
|
||||
policy := loadMDMPolicy()
|
||||
policy := s.mdmLoader.Load()
|
||||
if err := rejectMDMManagedFieldConflicts(mdmManagedFieldConflicts(msg, policy)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -642,7 +657,7 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
|
||||
if s.checkUpdateSettingsDisabled() {
|
||||
return nil, gstatus.Errorf(codes.Unavailable, errUpdateSettingsDisabled)
|
||||
}
|
||||
policy := loadMDMPolicy()
|
||||
policy := s.mdmLoader.Load()
|
||||
if err := rejectMDMManagedFieldConflicts(loginRequestMDMConflicts(msg, policy)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1493,6 +1508,12 @@ func (s *Server) getConfig(activeProf *profilemanager.ActiveProfileState) (*prof
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1549,6 +1570,9 @@ func (s *Server) logoutFromProfile(ctx context.Context, profile *profilemanager.
|
||||
if err != nil {
|
||||
return fmt.Errorf("profile '%s' not found", profile.ID)
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
@@ -2257,6 +2281,11 @@ func (s *Server) GetConfig(ctx context.Context, req *proto.GetConfigRequest) (*p
|
||||
log.Errorf("failed to get active profile config: %v", 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
|
||||
adminURL := cfg.AdminURL
|
||||
|
||||
|
||||
@@ -16,14 +16,40 @@ import (
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
)
|
||||
|
||||
// withMDMPolicy temporarily overrides the server-package loadMDMPolicy hook
|
||||
// so SetConfig observes the supplied Policy. Restores the original loader
|
||||
// at test cleanup.
|
||||
func withMDMPolicy(t *testing.T, policy *mdm.Policy) {
|
||||
// fakeMDMFetcher implements mdm.PolicyFetcher returning a pre-set
|
||||
// policy map. Tests build one per Server instance to inject a
|
||||
// scripted MDM overlay via a Loader rather than via package-level state.
|
||||
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()
|
||||
prev := loadMDMPolicy
|
||||
loadMDMPolicy = func() *mdm.Policy { return policy }
|
||||
t.Cleanup(func() { loadMDMPolicy = prev })
|
||||
values := map[string]any{}
|
||||
if policy != nil {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
s.mdmLoader = mdm.NewLoader(&fakeMDMFetcher{values: values})
|
||||
}
|
||||
|
||||
// setupServerWithProfile mirrors the boilerplate of TestSetConfig_AllFieldsSaved:
|
||||
@@ -93,12 +119,11 @@ func extractViolation(t *testing.T, err error) *proto.MDMManagedFieldsViolation
|
||||
}
|
||||
|
||||
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",
|
||||
}))
|
||||
|
||||
s, ctx, profName, username, _ := setupServerWithProfile(t)
|
||||
|
||||
_, err := s.SetConfig(ctx, &proto.SetConfigRequest{
|
||||
ProfileName: profName,
|
||||
Username: username,
|
||||
@@ -110,13 +135,12 @@ func TestSetConfig_MDMReject_SingleField(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSetConfig_MDMReject_VNCFields(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.KeyAllowServerVNC: true,
|
||||
mdm.KeyDisableVNCApproval: false,
|
||||
}))
|
||||
|
||||
s, ctx, profName, username, _ := setupServerWithProfile(t)
|
||||
|
||||
vncAllowed := false
|
||||
disableApproval := true
|
||||
_, err := s.SetConfig(ctx, &proto.SetConfigRequest{
|
||||
@@ -134,14 +158,13 @@ func TestSetConfig_MDMReject_VNCFields(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSetConfig_MDMReject_MultipleFields(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.KeyBlockInbound: true,
|
||||
mdm.KeyRosenpassEnabled: true,
|
||||
}))
|
||||
|
||||
s, ctx, profName, username, _ := setupServerWithProfile(t)
|
||||
|
||||
blockInbound := false
|
||||
rosenpassEnabled := false
|
||||
_, err := s.SetConfig(ctx, &proto.SetConfigRequest{
|
||||
@@ -161,13 +184,12 @@ func TestSetConfig_MDMReject_MultipleFields(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSetConfig_MDMReject_LocalMetrics(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.KeyEnableLocalMetrics: true,
|
||||
mdm.KeyLocalMetricsAddress: "127.0.0.1:9191",
|
||||
}))
|
||||
|
||||
s, ctx, profName, username, _ := setupServerWithProfile(t)
|
||||
|
||||
enabled := false
|
||||
addr := "0.0.0.0:9999"
|
||||
_, err := s.SetConfig(ctx, &proto.SetConfigRequest{
|
||||
@@ -188,12 +210,11 @@ func TestSetConfig_MDMReject_LocalMetrics(t *testing.T) {
|
||||
// (the manager falls back to the default), so presence must be honored
|
||||
// rather than collapsed to "field not set".
|
||||
func TestSetConfig_MDMReject_LocalMetricsEmptyAddress(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.KeyLocalMetricsAddress: "127.0.0.1:9999",
|
||||
}))
|
||||
|
||||
s, ctx, profName, username, _ := setupServerWithProfile(t)
|
||||
|
||||
addr := ""
|
||||
_, err := s.SetConfig(ctx, &proto.SetConfigRequest{
|
||||
ProfileName: profName,
|
||||
@@ -205,17 +226,80 @@ func TestSetConfig_MDMReject_LocalMetricsEmptyAddress(t *testing.T) {
|
||||
assert.ElementsMatch(t, []string{mdm.KeyLocalMetricsAddress}, v.GetFields())
|
||||
}
|
||||
|
||||
func TestSetConfig_MDMReject_EmptyPreSharedKey(t *testing.T) {
|
||||
s, ctx, profName, username, _ := setupServerWithProfile(t)
|
||||
withMDMPolicy(t, s, mdm.NewPolicy(map[string]any{
|
||||
mdm.KeyPreSharedKey: "mdm-enforced-psk",
|
||||
}))
|
||||
|
||||
psk := ""
|
||||
_, err := s.SetConfig(ctx, &proto.SetConfigRequest{
|
||||
ProfileName: profName,
|
||||
Username: username,
|
||||
OptionalPreSharedKey: &psk,
|
||||
})
|
||||
|
||||
v := extractViolation(t, err)
|
||||
assert.ElementsMatch(t, []string{mdm.KeyPreSharedKey}, v.GetFields())
|
||||
}
|
||||
|
||||
func TestSetConfig_MDMAllow_PreSharedKeySentinelEcho(t *testing.T) {
|
||||
s, ctx, profName, username, _ := setupServerWithProfile(t)
|
||||
withMDMPolicy(t, s, mdm.NewPolicy(map[string]any{
|
||||
mdm.KeyPreSharedKey: "mdm-enforced-psk",
|
||||
}))
|
||||
|
||||
psk := mdm.PreSharedKeyRedactedSentinel
|
||||
resp, err := s.SetConfig(ctx, &proto.SetConfigRequest{
|
||||
ProfileName: profName,
|
||||
Username: username,
|
||||
OptionalPreSharedKey: &psk,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
}
|
||||
|
||||
func TestLoginRequestMDMConflicts_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
|
||||
msg *proto.LoginRequest
|
||||
want []string
|
||||
}{
|
||||
{name: "unset", msg: &proto.LoginRequest{}, want: nil},
|
||||
{name: "optional empty", msg: &proto.LoginRequest{OptionalPreSharedKey: &empty}, want: []string{mdm.KeyPreSharedKey}},
|
||||
{name: "optional sentinel echo", msg: &proto.LoginRequest{OptionalPreSharedKey: &sentinel}, want: nil},
|
||||
{name: "optional same value", msg: &proto.LoginRequest{OptionalPreSharedKey: &same}, want: nil},
|
||||
{name: "optional divergent", msg: &proto.LoginRequest{OptionalPreSharedKey: &other}, want: []string{mdm.KeyPreSharedKey}},
|
||||
{name: "legacy empty is unset", msg: &proto.LoginRequest{PreSharedKey: ""}, want: nil}, //nolint:staticcheck // SA1019: legacy proto field still accepted by Login
|
||||
{name: "legacy sentinel echo", msg: &proto.LoginRequest{PreSharedKey: sentinel}, want: nil}, //nolint:staticcheck // SA1019: legacy proto field still accepted by Login
|
||||
{name: "legacy divergent", msg: &proto.LoginRequest{PreSharedKey: other}, want: []string{mdm.KeyPreSharedKey}}, //nolint:staticcheck // SA1019: legacy proto field still accepted by Login
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.want, loginRequestMDMConflicts(tc.msg, policy))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetConfig_MDMReject_AllOrNothing(t *testing.T) {
|
||||
// MDM enforces ManagementURL only; user request touches both the
|
||||
// enforced field AND a non-enforced field (RosenpassEnabled).
|
||||
// The whole request must be rejected — non-conflicting fields are not
|
||||
// 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",
|
||||
}))
|
||||
|
||||
s, ctx, profName, username, cfgPath := setupServerWithProfile(t)
|
||||
|
||||
rosenpassEnabled := true
|
||||
_, err := s.SetConfig(ctx, &proto.SetConfigRequest{
|
||||
ProfileName: profName,
|
||||
@@ -237,12 +321,11 @@ func TestSetConfig_MDMReject_AllOrNothing(t *testing.T) {
|
||||
func TestSetConfig_MDMAllow_NonManagedFields(t *testing.T) {
|
||||
// MDM enforces ManagementURL but the user only writes RosenpassEnabled.
|
||||
// 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",
|
||||
}))
|
||||
|
||||
s, ctx, profName, username, _ := setupServerWithProfile(t)
|
||||
|
||||
rosenpassEnabled := true
|
||||
resp, err := s.SetConfig(ctx, &proto.SetConfigRequest{
|
||||
ProfileName: profName,
|
||||
@@ -271,12 +354,11 @@ func TestSetConfig_MDMAllow_ManagementURLPortNormalized(t *testing.T) {
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(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: tc.mdmURL,
|
||||
}))
|
||||
|
||||
s, ctx, profName, username, _ := setupServerWithProfile(t)
|
||||
|
||||
rosenpassEnabled := true
|
||||
resp, err := s.SetConfig(ctx, &proto.SetConfigRequest{
|
||||
ProfileName: profName,
|
||||
@@ -293,9 +375,8 @@ func TestSetConfig_MDMAllow_ManagementURLPortNormalized(t *testing.T) {
|
||||
|
||||
func TestSetConfig_MDMEmpty_NoEnforcement(t *testing.T) {
|
||||
// No MDM policy active: any field can be written.
|
||||
withMDMPolicy(t, mdm.NewPolicy(nil))
|
||||
|
||||
s, ctx, profName, username, _ := setupServerWithProfile(t)
|
||||
withMDMPolicy(t, s, mdm.NewPolicy(nil))
|
||||
|
||||
resp, err := s.SetConfig(ctx, &proto.SetConfigRequest{
|
||||
ProfileName: profName,
|
||||
|
||||
Reference in New Issue
Block a user