Explorar el Código

Merge branch 'develop' into NM-24

Vishal Dalwadi hace 1 mes
padre
commit
472bf3dbbf

+ 1 - 1
auth/host_session.go

@@ -77,7 +77,7 @@ func SessionHandler(conn *websocket.Conn) {
 		_, err := logic.VerifyAuthRequest(models.UserAuthParams{
 			UserName: registerMessage.User,
 			Password: registerMessage.Password,
-		})
+		}, logic.NetclientApp)
 		if err != nil {
 			err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
 			if err != nil {

+ 1 - 0
config/config.go

@@ -89,6 +89,7 @@ type ServerConfig struct {
 	DeployedByOperator         bool          `yaml:"deployed_by_operator"`
 	Environment                string        `yaml:"environment"`
 	JwtValidityDuration        time.Duration `yaml:"jwt_validity_duration" swaggertype:"primitive,integer" format:"int64"`
+	JwtValidityDurationClients time.Duration `yaml:"jwt_validity_duration_clients" swaggertype:"primitive,integer" format:"int64"`
 	RacRestrictToSingleNetwork bool          `yaml:"rac_restrict_to_single_network"`
 	CacheEnabled               string        `yaml:"caching_enabled"`
 	EndpointDetection          bool          `yaml:"endpoint_detection"`

+ 1 - 0
controllers/controller.go

@@ -56,6 +56,7 @@ func HandleRESTRequests(wg *sync.WaitGroup, ctx context.Context) {
 			"Content-Type",
 			"authorization",
 			"From-Ui",
+			"X-Application-Name",
 		},
 	)
 	originsOk := handlers.AllowedOrigins(strings.Split(servercfg.GetAllowedOrigin(), ","))

+ 96 - 2
controllers/server.go

@@ -3,6 +3,7 @@ package controller
 import (
 	"encoding/json"
 	"errors"
+	"github.com/google/go-cmp/cmp"
 	"net/http"
 	"os"
 	"strings"
@@ -56,6 +57,7 @@ func serverHandlers(r *mux.Router) {
 		Methods(http.MethodPost)
 	r.HandleFunc("/api/server/mem_profile", logic.SecurityCheck(false, http.HandlerFunc(memProfile))).
 		Methods(http.MethodPost)
+	r.HandleFunc("/api/server/feature_flags", getFeatureFlags).Methods(http.MethodGet)
 }
 
 func cpuProfile(w http.ResponseWriter, r *http.Request) {
@@ -274,11 +276,11 @@ func updateSettings(w http.ResponseWriter, r *http.Request) {
 	currSettings := logic.GetServerSettings()
 	err := logic.UpsertServerSettings(req)
 	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to udpate server settings "+err.Error()), "internal"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to update server settings "+err.Error()), "internal"))
 		return
 	}
 	logic.LogEvent(&models.Event{
-		Action: models.Update,
+		Action: identifySettingsUpdateAction(currSettings, req),
 		Source: models.Subject{
 			ID:   r.Header.Get("user"),
 			Name: r.Header.Get("user"),
@@ -323,5 +325,97 @@ func reInit(curr, new models.ServerSettings, force bool) {
 		}
 	}
 	go mq.PublishPeerUpdate(false)
+}
+
+func identifySettingsUpdateAction(old, new models.ServerSettings) models.Action {
+	// TODO: here we are relying on the dashboard to only
+	// make singular updates, but it's possible that the
+	// API can be called to make multiple changes to the
+	// server settings. We should update it to log multiple
+	// events or create singular update APIs.
+	if old.MFAEnforced != new.MFAEnforced {
+		if new.MFAEnforced {
+			return models.EnforceMFA
+		} else {
+			return models.UnenforceMFA
+		}
+	}
+
+	if old.BasicAuth != new.BasicAuth {
+		if new.BasicAuth {
+			return models.EnableBasicAuth
+		} else {
+			return models.DisableBasicAuth
+		}
+	}
+
+	if old.Telemetry != new.Telemetry {
+		if new.Telemetry == "off" {
+			return models.DisableTelemetry
+		} else {
+			return models.EnableTelemetry
+		}
+	}
+
+	if old.NetclientAutoUpdate != new.NetclientAutoUpdate ||
+		old.RacRestrictToSingleNetwork != new.RacRestrictToSingleNetwork ||
+		old.ManageDNS != new.ManageDNS ||
+		old.DefaultDomain != new.DefaultDomain ||
+		old.EndpointDetection != new.EndpointDetection {
+		return models.UpdateClientSettings
+	}
+
+	if old.AllowedEmailDomains != new.AllowedEmailDomains ||
+		old.JwtValidityDuration != new.JwtValidityDuration {
+		return models.UpdateAuthenticationSecuritySettings
+	}
+
+	if old.Verbosity != new.Verbosity ||
+		old.MetricsPort != new.MetricsPort ||
+		old.MetricInterval != new.MetricInterval ||
+		old.AuditLogsRetentionPeriodInDays != new.AuditLogsRetentionPeriodInDays {
+		return models.UpdateMonitoringAndDebuggingSettings
+	}
+
+	if old.Theme != new.Theme {
+		return models.UpdateDisplaySettings
+	}
+
+	if old.TextSize != new.TextSize ||
+		old.ReducedMotion != new.ReducedMotion {
+		return models.UpdateAccessibilitySettings
+	}
+
+	if old.EmailSenderAddr != new.EmailSenderAddr ||
+		old.EmailSenderUser != new.EmailSenderUser ||
+		old.EmailSenderPassword != new.EmailSenderPassword ||
+		old.SmtpHost != new.SmtpHost ||
+		old.SmtpPort != new.SmtpPort {
+		return models.UpdateSMTPSettings
+	}
+
+	if old.AuthProvider != new.AuthProvider ||
+		old.OIDCIssuer != new.OIDCIssuer ||
+		old.ClientID != new.ClientID ||
+		old.ClientSecret != new.ClientSecret ||
+		old.SyncEnabled != new.SyncEnabled ||
+		old.IDPSyncInterval != new.IDPSyncInterval ||
+		old.GoogleAdminEmail != new.GoogleAdminEmail ||
+		old.GoogleSACredsJson != new.GoogleSACredsJson ||
+		old.AzureTenant != new.AzureTenant ||
+		!cmp.Equal(old.GroupFilters, new.GroupFilters) ||
+		cmp.Equal(old.UserFilters, new.UserFilters) {
+		return models.UpdateIDPSettings
+	}
+
+	return models.Update
+}
 
+// @Summary     Get feature flags for this server.
+// @Router      /api/server/feature_flags [get]
+// @Tags        Server
+// @Security    oauth2
+// @Success     200 {object} config.ServerSettings
+func getFeatureFlags(w http.ResponseWriter, r *http.Request) {
+	logic.ReturnSuccessResponseWithJson(w, r, logic.GetFeatureFlags(), "")
 }

+ 135 - 35
controllers/user.go

@@ -7,6 +7,7 @@ import (
 	"errors"
 	"fmt"
 	"github.com/pquerna/otp"
+	"golang.org/x/crypto/bcrypt"
 	"image/png"
 	"net/http"
 	"reflect"
@@ -38,6 +39,7 @@ func userHandlers(r *mux.Router) {
 	r.HandleFunc("/api/users/adm/transfersuperadmin/{username}", logic.SecurityCheck(true, http.HandlerFunc(transferSuperAdmin))).
 		Methods(http.MethodPost)
 	r.HandleFunc("/api/users/adm/authenticate", authenticateUser).Methods(http.MethodPost)
+	r.HandleFunc("/api/users/{username}/validate-identity", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(validateUserIdentity)))).Methods(http.MethodPost)
 	r.HandleFunc("/api/users/{username}/auth/init-totp", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(initiateTOTPSetup)))).Methods(http.MethodPost)
 	r.HandleFunc("/api/users/{username}/auth/complete-totp", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(completeTOTPSetup)))).Methods(http.MethodPost)
 	r.HandleFunc("/api/users/{username}/auth/verify-totp", logic.PreAuthCheck(logic.ContinueIfUserMatch(http.HandlerFunc(verifyTOTP)))).Methods(http.MethodPost)
@@ -253,6 +255,10 @@ func deleteUserAccessTokens(w http.ResponseWriter, r *http.Request) {
 // @Failure     401 {object} models.ErrorResponse
 // @Failure     500 {object} models.ErrorResponse
 func authenticateUser(response http.ResponseWriter, request *http.Request) {
+	appName := request.Header.Get("X-Application-Name")
+	if appName == "" {
+		appName = logic.NetmakerDesktopApp
+	}
 
 	// Auth request consists of Mac Address and Password (from node that is authorizing
 	// in case of Master, auth is ignored and mac is set to "mastermac"
@@ -308,42 +314,10 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 			logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("access denied to dashboard"), "unauthorized"))
 			return
 		}
-		// log user activity
-		logic.LogEvent(&models.Event{
-			Action: models.Login,
-			Source: models.Subject{
-				ID:   user.UserName,
-				Name: user.UserName,
-				Type: models.UserSub,
-			},
-			TriggeredBy: user.UserName,
-			Target: models.Subject{
-				ID:   models.DashboardSub.String(),
-				Name: models.DashboardSub.String(),
-				Type: models.DashboardSub,
-			},
-			Origin: models.Dashboard,
-		})
-	} else {
-		logic.LogEvent(&models.Event{
-			Action: models.Login,
-			Source: models.Subject{
-				ID:   user.UserName,
-				Name: user.UserName,
-				Type: models.UserSub,
-			},
-			TriggeredBy: user.UserName,
-			Target: models.Subject{
-				ID:   models.ClientAppSub.String(),
-				Name: models.ClientAppSub.String(),
-				Type: models.ClientAppSub,
-			},
-			Origin: models.ClientApp,
-		})
 	}
 
 	username := authRequest.UserName
-	jwt, err := logic.VerifyAuthRequest(authRequest)
+	jwt, err := logic.VerifyAuthRequest(authRequest, appName)
 	if err != nil {
 		logger.Log(0, username, "user validation failed: ",
 			err.Error())
@@ -393,6 +367,44 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 		return
 	}
 	logger.Log(2, username, "was authenticated")
+
+	// log user activity
+	if !user.IsMFAEnabled {
+		if val := request.Header.Get("From-Ui"); val == "true" {
+			logic.LogEvent(&models.Event{
+				Action: models.Login,
+				Source: models.Subject{
+					ID:   user.UserName,
+					Name: user.UserName,
+					Type: models.UserSub,
+				},
+				TriggeredBy: user.UserName,
+				Target: models.Subject{
+					ID:   models.DashboardSub.String(),
+					Name: models.DashboardSub.String(),
+					Type: models.DashboardSub,
+				},
+				Origin: models.Dashboard,
+			})
+		} else {
+			logic.LogEvent(&models.Event{
+				Action: models.Login,
+				Source: models.Subject{
+					ID:   user.UserName,
+					Name: user.UserName,
+					Type: models.UserSub,
+				},
+				TriggeredBy: user.UserName,
+				Target: models.Subject{
+					ID:   models.ClientAppSub.String(),
+					Name: models.ClientAppSub.String(),
+					Type: models.ClientAppSub,
+				},
+				Origin: models.ClientApp,
+			})
+		}
+	}
+
 	response.Header().Set("Content-Type", "application/json")
 	response.Write(successJSONResponse)
 
@@ -434,6 +446,43 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 	}()
 }
 
+// @Summary     Validates a user's identity against it's token. This is used by UI before a user performing a critical operation to validate the user's identity.
+// @Router      /api/users/{username}/validate-identity [post]
+// @Tags        Auth
+// @Accept      json
+// @Param       body body models.UserIdentityValidationRequest true "User Identity Validation Request"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+func validateUserIdentity(w http.ResponseWriter, r *http.Request) {
+	username := r.Header.Get("user")
+
+	var req models.UserIdentityValidationRequest
+	err := json.NewDecoder(r.Body).Decode(&req)
+	if err != nil {
+		logger.Log(0, "failed to decode request body: ", err.Error())
+		err = fmt.Errorf("invalid request body: %v", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	user, err := logic.GetUser(username)
+	if err != nil {
+		logger.Log(0, "failed to get user: ", err.Error())
+		err = fmt.Errorf("user not found: %v", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	var resp models.UserIdentityValidationResponse
+	err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password))
+	if err != nil {
+		logic.ReturnSuccessResponseWithJson(w, r, resp, "user identity validation failed")
+	} else {
+		resp.IdentityValidated = true
+		logic.ReturnSuccessResponseWithJson(w, r, resp, "user identity validated")
+	}
+}
+
 // @Summary     Initiate setting up TOTP 2FA for a user.
 // @Router      /api/users/auth/init-totp [post]
 // @Tags        Auth
@@ -557,6 +606,22 @@ func completeTOTPSetup(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 
+		logic.LogEvent(&models.Event{
+			Action: models.EnableMFA,
+			Source: models.Subject{
+				ID:   user.UserName,
+				Name: user.UserName,
+				Type: models.UserSub,
+			},
+			TriggeredBy: user.UserName,
+			Target: models.Subject{
+				ID:   user.UserName,
+				Name: user.UserName,
+				Type: models.UserSub,
+			},
+			Origin: models.Dashboard,
+		})
+
 		logic.ReturnSuccessResponse(w, r, fmt.Sprintf("totp setup complete for user %s", username))
 	} else {
 		err = fmt.Errorf("cannot setup totp for user %s: invalid otp", username)
@@ -576,6 +641,11 @@ func completeTOTPSetup(w http.ResponseWriter, r *http.Request) {
 func verifyTOTP(w http.ResponseWriter, r *http.Request) {
 	username := r.Header.Get("user")
 
+	appName := r.Header.Get("X-Application-Name")
+	if appName == "" {
+		appName = logic.NetmakerDesktopApp
+	}
+
 	var req models.UserTOTPVerificationParams
 	err := json.NewDecoder(r.Body).Decode(&req)
 	if err != nil {
@@ -601,7 +671,7 @@ func verifyTOTP(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if totp.Validate(req.TOTP, user.TOTPSecret) {
-		jwt, err := logic.CreateUserJWT(user.UserName, user.PlatformRoleID)
+		jwt, err := logic.CreateUserJWT(user.UserName, user.PlatformRoleID, appName)
 		if err != nil {
 			err = fmt.Errorf("error creating token: %v", err)
 			logger.Log(0, err.Error())
@@ -619,6 +689,22 @@ func verifyTOTP(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 
+		logic.LogEvent(&models.Event{
+			Action: models.Login,
+			Source: models.Subject{
+				ID:   user.UserName,
+				Name: user.UserName,
+				Type: models.UserSub,
+			},
+			TriggeredBy: user.UserName,
+			Target: models.Subject{
+				ID:   models.DashboardSub.String(),
+				Name: models.DashboardSub.String(),
+				Type: models.DashboardSub,
+			},
+			Origin: models.Dashboard,
+		})
+
 		logic.ReturnSuccessResponseWithJson(w, r, models.SuccessfulUserLoginResponse{
 			UserName:  username,
 			AuthToken: jwt,
@@ -1135,8 +1221,22 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
 			UserName: logic.MasterUser,
 		}
 	}
+	action := models.Update
+	// TODO: here we are relying on the dashboard to only
+	// make singular updates, but it's possible that the
+	// API can be called to make multiple changes to the
+	// user. We should update it to log multiple events
+	// or create singular update APIs.
+	if userchange.IsMFAEnabled != user.IsMFAEnabled {
+		if userchange.IsMFAEnabled {
+			// the update API won't be used to enable MFA.
+			action = models.EnableMFA
+		} else {
+			action = models.DisableMFA
+		}
+	}
 	e := models.Event{
-		Action: models.Update,
+		Action: action,
 		Source: models.Subject{
 			ID:   caller.UserName,
 			Name: caller.UserName,

+ 12 - 4
logic/auth.go

@@ -24,6 +24,12 @@ const (
 	auth_key = "netmaker_auth"
 )
 
+const (
+	DashboardApp       = "dashboard"
+	NetclientApp       = "netclient"
+	NetmakerDesktopApp = "netmaker-desktop"
+)
+
 var (
 	superUser = models.User{}
 )
@@ -178,7 +184,8 @@ func CreateUser(user *models.User) error {
 		user.AuthType = models.OAuth
 	}
 	AddGlobalNetRolesToAdmins(user)
-	_, err = CreateUserJWT(user.UserName, user.PlatformRoleID)
+	// create user will always be called either from API or Dashboard.
+	_, err = CreateUserJWT(user.UserName, user.PlatformRoleID, DashboardApp)
 	if err != nil {
 		logger.Log(0, "failed to generate token", err.Error())
 		return err
@@ -212,7 +219,7 @@ func CreateSuperAdmin(u *models.User) error {
 }
 
 // VerifyAuthRequest - verifies an auth request
-func VerifyAuthRequest(authRequest models.UserAuthParams) (string, error) {
+func VerifyAuthRequest(authRequest models.UserAuthParams, appName string) (string, error) {
 	var result models.User
 	if authRequest.UserName == "" {
 		return "", errors.New("username can't be empty")
@@ -245,7 +252,7 @@ func VerifyAuthRequest(authRequest models.UserAuthParams) (string, error) {
 		return tokenString, nil
 	} else {
 		// Create a new JWT for the node
-		tokenString, err := CreateUserJWT(authRequest.UserName, result.PlatformRoleID)
+		tokenString, err := CreateUserJWT(authRequest.UserName, result.PlatformRoleID, appName)
 		if err != nil {
 			slog.Error("error creating jwt", "error", err)
 			return "", err
@@ -483,8 +490,9 @@ func GetState(state string) (*models.SsoState, error) {
 }
 
 // SetState - sets a state with new expiration
-func SetState(state string) error {
+func SetState(appName, state string) error {
 	s := models.SsoState{
+		AppName:    appName,
 		Value:      state,
 		Expiration: time.Now().Add(models.DefaultExpDuration),
 	}

+ 1 - 1
logic/extpeers.go

@@ -484,7 +484,7 @@ func GetAllExtClientsWithStatus(status models.NodeStatus) ([]models.ExtClient, e
 	var validExtClients []models.ExtClient
 	for _, extClient := range extClients {
 		node := extClient.ConvertToStaticNode()
-		GetNodeCheckInStatus(&node, false)
+		GetNodeStatus(&node, false)
 
 		if node.Status == status {
 			validExtClients = append(validExtClients, extClient)

+ 5 - 0
logic/hosts.go

@@ -6,6 +6,7 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"reflect"
 	"sort"
 	"sync"
 
@@ -307,6 +308,10 @@ func UpdateHostFromClient(newHost, currHost *models.Host) (sendPeerUpdate bool)
 		sendPeerUpdate = true
 		isEndpointChanged = true
 	}
+	if !reflect.DeepEqual(currHost.Interfaces, newHost.Interfaces) {
+		currHost.Interfaces = newHost.Interfaces
+		sendPeerUpdate = true
+	}
 
 	if isEndpointChanged {
 		for _, nodeID := range currHost.Nodes {

+ 7 - 3
logic/jwts.go

@@ -83,9 +83,13 @@ func CreateUserAccessJwtToken(username string, role models.UserRoleID, d time.Ti
 }
 
 // CreateUserJWT - creates a user jwt token
-func CreateUserJWT(username string, role models.UserRoleID) (response string, err error) {
-	settings := GetServerSettings()
-	expirationTime := time.Now().Add(time.Duration(settings.JwtValidityDuration) * time.Minute)
+func CreateUserJWT(username string, role models.UserRoleID, appName string) (response string, err error) {
+	duration := GetJwtValidityDuration()
+	if appName == NetclientApp || appName == NetmakerDesktopApp {
+		duration = GetJwtValidityDurationForClients()
+	}
+
+	expirationTime := time.Now().Add(duration)
 	claims := &models.UserClaims{
 		UserName:  username,
 		Role:      role,

+ 5 - 0
logic/server.go

@@ -1,7 +1,12 @@
 package logic
 
+import "github.com/gravitl/netmaker/models"
+
 // EnterpriseCheckFuncs - can be set to run functions for EE
 var EnterpriseCheckFuncs []func()
+var GetFeatureFlags = func() models.FeatureFlags {
+	return models.FeatureFlags{}
+}
 
 // == Join, Checkin, and Leave for Server ==
 

+ 33 - 11
logic/settings.go

@@ -33,6 +33,11 @@ func UpsertServerSettings(s models.ServerSettings) error {
 	if s.ClientSecret == Mask() {
 		s.ClientSecret = currSettings.ClientSecret
 	}
+
+	if servercfg.DeployedByOperator() {
+		s.BasicAuth = true
+	}
+
 	data, err := json.Marshal(s)
 	if err != nil {
 		return err
@@ -46,22 +51,28 @@ func UpsertServerSettings(s models.ServerSettings) error {
 
 func ValidateNewSettings(req models.ServerSettings) bool {
 	// TODO: add checks for different fields
+	if req.JwtValidityDuration > 525600 || req.JwtValidityDuration < 5 {
+		return false
+	}
 	return true
 }
 
 func GetServerSettingsFromEnv() (s models.ServerSettings) {
 
 	s = models.ServerSettings{
-		NetclientAutoUpdate:        servercfg.AutoUpdateEnabled(),
-		Verbosity:                  servercfg.GetVerbosity(),
-		AuthProvider:               os.Getenv("AUTH_PROVIDER"),
-		OIDCIssuer:                 os.Getenv("OIDC_ISSUER"),
-		ClientID:                   os.Getenv("CLIENT_ID"),
-		ClientSecret:               os.Getenv("CLIENT_SECRET"),
-		AzureTenant:                servercfg.GetAzureTenant(),
-		Telemetry:                  servercfg.Telemetry(),
-		BasicAuth:                  servercfg.IsBasicAuthEnabled(),
-		JwtValidityDuration:        servercfg.GetJwtValidityDurationFromEnv() / 60,
+		NetclientAutoUpdate: servercfg.AutoUpdateEnabled(),
+		Verbosity:           servercfg.GetVerbosity(),
+		AuthProvider:        os.Getenv("AUTH_PROVIDER"),
+		OIDCIssuer:          os.Getenv("OIDC_ISSUER"),
+		ClientID:            os.Getenv("CLIENT_ID"),
+		ClientSecret:        os.Getenv("CLIENT_SECRET"),
+		AzureTenant:         servercfg.GetAzureTenant(),
+		Telemetry:           servercfg.Telemetry(),
+		BasicAuth:           servercfg.IsBasicAuthEnabled(),
+		JwtValidityDuration: servercfg.GetJwtValidityDurationFromEnv() / 60,
+		// setting client's jwt validity duration to be the same as that of
+		// dashboard.
+		JwtValidityDurationClients: servercfg.GetJwtValidityDurationFromEnv() / 60,
 		RacRestrictToSingleNetwork: servercfg.GetRacRestrictToSingleNetwork(),
 		EndpointDetection:          servercfg.IsEndpointDetectionEnabled(),
 		AllowedEmailDomains:        servercfg.GetAllowedEmailDomains(),
@@ -139,6 +150,7 @@ func GetServerConfig() config.ServerConfig {
 		cfg.IsPro = "yes"
 	}
 	cfg.JwtValidityDuration = time.Duration(settings.JwtValidityDuration) * time.Minute
+	cfg.JwtValidityDurationClients = time.Duration(settings.JwtValidityDurationClients) * time.Minute
 	cfg.RacRestrictToSingleNetwork = settings.RacRestrictToSingleNetwork
 	cfg.MetricInterval = settings.MetricInterval
 	cfg.ManageDNS = settings.ManageDNS
@@ -201,7 +213,13 @@ func Telemetry() string {
 
 // GetJwtValidityDuration - returns the JWT validity duration in minutes
 func GetJwtValidityDuration() time.Duration {
-	return GetServerConfig().JwtValidityDuration
+	return time.Duration(GetServerSettings().JwtValidityDuration) * time.Minute
+}
+
+// GetJwtValidityDurationForClients returns the JWT validity duration in
+// minutes for clients.
+func GetJwtValidityDurationForClients() time.Duration {
+	return time.Duration(GetServerSettings().JwtValidityDurationClients) * time.Minute
 }
 
 // GetRacRestrictToSingleNetwork - returns whether the feature to allow simultaneous network connections via RAC is enabled
@@ -317,6 +335,10 @@ func GetManageDNS() bool {
 
 // IsBasicAuthEnabled - checks if basic auth has been configured to be turned off
 func IsBasicAuthEnabled() bool {
+	if servercfg.DeployedByOperator() {
+		return true
+	}
+
 	return GetServerSettings().BasicAuth
 }
 

+ 20 - 0
logic/util.go

@@ -20,6 +20,7 @@ import (
 	"github.com/c-robinson/iplib"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/models"
 )
 
 // IsBase64 - checks if a string is in base64 format
@@ -253,3 +254,22 @@ func GetClientIP(r *http.Request) string {
 	}
 	return ip
 }
+
+// CompareIfaceSlices compares two slices of Iface for deep equality (order-sensitive)
+func CompareIfaceSlices(a, b []models.Iface) bool {
+	if len(a) != len(b) {
+		return false
+	}
+	for i := range a {
+		if !compareIface(a[i], b[i]) {
+			return false
+		}
+	}
+	return true
+}
+func compareIface(a, b models.Iface) bool {
+	return a.Name == b.Name &&
+		a.Address.IP.Equal(b.Address.IP) &&
+		a.Address.Mask.String() == b.Address.Mask.String() &&
+		a.AddressString == b.AddressString
+}

+ 6 - 3
logic/zombie.go

@@ -139,7 +139,10 @@ func ManageZombies(ctx context.Context, peerUpdate chan *models.Node) {
 			if servercfg.IsAutoCleanUpEnabled() {
 				nodes, _ := GetAllNodes()
 				for _, node := range nodes {
-					if time.Since(node.LastCheckIn) > time.Minute*ZOMBIE_DELETE_TIME {
+					if !node.Connected {
+						continue
+					}
+					if time.Since(node.LastCheckIn) > time.Hour*2 {
 						if err := DeleteNode(&node, true); err != nil {
 							continue
 						}
@@ -168,8 +171,8 @@ func checkPendingRemovalNodes(peerUpdate chan *models.Node) {
 			peerUpdate <- &node
 			continue
 		}
-		if servercfg.IsAutoCleanUpEnabled() {
-			if time.Since(node.LastCheckIn) > time.Minute*ZOMBIE_DELETE_TIME {
+		if servercfg.IsAutoCleanUpEnabled() && node.Connected {
+			if time.Since(node.LastCheckIn) > time.Hour*2 {
 				if err := DeleteNode(&node, true); err != nil {
 					continue
 				}

+ 15 - 6
migrate/migrate.go

@@ -536,21 +536,27 @@ func migrateToEgressV1() {
 	}
 	for _, node := range nodes {
 		if node.IsEgressGateway {
-			egressHost, err := logic.GetHost(node.HostID.String())
+			_, err := logic.GetHost(node.HostID.String())
 			if err != nil {
 				continue
 			}
-			for _, rangeI := range node.EgressGatewayRequest.Ranges {
-				e := schema.Egress{
+			for _, rangeMetric := range node.EgressGatewayRequest.RangesWithMetric {
+				e := &schema.Egress{Range: rangeMetric.Network}
+				if err := e.DoesEgressRouteExists(db.WithContext(context.TODO())); err == nil {
+					e.Nodes[node.ID.String()] = rangeMetric.RouteMetric
+					e.Update(db.WithContext(context.TODO()))
+					continue
+				}
+				e = &schema.Egress{
 					ID:          uuid.New().String(),
-					Name:        fmt.Sprintf("%s egress", egressHost.Name),
+					Name:        fmt.Sprintf("%s egress", rangeMetric.Network),
 					Description: "",
 					Network:     node.Network,
 					Nodes: datatypes.JSONMap{
-						node.ID.String(): 256,
+						node.ID.String(): rangeMetric.RouteMetric,
 					},
 					Tags:      make(datatypes.JSONMap),
-					Range:     rangeI,
+					Range:     rangeMetric.Network,
 					Nat:       node.EgressGatewayRequest.NatEnabled == "yes",
 					Status:    true,
 					CreatedBy: user.UserName,
@@ -641,5 +647,8 @@ func settings() {
 	if settings.DefaultDomain == "" {
 		settings.DefaultDomain = servercfg.GetDefaultDomain()
 	}
+	if settings.JwtValidityDurationClients == 0 {
+		settings.JwtValidityDurationClients = servercfg.GetJwtValidityDurationFromEnv() / 60
+	}
 	logic.UpsertServerSettings(settings)
 }

+ 30 - 15
models/events.go

@@ -3,21 +3,36 @@ package models
 type Action string
 
 const (
-	Create            Action = "CREATE"
-	Update            Action = "UPDATE"
-	Delete            Action = "DELETE"
-	DeleteAll         Action = "DELETE_ALL"
-	Login             Action = "LOGIN"
-	LogOut            Action = "LOGOUT"
-	Connect           Action = "CONNECT"
-	Sync              Action = "SYNC"
-	RefreshKey        Action = "REFRESH_KEY"
-	RefreshAllKeys    Action = "REFRESH_ALL_KEYS"
-	SyncAll           Action = "SYNC_ALL"
-	UpgradeAll        Action = "UPGRADE_ALL"
-	Disconnect        Action = "DISCONNECT"
-	JoinHostToNet     Action = "JOIN_HOST_TO_NETWORK"
-	RemoveHostFromNet Action = "REMOVE_HOST_FROM_NETWORK"
+	Create                               Action = "CREATE"
+	Update                               Action = "UPDATE"
+	Delete                               Action = "DELETE"
+	DeleteAll                            Action = "DELETE_ALL"
+	Login                                Action = "LOGIN"
+	LogOut                               Action = "LOGOUT"
+	Connect                              Action = "CONNECT"
+	Sync                                 Action = "SYNC"
+	RefreshKey                           Action = "REFRESH_KEY"
+	RefreshAllKeys                       Action = "REFRESH_ALL_KEYS"
+	SyncAll                              Action = "SYNC_ALL"
+	UpgradeAll                           Action = "UPGRADE_ALL"
+	Disconnect                           Action = "DISCONNECT"
+	JoinHostToNet                        Action = "JOIN_HOST_TO_NETWORK"
+	RemoveHostFromNet                    Action = "REMOVE_HOST_FROM_NETWORK"
+	EnableMFA                            Action = "ENABLE_MFA"
+	DisableMFA                           Action = "DISABLE_MFA"
+	EnforceMFA                           Action = "ENFORCE_MFA"
+	UnenforceMFA                         Action = "UNENFORCE_MFA"
+	EnableBasicAuth                      Action = "ENABLE_BASIC_AUTH"
+	DisableBasicAuth                     Action = "DISABLE_BASIC_AUTH"
+	EnableTelemetry                      Action = "ENABLE_TELEMETRY"
+	DisableTelemetry                     Action = "DISABLE_TELEMETRY"
+	UpdateClientSettings                 Action = "UPDATE_CLIENT_SETTINGS"
+	UpdateAuthenticationSecuritySettings Action = "UPDATE_AUTHENTICATION_SECURITY_SETTINGS"
+	UpdateMonitoringAndDebuggingSettings Action = "UPDATE_MONITORING_AND_DEBUGGING_SETTINGS"
+	UpdateDisplaySettings                Action = "UPDATE_DISPLAY_SETTINGS"
+	UpdateAccessibilitySettings          Action = "UPDATE_ACCESSIBILITY_SETTINGS"
+	UpdateSMTPSettings                   Action = "UPDATE_EMAIL_SETTINGS"
+	UpdateIDPSettings                    Action = "UPDATE_IDP_SETTINGS"
 )
 
 type SubjectType string

+ 40 - 35
models/settings.go

@@ -9,39 +9,44 @@ const (
 )
 
 type ServerSettings struct {
-	NetclientAutoUpdate            bool     `json:"netclientautoupdate"`
-	Verbosity                      int32    `json:"verbosity"`
-	AuthProvider                   string   `json:"authprovider"`
-	OIDCIssuer                     string   `json:"oidcissuer"`
-	ClientID                       string   `json:"client_id"`
-	ClientSecret                   string   `json:"client_secret"`
-	SyncEnabled                    bool     `json:"sync_enabled"`
-	GoogleAdminEmail               string   `json:"google_admin_email"`
-	GoogleSACredsJson              string   `json:"google_sa_creds_json"`
-	AzureTenant                    string   `json:"azure_tenant"`
-	UserFilters                    []string `json:"user_filters"`
-	GroupFilters                   []string `json:"group_filters"`
-	IDPSyncInterval                string   `json:"idp_sync_interval"`
-	Telemetry                      string   `json:"telemetry"`
-	BasicAuth                      bool     `json:"basic_auth"`
-	JwtValidityDuration            int      `json:"jwt_validity_duration"`
-	MFAEnforced                    bool     `json:"mfa_enforced"`
-	RacRestrictToSingleNetwork     bool     `json:"rac_restrict_to_single_network"`
-	EndpointDetection              bool     `json:"endpoint_detection"`
-	AllowedEmailDomains            string   `json:"allowed_email_domains"`
-	EmailSenderAddr                string   `json:"email_sender_addr"`
-	EmailSenderUser                string   `json:"email_sender_user"`
-	EmailSenderPassword            string   `json:"email_sender_password"`
-	SmtpHost                       string   `json:"smtp_host"`
-	SmtpPort                       int      `json:"smtp_port"`
-	MetricInterval                 string   `json:"metric_interval"`
-	MetricsPort                    int      `json:"metrics_port"`
-	ManageDNS                      bool     `json:"manage_dns"`
-	DefaultDomain                  string   `json:"default_domain"`
-	Stun                           bool     `json:"stun"`
-	StunServers                    string   `json:"stun_servers"`
-	Theme                          Theme    `json:"theme"`
-	TextSize                       string   `json:"text_size"`
-	ReducedMotion                  bool     `json:"reduced_motion"`
-	AuditLogsRetentionPeriodInDays int      `json:"audit_logs_retention_period"`
+	NetclientAutoUpdate bool     `json:"netclientautoupdate"`
+	Verbosity           int32    `json:"verbosity"`
+	AuthProvider        string   `json:"authprovider"`
+	OIDCIssuer          string   `json:"oidcissuer"`
+	ClientID            string   `json:"client_id"`
+	ClientSecret        string   `json:"client_secret"`
+	SyncEnabled         bool     `json:"sync_enabled"`
+	GoogleAdminEmail    string   `json:"google_admin_email"`
+	GoogleSACredsJson   string   `json:"google_sa_creds_json"`
+	AzureTenant         string   `json:"azure_tenant"`
+	UserFilters         []string `json:"user_filters"`
+	GroupFilters        []string `json:"group_filters"`
+	IDPSyncInterval     string   `json:"idp_sync_interval"`
+	Telemetry           string   `json:"telemetry"`
+	BasicAuth           bool     `json:"basic_auth"`
+	// JwtValidityDuration is the validity duration of auth tokens for users
+	// on the dashboard (NMUI).
+	JwtValidityDuration int `json:"jwt_validity_duration"`
+	// JwtValidityDurationClients is the validity duration of auth tokens for
+	// users on the clients (NetDesk).
+	JwtValidityDurationClients     int    `json:"jwt_validity_duration_clients"`
+	MFAEnforced                    bool   `json:"mfa_enforced"`
+	RacRestrictToSingleNetwork     bool   `json:"rac_restrict_to_single_network"`
+	EndpointDetection              bool   `json:"endpoint_detection"`
+	AllowedEmailDomains            string `json:"allowed_email_domains"`
+	EmailSenderAddr                string `json:"email_sender_addr"`
+	EmailSenderUser                string `json:"email_sender_user"`
+	EmailSenderPassword            string `json:"email_sender_password"`
+	SmtpHost                       string `json:"smtp_host"`
+	SmtpPort                       int    `json:"smtp_port"`
+	MetricInterval                 string `json:"metric_interval"`
+	MetricsPort                    int    `json:"metrics_port"`
+	ManageDNS                      bool   `json:"manage_dns"`
+	DefaultDomain                  string `json:"default_domain"`
+	Stun                           bool   `json:"stun"`
+	StunServers                    string `json:"stun_servers"`
+	Theme                          Theme  `json:"theme"`
+	TextSize                       string `json:"text_size"`
+	ReducedMotion                  bool   `json:"reduced_motion"`
+	AuditLogsRetentionPeriodInDays int    `json:"audit_logs_retention_period"`
 }

+ 1 - 0
models/ssocache.go

@@ -7,6 +7,7 @@ const DefaultExpDuration = time.Minute * 5
 
 // SsoState - holds SSO sign-in session data
 type SsoState struct {
+	AppName    string    `json:"app_name"`
 	Value      string    `json:"value"`
 	Expiration time.Time `json:"expiration"`
 }

+ 7 - 0
models/structs.go

@@ -16,6 +16,13 @@ const (
 	PLACEHOLDER_TOKEN_TEXT = "ACCESS_TOKEN"
 )
 
+type FeatureFlags struct {
+	EnableNetworkActivity   bool `json:"enable_network_activity"`
+	EnableOAuth             bool `json:"enable_oauth"`
+	EnableIDPIntegration    bool `json:"enable_idp_integration"`
+	AllowMultiServerLicense bool `json:"allow_multi_server_license"`
+}
+
 // AuthParams - struct for auth params
 type AuthParams struct {
 	MacAddress string `json:"macaddress"`

+ 10 - 0
models/user_mgmt.go

@@ -202,6 +202,16 @@ type UserAuthParams struct {
 	Password string `json:"password"`
 }
 
+// UserIdentityValidationRequest - user identity validation request struct
+type UserIdentityValidationRequest struct {
+	Password string `json:"password"`
+}
+
+// UserIdentityValidationResponse - user identity validation response struct
+type UserIdentityValidationResponse struct {
+	IdentityValidated bool `json:"identity_validated"`
+}
+
 type UserTOTPVerificationParams struct {
 	OTPAuthURL          string `json:"otp_auth_url"`
 	OTPAuthURLSignature string `json:"otp_auth_url_signature"`

+ 1 - 1
mq/handlers.go

@@ -274,7 +274,7 @@ func HandleHostCheckin(h, currentHost *models.Host) bool {
 			return false
 		}
 	}
-	ifaceDelta := len(h.Interfaces) != len(currentHost.Interfaces) ||
+	ifaceDelta := len(h.Interfaces) != len(currentHost.Interfaces) || !logic.CompareIfaceSlices(h.Interfaces, currentHost.Interfaces) ||
 		!h.EndpointIP.Equal(currentHost.EndpointIP) ||
 		(len(h.NatType) > 0 && h.NatType != currentHost.NatType) ||
 		h.DefaultInterface != currentHost.DefaultInterface ||

+ 15 - 4
pro/auth/azure-ad.go

@@ -40,13 +40,18 @@ func initAzureAD(redirectURL string, clientID string, clientSecret string) {
 }
 
 func handleAzureLogin(w http.ResponseWriter, r *http.Request) {
+	appName := r.Header.Get("X-Application-Name")
+	if appName == "" {
+		appName = logic.NetmakerDesktopApp
+	}
+
 	var oauth_state_string = logic.RandomString(user_signin_length)
 	if auth_provider == nil {
 		handleOauthNotConfigured(w)
 		return
 	}
 
-	if err := logic.SetState(oauth_state_string); err != nil {
+	if err := logic.SetState(appName, oauth_state_string); err != nil {
 		handleOauthNotConfigured(w)
 		return
 	}
@@ -56,9 +61,15 @@ func handleAzureLogin(w http.ResponseWriter, r *http.Request) {
 }
 
 func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
-
 	var rState, rCode = getStateAndCode(r)
-	var content, err = getAzureUserInfo(rState, rCode)
+
+	state, err := logic.GetState(rState)
+	if err != nil {
+		handleOauthNotValid(w)
+		return
+	}
+
+	content, err := getAzureUserInfo(rState, rCode)
 	if err != nil {
 		logger.Log(1, "error when getting user info from azure:", err.Error())
 		if strings.Contains(err.Error(), "invalid oauth state") || strings.Contains(err.Error(), "failed to fetch user email from SSO state") {
@@ -179,7 +190,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		Password: newPass,
 	}
 
-	var jwt, jwtErr = logic.VerifyAuthRequest(authRequest)
+	var jwt, jwtErr = logic.VerifyAuthRequest(authRequest, state.AppName)
 	if jwtErr != nil {
 		logger.Log(1, "could not parse jwt for user", authRequest.UserName)
 		return

+ 15 - 4
pro/auth/github.go

@@ -40,13 +40,18 @@ func initGithub(redirectURL string, clientID string, clientSecret string) {
 }
 
 func handleGithubLogin(w http.ResponseWriter, r *http.Request) {
+	appName := r.Header.Get("X-Application-Name")
+	if appName == "" {
+		appName = logic.NetmakerDesktopApp
+	}
+
 	var oauth_state_string = logic.RandomString(user_signin_length)
 	if auth_provider == nil {
 		handleOauthNotConfigured(w)
 		return
 	}
 
-	if err := logic.SetState(oauth_state_string); err != nil {
+	if err := logic.SetState(appName, oauth_state_string); err != nil {
 		handleOauthNotConfigured(w)
 		return
 	}
@@ -56,9 +61,15 @@ func handleGithubLogin(w http.ResponseWriter, r *http.Request) {
 }
 
 func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
-
 	var rState, rCode = getStateAndCode(r)
-	var content, err = getGithubUserInfo(rState, rCode)
+
+	state, err := logic.GetState(rState)
+	if err != nil {
+		handleOauthNotValid(w)
+		return
+	}
+
+	content, err := getGithubUserInfo(rState, rCode)
 	if err != nil {
 		logger.Log(1, "error when getting user info from github:", err.Error())
 		if strings.Contains(err.Error(), "invalid oauth state") || strings.Contains(err.Error(), "failed to fetch user email from SSO state") {
@@ -170,7 +181,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 		Password: newPass,
 	}
 
-	var jwt, jwtErr = logic.VerifyAuthRequest(authRequest)
+	var jwt, jwtErr = logic.VerifyAuthRequest(authRequest, state.AppName)
 	if jwtErr != nil {
 		logger.Log(1, "could not parse jwt for user", authRequest.UserName)
 		return

+ 15 - 4
pro/auth/google.go

@@ -40,13 +40,18 @@ func initGoogle(redirectURL string, clientID string, clientSecret string) {
 }
 
 func handleGoogleLogin(w http.ResponseWriter, r *http.Request) {
+	appName := r.Header.Get("X-Application-Name")
+	if appName == "" {
+		appName = logic.NetmakerDesktopApp
+	}
+
 	var oauth_state_string = logic.RandomString(user_signin_length)
 	if auth_provider == nil {
 		handleOauthNotConfigured(w)
 		return
 	}
 	logger.Log(0, "Setting OAuth State ", oauth_state_string)
-	if err := logic.SetState(oauth_state_string); err != nil {
+	if err := logic.SetState(appName, oauth_state_string); err != nil {
 		handleOauthNotConfigured(w)
 		return
 	}
@@ -56,10 +61,16 @@ func handleGoogleLogin(w http.ResponseWriter, r *http.Request) {
 }
 
 func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
-
 	var rState, rCode = getStateAndCode(r)
 	logger.Log(0, "Fetched OAuth State ", rState)
-	var content, err = getGoogleUserInfo(rState, rCode)
+
+	state, err := logic.GetState(rState)
+	if err != nil {
+		handleOauthNotValid(w)
+		return
+	}
+
+	content, err := getGoogleUserInfo(rState, rCode)
 	if err != nil {
 		logger.Log(1, "error when getting user info from google:", err.Error())
 		if strings.Contains(err.Error(), "invalid oauth state") {
@@ -162,7 +173,7 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 		Password: newPass,
 	}
 
-	var jwt, jwtErr = logic.VerifyAuthRequest(authRequest)
+	var jwt, jwtErr = logic.VerifyAuthRequest(authRequest, state.AppName)
 	if jwtErr != nil {
 		logger.Log(1, "could not parse jwt for user", authRequest.UserName)
 		return

+ 1 - 1
pro/auth/headless_callback.go

@@ -86,7 +86,7 @@ func HandleHeadlessSSOCallback(w http.ResponseWriter, r *http.Request) {
 	jwt, jwtErr := logic.VerifyAuthRequest(models.UserAuthParams{
 		UserName: user.UserName,
 		Password: newPass,
-	})
+	}, logic.NetclientApp)
 	if jwtErr != nil {
 		logger.Log(1, "could not parse jwt for user", userClaims.getUserName())
 		return

+ 14 - 4
pro/auth/oidc.go

@@ -52,13 +52,18 @@ func initOIDC(redirectURL string, clientID string, clientSecret string, issuer s
 }
 
 func handleOIDCLogin(w http.ResponseWriter, r *http.Request) {
+	appName := r.Header.Get("X-Application-Name")
+	if appName == "" {
+		appName = logic.NetmakerDesktopApp
+	}
+
 	var oauth_state_string = logic.RandomString(user_signin_length)
 	if auth_provider == nil {
 		handleOauthNotConfigured(w)
 		return
 	}
 
-	if err := logic.SetState(oauth_state_string); err != nil {
+	if err := logic.SetState(appName, oauth_state_string); err != nil {
 		handleOauthNotConfigured(w)
 		return
 	}
@@ -67,10 +72,15 @@ func handleOIDCLogin(w http.ResponseWriter, r *http.Request) {
 }
 
 func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
-
 	var rState, rCode = getStateAndCode(r)
 
-	var content, err = getOIDCUserInfo(rState, rCode)
+	state, err := logic.GetState(rState)
+	if err != nil {
+		handleOauthNotValid(w)
+		return
+	}
+
+	content, err := getOIDCUserInfo(rState, rCode)
 	if err != nil {
 		logger.Log(1, "error when getting user info from callback:", err.Error())
 		if strings.Contains(err.Error(), "invalid oauth state") {
@@ -170,7 +180,7 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 		Password: newPass,
 	}
 
-	var jwt, jwtErr = logic.VerifyAuthRequest(authRequest)
+	var jwt, jwtErr = logic.VerifyAuthRequest(authRequest, state.AppName)
 	if jwtErr != nil {
 		logger.Log(1, "could not parse jwt for user", authRequest.UserName, jwtErr.Error())
 		return

+ 1 - 1
pro/initialize.go

@@ -155,7 +155,7 @@ func InitPro() {
 	logic.GetFwRulesForNodeAndPeerOnGw = proLogic.GetFwRulesForNodeAndPeerOnGw
 	logic.GetFwRulesForUserNodesOnGw = proLogic.GetFwRulesForUserNodesOnGw
 	logic.GetHostLocInfo = proLogic.GetHostLocInfo
-
+	logic.GetFeatureFlags = proLogic.GetFeatureFlags
 }
 
 func retrieveProLogo() string {

+ 3 - 0
pro/license.go

@@ -135,6 +135,8 @@ func ValidateLicense() (err error) {
 		return err
 	}
 
+	proLogic.SetFeatureFlags(licenseResponse.FeatureFlags)
+
 	slog.Info("License validation succeeded!")
 	return nil
 }
@@ -200,6 +202,7 @@ func validateLicenseKey(encryptedData []byte, publicKey *[32]byte) ([]byte, bool
 		LicenseKey:     servercfg.GetLicenseKey(),
 		NmServerPubKey: base64encode(publicKeyBytes),
 		EncryptedPart:  base64encode(encryptedData),
+		NmBaseDomain:   servercfg.GetNmBaseDomain(),
 	}
 
 	requestBody, err := json.Marshal(msg)

+ 13 - 0
pro/logic/server.go

@@ -0,0 +1,13 @@
+package logic
+
+import "github.com/gravitl/netmaker/models"
+
+var featureFlagsCache models.FeatureFlags
+
+func SetFeatureFlags(featureFlags models.FeatureFlags) {
+	featureFlagsCache = featureFlags
+}
+
+func GetFeatureFlags() models.FeatureFlags {
+	return featureFlagsCache
+}

+ 5 - 2
pro/types.go

@@ -5,6 +5,7 @@ package pro
 
 import (
 	"errors"
+	"github.com/gravitl/netmaker/models"
 )
 
 const (
@@ -32,8 +33,9 @@ type LicenseKey struct {
 
 // ValidatedLicense - the validated license struct
 type ValidatedLicense struct {
-	LicenseValue     string `json:"license_value"     binding:"required"` // license that validation is being requested for
-	EncryptedLicense string `json:"encrypted_license" binding:"required"` // to be decrypted by Netmaker using Netmaker server's private key
+	LicenseValue     string              `json:"license_value"     binding:"required"` // license that validation is being requested for
+	EncryptedLicense string              `json:"encrypted_license" binding:"required"` // to be decrypted by Netmaker using Netmaker server's private key
+	FeatureFlags     models.FeatureFlags `json:"feature_flags" binding:"required"`
 }
 
 // LicenseSecret - the encrypted struct for sending user-id
@@ -74,6 +76,7 @@ type ValidateLicenseRequest struct {
 	LicenseKey     string `json:"license_key"       binding:"required"`
 	NmServerPubKey string `json:"nm_server_pub_key" binding:"required"` // Netmaker server public key used to send data back to Netmaker for the Netmaker server to decrypt (eg output from validating license)
 	EncryptedPart  string `json:"secret"            binding:"required"`
+	NmBaseDomain   string `json:"nm_base_domain"`
 }
 
 type licenseResponseCache struct {

+ 4 - 0
schema/egress.go

@@ -50,6 +50,10 @@ func (e *Egress) UpdateEgressStatus(ctx context.Context) error {
 	}).Error
 }
 
+func (e *Egress) DoesEgressRouteExists(ctx context.Context) error {
+	return db.FromContext(ctx).Table(e.Table()).Where("range = ?", e.Range).First(&e).Error
+}
+
 func (e *Egress) Create(ctx context.Context) error {
 	return db.FromContext(ctx).Table(e.Table()).Create(&e).Error
 }

+ 5 - 76
servercfg/serverconf.go

@@ -38,82 +38,7 @@ func SetHost() error {
 	return nil
 }
 
-// GetServerConfig - gets the server config into memory from file or env
-func GetServerConfig() config.ServerConfig {
-	var cfg config.ServerConfig
-	cfg.APIConnString = GetAPIConnString()
-	cfg.CoreDNSAddr = GetCoreDNSAddr()
-	cfg.APIHost = GetAPIHost()
-	cfg.APIPort = GetAPIPort()
-	cfg.MasterKey = "(hidden)"
-	cfg.DNSKey = "(hidden)"
-	cfg.AllowedOrigin = GetAllowedOrigin()
-	cfg.RestBackend = "off"
-	cfg.NodeID = GetNodeID()
-	cfg.BrokerType = GetBrokerType()
-	cfg.EmqxRestEndpoint = GetEmqxRestEndpoint()
-	if AutoUpdateEnabled() {
-		cfg.NetclientAutoUpdate = "enabled"
-	} else {
-		cfg.NetclientAutoUpdate = "disabled"
-	}
-	if IsRestBackend() {
-		cfg.RestBackend = "on"
-	}
-	cfg.DNSMode = "off"
-	if IsDNSMode() {
-		cfg.DNSMode = "on"
-	}
-	cfg.DisplayKeys = "off"
-	if IsDisplayKeys() {
-		cfg.DisplayKeys = "on"
-	}
-	cfg.DisableRemoteIPCheck = "off"
-	if DisableRemoteIPCheck() {
-		cfg.DisableRemoteIPCheck = "on"
-	}
-	cfg.Database = GetDB()
-	cfg.Platform = GetPlatform()
-	cfg.Version = GetVersion()
-	cfg.PublicIp = GetServerHostIP()
-
-	// == auth config ==
-	var authInfo = GetAuthProviderInfo()
-	cfg.AuthProvider = authInfo[0]
-	cfg.ClientID = authInfo[1]
-	cfg.ClientSecret = authInfo[2]
-	cfg.FrontendURL = GetFrontendURL()
-	cfg.Telemetry = Telemetry()
-	cfg.Server = GetServer()
-	cfg.Verbosity = GetVerbosity()
-	cfg.IsPro = "no"
-	if IsPro {
-		cfg.IsPro = "yes"
-	}
-	cfg.JwtValidityDuration = GetJwtValidityDuration()
-	cfg.RacRestrictToSingleNetwork = GetRacRestrictToSingleNetwork()
-	cfg.MetricInterval = GetMetricInterval()
-	cfg.ManageDNS = GetManageDNS()
-	cfg.Stun = IsStunEnabled()
-	cfg.StunServers = GetStunServers()
-	cfg.DefaultDomain = GetDefaultDomain()
-	return cfg
-}
-
-// GetJwtValidityDuration - returns the JWT validity duration in seconds
-func GetJwtValidityDuration() time.Duration {
-	var defaultDuration = time.Duration(24) * time.Hour
-	if os.Getenv("JWT_VALIDITY_DURATION") != "" {
-		t, err := strconv.Atoi(os.Getenv("JWT_VALIDITY_DURATION"))
-		if err != nil {
-			return defaultDuration
-		}
-		return time.Duration(t) * time.Second
-	}
-	return defaultDuration
-}
-
-// GetJwtValidityDuration - returns the JWT validity duration in seconds
+// GetJwtValidityDurationFromEnv - returns the JWT validity duration in seconds
 func GetJwtValidityDurationFromEnv() int {
 	var defaultDuration = 43200
 	if os.Getenv("JWT_VALIDITY_DURATION") != "" {
@@ -721,6 +646,10 @@ func GetEmqxRestEndpoint() string {
 
 // IsBasicAuthEnabled - checks if basic auth has been configured to be turned off
 func IsBasicAuthEnabled() bool {
+	if DeployedByOperator() {
+		return true
+	}
+
 	var enabled = true //default
 	if os.Getenv("BASIC_AUTH") != "" {
 		enabled = os.Getenv("BASIC_AUTH") == "yes"