Browse Source

resolve merge conflicts

abhishek9686 1 year ago
parent
commit
629c24d2b3

+ 24 - 4
auth/auth.go

@@ -75,7 +75,7 @@ func InitializeAuthProvider() string {
 	if functions == nil {
 	if functions == nil {
 		return ""
 		return ""
 	}
 	}
-	var _, err = fetchPassValue(logic.RandomString(64))
+	var _, err = FetchPassValue(logic.RandomString(64))
 	if err != nil {
 	if err != nil {
 		logger.Log(0, err.Error())
 		logger.Log(0, err.Error())
 		return ""
 		return ""
@@ -156,7 +156,7 @@ func HandleAuthLogin(w http.ResponseWriter, r *http.Request) {
 
 
 // IsOauthUser - returns
 // IsOauthUser - returns
 func IsOauthUser(user *models.User) error {
 func IsOauthUser(user *models.User) error {
-	var currentValue, err = fetchPassValue("")
+	var currentValue, err = FetchPassValue("")
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -246,7 +246,7 @@ func addUser(email string) error {
 		slog.Error("error checking for existence of admin user during OAuth login for", "email", email, "error", err)
 		slog.Error("error checking for existence of admin user during OAuth login for", "email", email, "error", err)
 		return err
 		return err
 	} // generate random password to adapt to current model
 	} // generate random password to adapt to current model
-	var newPass, fetchErr = fetchPassValue("")
+	var newPass, fetchErr = FetchPassValue("")
 	if fetchErr != nil {
 	if fetchErr != nil {
 		return fetchErr
 		return fetchErr
 	}
 	}
@@ -272,7 +272,7 @@ func addUser(email string) error {
 	return nil
 	return nil
 }
 }
 
 
-func fetchPassValue(newValue string) (string, error) {
+func FetchPassValue(newValue string) (string, error) {
 
 
 	type valueHolder struct {
 	type valueHolder struct {
 		Value string `json:"value" bson:"value"`
 		Value string `json:"value" bson:"value"`
@@ -334,3 +334,23 @@ func isStateCached(state string) bool {
 	_, err := netcache.Get(state)
 	_, err := netcache.Get(state)
 	return err == nil || strings.Contains(err.Error(), "expired")
 	return err == nil || strings.Contains(err.Error(), "expired")
 }
 }
+
+// isEmailAllowed - checks if email is allowed to signup
+func isEmailAllowed(email string) bool {
+	allowedDomains := servercfg.GetAllowedEmailDomains()
+	domains := strings.Split(allowedDomains, ",")
+	if len(domains) == 1 && domains[0] == "*" {
+		return true
+	}
+	emailParts := strings.Split(email, "@")
+	if len(emailParts) < 2 {
+		return false
+	}
+	baseDomainOfEmail := emailParts[1]
+	for _, domain := range domains {
+		if domain == baseDomainOfEmail {
+			return true
+		}
+	}
+	return false
+}

+ 25 - 4
auth/azure-ad.go

@@ -7,6 +7,7 @@ import (
 	"io"
 	"io"
 	"net/http"
 	"net/http"
 
 
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
@@ -60,13 +61,33 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		handleOauthNotConfigured(w)
 		return
 		return
 	}
 	}
+	if !isEmailAllowed(content.UserPrincipalName) {
+		handleOauthUserNotAllowedToSignUp(w)
+		return
+	}
+	// check if user approval is already pending
+	if logic.IsPendingUser(content.UserPrincipalName) {
+		handleOauthUserNotAllowed(w)
+		return
+	}
 	_, err = logic.GetUser(content.UserPrincipalName)
 	_, err = logic.GetUser(content.UserPrincipalName)
-	if err != nil { // user must not exists, so try to make one
-		if err = addUser(content.UserPrincipalName); err != nil {
+	if err != nil {
+		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
+			err = logic.InsertPendingUser(&models.User{
+				UserName: content.UserPrincipalName,
+			})
+			if err != nil {
+				handleSomethingWentWrong(w)
+				return
+			}
+			handleOauthUserNotAllowed(w)
+			return
+		} else {
+			handleSomethingWentWrong(w)
 			return
 			return
 		}
 		}
 	}
 	}
-	user, err := logic.GetUser(content.Email)
+	user, err := logic.GetUser(content.UserPrincipalName)
 	if err != nil {
 	if err != nil {
 		handleOauthUserNotFound(w)
 		handleOauthUserNotFound(w)
 		return
 		return
@@ -75,7 +96,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		handleOauthUserNotAllowed(w)
 		return
 		return
 	}
 	}
-	var newPass, fetchErr = fetchPassValue("")
+	var newPass, fetchErr = FetchPassValue("")
 	if fetchErr != nil {
 	if fetchErr != nil {
 		return
 		return
 	}
 	}

+ 26 - 1
auth/error.go

@@ -13,7 +13,8 @@ const oauthNotConfigured = `<!DOCTYPE html><html>
 const userNotAllowed = `<!DOCTYPE html><html>
 const userNotAllowed = `<!DOCTYPE html><html>
 <body>
 <body>
 <h3>Only Admins are allowed to access Dashboard.</h3>
 <h3>Only Admins are allowed to access Dashboard.</h3>
-<p>Non-Admins can access the netmaker networks using <a href="https://docs.netmaker.io/pro/rac.html" target="_blank" rel="noopener">RemoteAccessClient.</a></p>
+<h3>Furthermore, Admin has to approve your identity to have access to netmaker networks</h3>
+<p>Once your identity is approved, Non-Admins can access the netmaker networks using <a href="https://docs.netmaker.io/pro/rac.html" target="_blank" rel="noopener">RemoteAccessClient.</a></p>
 </body>
 </body>
 </html>
 </html>
 `
 `
@@ -23,6 +24,18 @@ const userNotFound = `<!DOCTYPE html><html>
 </body>
 </body>
 </html>`
 </html>`
 
 
+const somethingwentwrong = `<!DOCTYPE html><html>
+<body>
+<h3>Something went wrong. Contact Admin</h3>
+</body>
+</html>`
+
+const notallowedtosignup = `<!DOCTYPE html><html>
+<body>
+<h3>You are not allowed to SignUp.</h3>
+</body>
+</html>`
+
 func handleOauthUserNotFound(response http.ResponseWriter) {
 func handleOauthUserNotFound(response http.ResponseWriter) {
 	response.Header().Set("Content-Type", "text/html; charset=utf-8")
 	response.Header().Set("Content-Type", "text/html; charset=utf-8")
 	response.WriteHeader(http.StatusNotFound)
 	response.WriteHeader(http.StatusNotFound)
@@ -35,9 +48,21 @@ func handleOauthUserNotAllowed(response http.ResponseWriter) {
 	response.Write([]byte(userNotAllowed))
 	response.Write([]byte(userNotAllowed))
 }
 }
 
 
+func handleOauthUserNotAllowedToSignUp(response http.ResponseWriter) {
+	response.Header().Set("Content-Type", "text/html; charset=utf-8")
+	response.WriteHeader(http.StatusForbidden)
+	response.Write([]byte(notallowedtosignup))
+}
+
 // handleOauthNotConfigured - returns an appropriate html page when oauth is not configured on netmaker server but an oauth login was attempted
 // handleOauthNotConfigured - returns an appropriate html page when oauth is not configured on netmaker server but an oauth login was attempted
 func handleOauthNotConfigured(response http.ResponseWriter) {
 func handleOauthNotConfigured(response http.ResponseWriter) {
 	response.Header().Set("Content-Type", "text/html; charset=utf-8")
 	response.Header().Set("Content-Type", "text/html; charset=utf-8")
 	response.WriteHeader(http.StatusInternalServerError)
 	response.WriteHeader(http.StatusInternalServerError)
 	response.Write([]byte(oauthNotConfigured))
 	response.Write([]byte(oauthNotConfigured))
 }
 }
+
+func handleSomethingWentWrong(response http.ResponseWriter) {
+	response.Header().Set("Content-Type", "text/html; charset=utf-8")
+	response.WriteHeader(http.StatusInternalServerError)
+	response.Write([]byte(somethingwentwrong))
+}

+ 25 - 4
auth/github.go

@@ -7,6 +7,7 @@ import (
 	"io"
 	"io"
 	"net/http"
 	"net/http"
 
 
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
@@ -60,13 +61,33 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		handleOauthNotConfigured(w)
 		return
 		return
 	}
 	}
+	if !isEmailAllowed(content.Login) {
+		handleOauthUserNotAllowedToSignUp(w)
+		return
+	}
+	// check if user approval is already pending
+	if logic.IsPendingUser(content.Login) {
+		handleOauthUserNotAllowed(w)
+		return
+	}
 	_, err = logic.GetUser(content.Login)
 	_, err = logic.GetUser(content.Login)
-	if err != nil { // user must not exist, so try to make one
-		if err = addUser(content.Login); err != nil {
+	if err != nil {
+		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
+			err = logic.InsertPendingUser(&models.User{
+				UserName: content.Login,
+			})
+			if err != nil {
+				handleSomethingWentWrong(w)
+				return
+			}
+			handleOauthUserNotAllowed(w)
+			return
+		} else {
+			handleSomethingWentWrong(w)
 			return
 			return
 		}
 		}
 	}
 	}
-	user, err := logic.GetUser(content.Email)
+	user, err := logic.GetUser(content.Login)
 	if err != nil {
 	if err != nil {
 		handleOauthUserNotFound(w)
 		handleOauthUserNotFound(w)
 		return
 		return
@@ -75,7 +96,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		handleOauthUserNotAllowed(w)
 		return
 		return
 	}
 	}
-	var newPass, fetchErr = fetchPassValue("")
+	var newPass, fetchErr = FetchPassValue("")
 	if fetchErr != nil {
 	if fetchErr != nil {
 		return
 		return
 	}
 	}

+ 24 - 3
auth/google.go

@@ -8,6 +8,7 @@ import (
 	"net/http"
 	"net/http"
 	"time"
 	"time"
 
 
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
@@ -62,9 +63,29 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		handleOauthNotConfigured(w)
 		return
 		return
 	}
 	}
+	if !isEmailAllowed(content.Email) {
+		handleOauthUserNotAllowedToSignUp(w)
+		return
+	}
+	// check if user approval is already pending
+	if logic.IsPendingUser(content.Email) {
+		handleOauthUserNotAllowed(w)
+		return
+	}
 	_, err = logic.GetUser(content.Email)
 	_, err = logic.GetUser(content.Email)
-	if err != nil { // user must not exists, so try to make one
-		if err = addUser(content.Email); err != nil {
+	if err != nil {
+		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
+			err = logic.InsertPendingUser(&models.User{
+				UserName: content.Email,
+			})
+			if err != nil {
+				handleSomethingWentWrong(w)
+				return
+			}
+			handleOauthUserNotAllowed(w)
+			return
+		} else {
+			handleSomethingWentWrong(w)
 			return
 			return
 		}
 		}
 	}
 	}
@@ -77,7 +98,7 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		handleOauthUserNotAllowed(w)
 		return
 		return
 	}
 	}
-	var newPass, fetchErr = fetchPassValue("")
+	var newPass, fetchErr = FetchPassValue("")
 	if fetchErr != nil {
 	if fetchErr != nil {
 		return
 		return
 	}
 	}

+ 13 - 8
auth/headless_callback.go

@@ -50,19 +50,24 @@ func HandleHeadlessSSOCallback(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
-	_, err = logic.GetUser(userClaims.getUserName())
-	if err != nil { // user must not exists, so try to make one
-		if err = addUser(userClaims.getUserName()); err != nil {
-			logger.Log(1, "could not create new user: ", userClaims.getUserName())
-			return
-		}
+	// check if user approval is already pending
+	if logic.IsPendingUser(userClaims.getUserName()) {
+		handleOauthUserNotAllowed(w)
+		return
+	}
+	user, err := logic.GetUser(userClaims.getUserName())
+	if err != nil {
+		response := returnErrTemplate("", "user not found", state, reqKeyIf)
+		w.WriteHeader(http.StatusForbidden)
+		w.Write(response)
+		return
 	}
 	}
-	newPass, fetchErr := fetchPassValue("")
+	newPass, fetchErr := FetchPassValue("")
 	if fetchErr != nil {
 	if fetchErr != nil {
 		return
 		return
 	}
 	}
 	jwt, jwtErr := logic.VerifyAuthRequest(models.UserAuthParams{
 	jwt, jwtErr := logic.VerifyAuthRequest(models.UserAuthParams{
-		UserName: userClaims.getUserName(),
+		UserName: user.UserName,
 		Password: newPass,
 		Password: newPass,
 	})
 	})
 	if jwtErr != nil {
 	if jwtErr != nil {

+ 24 - 3
auth/oidc.go

@@ -7,6 +7,7 @@ import (
 	"time"
 	"time"
 
 
 	"github.com/coreos/go-oidc/v3/oidc"
 	"github.com/coreos/go-oidc/v3/oidc"
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
@@ -73,9 +74,29 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		handleOauthNotConfigured(w)
 		return
 		return
 	}
 	}
+	if !isEmailAllowed(content.Email) {
+		handleOauthUserNotAllowedToSignUp(w)
+		return
+	}
+	// check if user approval is already pending
+	if logic.IsPendingUser(content.Email) {
+		handleOauthUserNotAllowed(w)
+		return
+	}
 	_, err = logic.GetUser(content.Email)
 	_, err = logic.GetUser(content.Email)
-	if err != nil { // user must not exists, so try to make one
-		if err = addUser(content.Email); err != nil {
+	if err != nil {
+		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
+			err = logic.InsertPendingUser(&models.User{
+				UserName: content.Email,
+			})
+			if err != nil {
+				handleSomethingWentWrong(w)
+				return
+			}
+			handleOauthUserNotAllowed(w)
+			return
+		} else {
+			handleSomethingWentWrong(w)
 			return
 			return
 		}
 		}
 	}
 	}
@@ -88,7 +109,7 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		handleOauthUserNotAllowed(w)
 		return
 		return
 	}
 	}
-	var newPass, fetchErr = fetchPassValue("")
+	var newPass, fetchErr = FetchPassValue("")
 	if fetchErr != nil {
 	if fetchErr != nil {
 		return
 		return
 	}
 	}

+ 20 - 0
cli/cmd/failover/disable.go

@@ -0,0 +1,20 @@
+package failover
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var disableFailoverCmd = &cobra.Command{
+	Use:   "disable [NODE ID]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Disable failover for a given Node",
+	Long:  `Disable failover for a given Node`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.DisableNodeFailover(args[0]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(disableFailoverCmd)
+}

+ 20 - 0
cli/cmd/failover/enable.go

@@ -0,0 +1,20 @@
+package failover
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var enableFailoverCmd = &cobra.Command{
+	Use:   "enable [NODE ID]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Enable failover for a given Node",
+	Long:  `Enable failover for a given Node`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.EnableNodeFailover(args[0]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(enableFailoverCmd)
+}

+ 28 - 0
cli/cmd/failover/root.go

@@ -0,0 +1,28 @@
+package failover
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "failover",
+	Short: "Enable/Disable failover for a node associated with a network",
+	Long:  `Enable/Disable failover for a node associated with a network`,
+}
+
+// GetRoot returns the root subcommand
+func GetRoot() *cobra.Command {
+	return rootCmd
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}

+ 2 - 0
cli/cmd/root.go

@@ -9,6 +9,7 @@ import (
 	"github.com/gravitl/netmaker/cli/cmd/dns"
 	"github.com/gravitl/netmaker/cli/cmd/dns"
 	"github.com/gravitl/netmaker/cli/cmd/enrollment_key"
 	"github.com/gravitl/netmaker/cli/cmd/enrollment_key"
 	"github.com/gravitl/netmaker/cli/cmd/ext_client"
 	"github.com/gravitl/netmaker/cli/cmd/ext_client"
+	"github.com/gravitl/netmaker/cli/cmd/failover"
 	"github.com/gravitl/netmaker/cli/cmd/host"
 	"github.com/gravitl/netmaker/cli/cmd/host"
 	"github.com/gravitl/netmaker/cli/cmd/metrics"
 	"github.com/gravitl/netmaker/cli/cmd/metrics"
 	"github.com/gravitl/netmaker/cli/cmd/network"
 	"github.com/gravitl/netmaker/cli/cmd/network"
@@ -53,4 +54,5 @@ func init() {
 	rootCmd.AddCommand(metrics.GetRoot())
 	rootCmd.AddCommand(metrics.GetRoot())
 	rootCmd.AddCommand(host.GetRoot())
 	rootCmd.AddCommand(host.GetRoot())
 	rootCmd.AddCommand(enrollment_key.GetRoot())
 	rootCmd.AddCommand(enrollment_key.GetRoot())
+	rootCmd.AddCommand(failover.GetRoot())
 }
 }

+ 18 - 0
cli/functions/failover.go

@@ -0,0 +1,18 @@
+package functions
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/gravitl/netmaker/models"
+)
+
+// EnableNodeFailover - Enable failover for a given Node
+func EnableNodeFailover(nodeID string) *models.SuccessResponse {
+	return request[models.SuccessResponse](http.MethodPost, fmt.Sprintf("/api/v1/node/%s/failover", nodeID), nil)
+}
+
+// DisableNodeFailover - Disable failover for a given Node
+func DisableNodeFailover(nodeID string) *models.SuccessResponse {
+	return request[models.SuccessResponse](http.MethodDelete, fmt.Sprintf("/api/v1/node/%s/failover", nodeID), nil)
+}

+ 1 - 0
config/config.go

@@ -93,6 +93,7 @@ type ServerConfig struct {
 	RacAutoDisable             bool          `yaml:"rac_auto_disable"`
 	RacAutoDisable             bool          `yaml:"rac_auto_disable"`
 	CacheEnabled               string        `yaml:"caching_enabled"`
 	CacheEnabled               string        `yaml:"caching_enabled"`
 	EndpointDetection          bool          `json:"endpoint_detection"`
 	EndpointDetection          bool          `json:"endpoint_detection"`
+	AllowedEmailDomains        string        `yaml:"allowed_email_domains"`
 }
 }
 
 
 // SQLConfig - Generic SQL Config
 // SQLConfig - Generic SQL Config

+ 9 - 2
controllers/docs.go

@@ -49,6 +49,12 @@ type hasAdmin struct {
 	Admin bool
 	Admin bool
 }
 }
 
 
+// swagger:response apiHostSliceResponse
+type apiHostSliceResponse struct {
+	// in: body
+	Host []models.ApiHost
+}
+
 // swagger:response apiHostResponse
 // swagger:response apiHostResponse
 type apiHostResponse struct {
 type apiHostResponse struct {
 	// in: body
 	// in: body
@@ -251,7 +257,7 @@ type networkBodyResponse struct {
 	Network models.Network `json:"network"`
 	Network models.Network `json:"network"`
 }
 }
 
 
-// swagger:parameters updateNetworkACL getNetworkACL
+// swagger:parameters updateNetworkACL
 type aclContainerBodyParam struct {
 type aclContainerBodyParam struct {
 	// ACL Container
 	// ACL Container
 	// in: body
 	// in: body
@@ -269,7 +275,7 @@ type aclContainerResponse struct {
 type nodeSliceResponse struct {
 type nodeSliceResponse struct {
 	// Nodes
 	// Nodes
 	// in: body
 	// in: body
-	Nodes []models.LegacyNode `json:"nodes"`
+	Nodes []models.ApiNode `json:"nodes"`
 }
 }
 
 
 // swagger:response nodeResponse
 // swagger:response nodeResponse
@@ -453,6 +459,7 @@ func useUnused() bool {
 	_ = userAuthBodyParam{}
 	_ = userAuthBodyParam{}
 	_ = usernamePathParam{}
 	_ = usernamePathParam{}
 	_ = hasAdmin{}
 	_ = hasAdmin{}
+	_ = apiHostSliceResponse{}
 	_ = apiHostResponse{}
 	_ = apiHostResponse{}
 	_ = fileResponse{}
 	_ = fileResponse{}
 	_ = extClientConfParams{}
 	_ = extClientConfParams{}

+ 33 - 1
controllers/hosts.go

@@ -32,6 +32,7 @@ func hostHandlers(r *mux.Router) {
 	r.HandleFunc("/api/v1/host", Authorize(true, false, "host", http.HandlerFunc(pull))).Methods(http.MethodGet)
 	r.HandleFunc("/api/v1/host", Authorize(true, false, "host", http.HandlerFunc(pull))).Methods(http.MethodGet)
 	r.HandleFunc("/api/v1/host/{hostid}/signalpeer", Authorize(true, false, "host", http.HandlerFunc(signalPeer))).Methods(http.MethodPost)
 	r.HandleFunc("/api/v1/host/{hostid}/signalpeer", Authorize(true, false, "host", http.HandlerFunc(signalPeer))).Methods(http.MethodPost)
 	r.HandleFunc("/api/v1/fallback/host/{hostid}", Authorize(true, false, "host", http.HandlerFunc(hostUpdateFallback))).Methods(http.MethodPut)
 	r.HandleFunc("/api/v1/fallback/host/{hostid}", Authorize(true, false, "host", http.HandlerFunc(hostUpdateFallback))).Methods(http.MethodPut)
+	r.HandleFunc("/api/emqx/hosts", logic.SecurityCheck(true, http.HandlerFunc(delEmqxHosts))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/v1/auth-register/host", socketHandler)
 	r.HandleFunc("/api/v1/auth-register/host", socketHandler)
 }
 }
 
 
@@ -61,7 +62,7 @@ func upgradeHost(w http.ResponseWriter, r *http.Request) {
 //	  		oauth
 //	  		oauth
 //
 //
 //			Responses:
 //			Responses:
-//				200: apiHostResponse
+//				200: apiHostSliceResponse
 func getHosts(w http.ResponseWriter, r *http.Request) {
 func getHosts(w http.ResponseWriter, r *http.Request) {
 	currentHosts, err := logic.GetAllHosts()
 	currentHosts, err := logic.GetAllHosts()
 	if err != nil {
 	if err != nil {
@@ -750,3 +751,34 @@ func syncHost(w http.ResponseWriter, r *http.Request) {
 	slog.Info("requested host pull", "user", r.Header.Get("user"), "host", host.ID)
 	slog.Info("requested host pull", "user", r.Header.Get("user"), "host", host.ID)
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 }
 }
+
+// swagger:route DELETE /api/emqx/hosts hosts delEmqxHosts
+//
+// Lists all hosts.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: apiHostResponse
+func delEmqxHosts(w http.ResponseWriter, r *http.Request) {
+	currentHosts, err := logic.GetAllHosts()
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "failed to fetch hosts: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	for _, host := range currentHosts {
+		// delete EMQX credentials for host
+		if err := mq.GetEmqxHandler().DeleteEmqxUser(host.ID.String()); err != nil {
+			slog.Error("failed to remove host credentials from EMQX", "id", host.ID, "error", err)
+		}
+	}
+	err = mq.GetEmqxHandler().DeleteEmqxUser(servercfg.GetMqUserName())
+	if err != nil {
+		slog.Error("failed to remove server credentials from EMQX", "user", servercfg.GetMqUserName(), "error", err)
+	}
+	logic.ReturnSuccessResponse(w, r, "deleted hosts data on emqx")
+}

+ 139 - 0
controllers/user.go

@@ -9,6 +9,7 @@ import (
 	"github.com/gorilla/mux"
 	"github.com/gorilla/mux"
 	"github.com/gorilla/websocket"
 	"github.com/gorilla/websocket"
 	"github.com/gravitl/netmaker/auth"
 	"github.com/gravitl/netmaker/auth"
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
@@ -35,6 +36,11 @@ func userHandlers(r *mux.Router) {
 	r.HandleFunc("/api/oauth/callback", auth.HandleAuthCallback).Methods(http.MethodGet)
 	r.HandleFunc("/api/oauth/callback", auth.HandleAuthCallback).Methods(http.MethodGet)
 	r.HandleFunc("/api/oauth/headless", auth.HandleHeadlessSSO)
 	r.HandleFunc("/api/oauth/headless", auth.HandleHeadlessSSO)
 	r.HandleFunc("/api/oauth/register/{regKey}", auth.RegisterHostSSO).Methods(http.MethodGet)
 	r.HandleFunc("/api/oauth/register/{regKey}", auth.RegisterHostSSO).Methods(http.MethodGet)
+	r.HandleFunc("/api/users_pending", logic.SecurityCheck(true, http.HandlerFunc(getPendingUsers))).Methods(http.MethodGet)
+	r.HandleFunc("/api/users_pending", logic.SecurityCheck(true, http.HandlerFunc(deleteAllPendingUsers))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/users_pending/user/{username}", logic.SecurityCheck(true, http.HandlerFunc(deletePendingUser))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/users_pending/user/{username}", logic.SecurityCheck(true, http.HandlerFunc(approvePendingUser))).Methods(http.MethodPost)
+
 }
 }
 
 
 // swagger:route POST /api/users/adm/authenticate authenticate authenticateUser
 // swagger:route POST /api/users/adm/authenticate authenticate authenticateUser
@@ -583,3 +589,136 @@ func socketHandler(w http.ResponseWriter, r *http.Request) {
 	// Start handling the session
 	// Start handling the session
 	go auth.SessionHandler(conn)
 	go auth.SessionHandler(conn)
 }
 }
+
+// swagger:route GET /api/users_pending user getPendingUsers
+//
+// Get all pending users.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
+func getPendingUsers(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+
+	users, err := logic.ListPendingUsers()
+	if err != nil {
+		logger.Log(0, "failed to fetch users: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+
+	logic.SortUsers(users[:])
+	logger.Log(2, r.Header.Get("user"), "fetched pending users")
+	json.NewEncoder(w).Encode(users)
+}
+
+// swagger:route POST /api/users_pending/user/{username} user approvePendingUser
+//
+// approve pending user.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
+func approvePendingUser(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+	var params = mux.Vars(r)
+	username := params["username"]
+	users, err := logic.ListPendingUsers()
+
+	if err != nil {
+		logger.Log(0, "failed to fetch users: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	for _, user := range users {
+		if user.UserName == username {
+			var newPass, fetchErr = auth.FetchPassValue("")
+			if fetchErr != nil {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(fetchErr, "internal"))
+				return
+			}
+			if err = logic.CreateUser(&models.User{
+				UserName: user.UserName,
+				Password: newPass,
+			}); err != nil {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to create user: %s", err), "internal"))
+				return
+			}
+			err = logic.DeletePendingUser(username)
+			if err != nil {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to delete pending user: %s", err), "internal"))
+				return
+			}
+			break
+		}
+	}
+	logic.ReturnSuccessResponse(w, r, "approved "+username)
+}
+
+// swagger:route DELETE /api/users_pending/user/{username} user deletePendingUser
+//
+// delete pending user.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
+func deletePendingUser(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+	var params = mux.Vars(r)
+	username := params["username"]
+	users, err := logic.ListPendingUsers()
+
+	if err != nil {
+		logger.Log(0, "failed to fetch users: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	for _, user := range users {
+		if user.UserName == username {
+			err = logic.DeletePendingUser(username)
+			if err != nil {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to delete pending user: %s", err), "internal"))
+				return
+			}
+			break
+		}
+	}
+	logic.ReturnSuccessResponse(w, r, "deleted pending "+username)
+}
+
+// swagger:route DELETE /api/users_pending/{username}/pending user deleteAllPendingUsers
+//
+// delete all pending users.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
+func deleteAllPendingUsers(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+	err := database.DeleteAllRecords(database.PENDING_USERS_TABLE_NAME)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to delete all pending users "+err.Error()), "internal"))
+		return
+	}
+	logic.ReturnSuccessResponse(w, r, "cleared all pending users")
+}

+ 3 - 1
database/database.go

@@ -61,7 +61,8 @@ const (
 	ENROLLMENT_KEYS_TABLE_NAME = "enrollmentkeys"
 	ENROLLMENT_KEYS_TABLE_NAME = "enrollmentkeys"
 	// HOST_ACTIONS_TABLE_NAME - table name for enrollmentkeys
 	// HOST_ACTIONS_TABLE_NAME - table name for enrollmentkeys
 	HOST_ACTIONS_TABLE_NAME = "hostactions"
 	HOST_ACTIONS_TABLE_NAME = "hostactions"
-
+	// PENDING_USERS_TABLE_NAME - table name for pending users
+	PENDING_USERS_TABLE_NAME = "pending_users"
 	// == ERROR CONSTS ==
 	// == ERROR CONSTS ==
 	// NO_RECORD - no singular result found
 	// NO_RECORD - no singular result found
 	NO_RECORD = "no result found"
 	NO_RECORD = "no result found"
@@ -144,6 +145,7 @@ func createTables() {
 	CreateTable(HOSTS_TABLE_NAME)
 	CreateTable(HOSTS_TABLE_NAME)
 	CreateTable(ENROLLMENT_KEYS_TABLE_NAME)
 	CreateTable(ENROLLMENT_KEYS_TABLE_NAME)
 	CreateTable(HOST_ACTIONS_TABLE_NAME)
 	CreateTable(HOST_ACTIONS_TABLE_NAME)
+	CreateTable(PENDING_USERS_TABLE_NAME)
 }
 }
 
 
 func CreateTable(tableName string) error {
 func CreateTable(tableName string) error {

+ 0 - 1
logic/jwts.go

@@ -106,7 +106,6 @@ func VerifyUserToken(tokenString string) (username string, issuperadmin, isadmin
 		if err != nil {
 		if err != nil {
 			return "", false, false, err
 			return "", false, false, err
 		}
 		}
-
 		if user.UserName != "" {
 		if user.UserName != "" {
 			return user.UserName, user.IsSuperAdmin, user.IsAdmin, nil
 			return user.UserName, user.IsSuperAdmin, user.IsAdmin, nil
 		}
 		}

+ 34 - 12
logic/telemetry.go

@@ -2,6 +2,7 @@ package logic
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
+	"os"
 	"time"
 	"time"
 
 
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
@@ -9,6 +10,7 @@ import (
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/posthog/posthog-go"
 	"github.com/posthog/posthog-go"
+	"golang.org/x/exp/slog"
 )
 )
 
 
 // flags to keep for telemetry
 // flags to keep for telemetry
@@ -39,14 +41,18 @@ func sendTelemetry() error {
 	// get telemetry data
 	// get telemetry data
 	d, err := FetchTelemetryData()
 	d, err := FetchTelemetryData()
 	if err != nil {
 	if err != nil {
-		return err
+		slog.Error("error fetching telemetry data", "error", err)
 	}
 	}
+	// get tenant admin email
+	adminEmail := os.Getenv("NM_EMAIL")
 	client, err := posthog.NewWithConfig(posthog_pub_key, posthog.Config{Endpoint: posthog_endpoint})
 	client, err := posthog.NewWithConfig(posthog_pub_key, posthog.Config{Endpoint: posthog_endpoint})
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 	defer client.Close()
 	defer client.Close()
 
 
+	slog.Info("sending telemetry data to posthog", "data", d)
+
 	// send to posthog
 	// send to posthog
 	return client.Enqueue(posthog.Capture{
 	return client.Enqueue(posthog.Capture{
 		DistinctId: telRecord.UUID,
 		DistinctId: telRecord.UUID,
@@ -67,7 +73,11 @@ func sendTelemetry() error {
 			Set("k8s", d.Count.K8S).
 			Set("k8s", d.Count.K8S).
 			Set("version", d.Version).
 			Set("version", d.Version).
 			Set("is_ee", d.IsPro). // TODO change is_ee to is_pro for consistency, but probably needs changes in posthog
 			Set("is_ee", d.IsPro). // TODO change is_ee to is_pro for consistency, but probably needs changes in posthog
-			Set("is_free_tier", isFreeTier),
+			Set("is_free_tier", isFreeTier).
+			Set("is_pro_trial", d.IsProTrial).
+			Set("pro_trial_end_date", d.ProTrialEndDate.In(time.UTC).Format("2006-01-02")).
+			Set("admin_email", adminEmail).
+			Set("is_saas_tenant", d.IsSaasTenant),
 	})
 	})
 }
 }
 
 
@@ -87,6 +97,15 @@ func FetchTelemetryData() (telemetryData, error) {
 		data.Nodes = len(nodes)
 		data.Nodes = len(nodes)
 		data.Count = getClientCount(nodes)
 		data.Count = getClientCount(nodes)
 	}
 	}
+	endDate, err := GetTrialEndDate()
+	if err != nil {
+		logger.Log(0, "error getting trial end date", err.Error())
+	}
+	data.ProTrialEndDate = endDate
+	if endDate.After(time.Now()) {
+		data.IsProTrial = true
+	}
+	data.IsSaasTenant = servercfg.DeployedByOperator()
 	return data, err
 	return data, err
 }
 }
 
 
@@ -162,16 +181,19 @@ func getDBLength(dbname string) int {
 
 
 // telemetryData - What data to send to posthog
 // telemetryData - What data to send to posthog
 type telemetryData struct {
 type telemetryData struct {
-	Nodes      int
-	Hosts      int
-	ExtClients int
-	Users      int
-	Count      clientCount
-	Networks   int
-	Servers    int
-	Version    string
-	IsPro      bool
-	IsFreeTier bool
+	Nodes           int
+	Hosts           int
+	ExtClients      int
+	Users           int
+	Count           clientCount
+	Networks        int
+	Servers         int
+	Version         string
+	IsPro           bool
+	IsFreeTier      bool
+	IsProTrial      bool
+	ProTrialEndDate time.Time
+	IsSaasTenant    bool
 }
 }
 
 
 // clientCount - What types of netclients we're tallying
 // clientCount - What types of netclients we're tallying

+ 44 - 0
logic/users.go

@@ -75,3 +75,47 @@ func GetSuperAdmin() (models.ReturnUser, error) {
 	}
 	}
 	return models.ReturnUser{}, errors.New("superadmin not found")
 	return models.ReturnUser{}, errors.New("superadmin not found")
 }
 }
+
+func InsertPendingUser(u *models.User) error {
+	data, err := json.Marshal(u)
+	if err != nil {
+		return err
+	}
+	return database.Insert(u.UserName, string(data), database.PENDING_USERS_TABLE_NAME)
+}
+
+func DeletePendingUser(username string) error {
+	return database.DeleteRecord(database.PENDING_USERS_TABLE_NAME, username)
+}
+
+func IsPendingUser(username string) bool {
+	records, err := database.FetchRecords(database.PENDING_USERS_TABLE_NAME)
+	if err != nil {
+		return false
+
+	}
+	for _, record := range records {
+		u := models.ReturnUser{}
+		err := json.Unmarshal([]byte(record), &u)
+		if err == nil && u.UserName == username {
+			return true
+		}
+	}
+	return false
+}
+
+func ListPendingUsers() ([]models.ReturnUser, error) {
+	pendingUsers := []models.ReturnUser{}
+	records, err := database.FetchRecords(database.PENDING_USERS_TABLE_NAME)
+	if err != nil && !database.IsEmptyRecord(err) {
+		return pendingUsers, err
+	}
+	for _, record := range records {
+		u := models.ReturnUser{}
+		err = json.Unmarshal([]byte(record), &u)
+		if err == nil {
+			pendingUsers = append(pendingUsers, u)
+		}
+	}
+	return pendingUsers, nil
+}

+ 33 - 23
models/api_host.go

@@ -8,27 +8,34 @@ import (
 
 
 // ApiHost - the host struct for API usage
 // ApiHost - the host struct for API usage
 type ApiHost struct {
 type ApiHost struct {
-	ID                  string   `json:"id"`
-	Verbosity           int      `json:"verbosity"`
-	FirewallInUse       string   `json:"firewallinuse"`
-	Version             string   `json:"version"`
-	Name                string   `json:"name"`
-	OS                  string   `json:"os"`
-	Debug               bool     `json:"debug"`
-	IsStatic            bool     `json:"isstatic"`
-	ListenPort          int      `json:"listenport"`
-	WgPublicListenPort  int      `json:"wg_public_listen_port" yaml:"wg_public_listen_port"`
-	MTU                 int      `json:"mtu"                   yaml:"mtu"`
-	Interfaces          []Iface  `json:"interfaces"            yaml:"interfaces"`
-	DefaultInterface    string   `json:"defaultinterface"      yaml:"defautlinterface"`
-	EndpointIP          string   `json:"endpointip"            yaml:"endpointip"`
-	PublicKey           string   `json:"publickey"`
-	MacAddress          string   `json:"macaddress"`
-	Nodes               []string `json:"nodes"`
-	IsDefault           bool     `json:"isdefault"             yaml:"isdefault"`
-	NatType             string   `json:"nat_type"              yaml:"nat_type"`
-	PersistentKeepalive int      `json:"persistentkeepalive"   yaml:"persistentkeepalive"`
-	AutoUpdate          bool     `json:"autoupdate"              yaml:"autoupdate"`
+	ID                  string     `json:"id"`
+	Verbosity           int        `json:"verbosity"`
+	FirewallInUse       string     `json:"firewallinuse"`
+	Version             string     `json:"version"`
+	Name                string     `json:"name"`
+	OS                  string     `json:"os"`
+	Debug               bool       `json:"debug"`
+	IsStatic            bool       `json:"isstatic"`
+	ListenPort          int        `json:"listenport"`
+	WgPublicListenPort  int        `json:"wg_public_listen_port" yaml:"wg_public_listen_port"`
+	MTU                 int        `json:"mtu"                   yaml:"mtu"`
+	Interfaces          []ApiIface `json:"interfaces"            yaml:"interfaces"`
+	DefaultInterface    string     `json:"defaultinterface"      yaml:"defautlinterface"`
+	EndpointIP          string     `json:"endpointip"            yaml:"endpointip"`
+	PublicKey           string     `json:"publickey"`
+	MacAddress          string     `json:"macaddress"`
+	Nodes               []string   `json:"nodes"`
+	IsDefault           bool       `json:"isdefault"             yaml:"isdefault"`
+	NatType             string     `json:"nat_type"              yaml:"nat_type"`
+	PersistentKeepalive int        `json:"persistentkeepalive"   yaml:"persistentkeepalive"`
+	AutoUpdate          bool       `json:"autoupdate"              yaml:"autoupdate"`
+}
+
+// ApiIface - the interface struct for API usage
+// The original Iface struct contains a net.Address, which does not get marshalled correctly
+type ApiIface struct {
+	Name          string `json:"name"`
+	AddressString string `json:"addressString"`
 }
 }
 
 
 // Host.ConvertNMHostToAPI - converts a Netmaker host to an API editable host
 // Host.ConvertNMHostToAPI - converts a Netmaker host to an API editable host
@@ -38,9 +45,12 @@ func (h *Host) ConvertNMHostToAPI() *ApiHost {
 	a.EndpointIP = h.EndpointIP.String()
 	a.EndpointIP = h.EndpointIP.String()
 	a.FirewallInUse = h.FirewallInUse
 	a.FirewallInUse = h.FirewallInUse
 	a.ID = h.ID.String()
 	a.ID = h.ID.String()
-	a.Interfaces = h.Interfaces
+	a.Interfaces = make([]ApiIface, len(h.Interfaces))
 	for i := range a.Interfaces {
 	for i := range a.Interfaces {
-		a.Interfaces[i].AddressString = a.Interfaces[i].Address.String()
+		a.Interfaces[i] = ApiIface{
+			Name:          h.Interfaces[i].Name,
+			AddressString: h.Interfaces[i].Address.String(),
+		}
 	}
 	}
 	a.DefaultInterface = h.DefaultInterface
 	a.DefaultInterface = h.DefaultInterface
 	a.IsStatic = h.IsStatic
 	a.IsStatic = h.IsStatic

+ 2 - 1
mq/mq.go

@@ -33,8 +33,9 @@ func setMqOptions(user, password string, opts *mqtt.ClientOptions) {
 	opts.SetPassword(password)
 	opts.SetPassword(password)
 	opts.SetAutoReconnect(true)
 	opts.SetAutoReconnect(true)
 	opts.SetConnectRetry(true)
 	opts.SetConnectRetry(true)
-	opts.SetConnectRetryInterval(time.Second << 2)
+	opts.SetConnectRetryInterval(time.Second * 4)
 	opts.SetKeepAlive(time.Minute)
 	opts.SetKeepAlive(time.Minute)
+	opts.SetCleanSession(true)
 	opts.SetWriteTimeout(time.Minute)
 	opts.SetWriteTimeout(time.Minute)
 }
 }
 
 

+ 5 - 2
pro/controllers/metrics.go

@@ -2,9 +2,11 @@ package controllers
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
-	proLogic "github.com/gravitl/netmaker/pro/logic"
 	"net/http"
 	"net/http"
 
 
+	proLogic "github.com/gravitl/netmaker/pro/logic"
+	"golang.org/x/exp/slog"
+
 	"github.com/gorilla/mux"
 	"github.com/gorilla/mux"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
@@ -122,12 +124,13 @@ func getNetworkExtMetrics(w http.ResponseWriter, r *http.Request) {
 				continue
 				continue
 			}
 			}
 			// if metrics for that client have been reported, append them
 			// if metrics for that client have been reported, append them
-			if len(ingressMetrics.Connectivity[clients[j].ClientID].NodeName) > 0 {
+			if _, ok := ingressMetrics.Connectivity[clients[j].ClientID]; ok {
 				networkMetrics.Connectivity[clients[j].ClientID] = ingressMetrics.Connectivity[clients[j].ClientID]
 				networkMetrics.Connectivity[clients[j].ClientID] = ingressMetrics.Connectivity[clients[j].ClientID]
 			}
 			}
 		}
 		}
 	}
 	}
 
 
+	slog.Debug("sending collected client metrics", "metrics", networkMetrics.Connectivity)
 	logger.Log(1, r.Header.Get("user"), "fetched ext client metrics for network", network)
 	logger.Log(1, r.Header.Get("user"), "fetched ext client metrics for network", network)
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(networkMetrics.Connectivity)
 	json.NewEncoder(w).Encode(networkMetrics.Connectivity)

+ 13 - 4
pro/controllers/middleware.go

@@ -1,16 +1,25 @@
 package controllers
 package controllers
 
 
 import (
 import (
+	"net/http"
+
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
-	"net/http"
 )
 )
 
 
+var limitedApis = map[string]struct{}{
+	"/api/server/status":          {},
+	"/api/emqx/hosts":             {},
+	"/api/users/adm/authenticate": {},
+}
+
 func OnlyServerAPIWhenUnlicensedMiddleware(handler http.Handler) http.Handler {
 func OnlyServerAPIWhenUnlicensedMiddleware(handler http.Handler) http.Handler {
 	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
 	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
-		if servercfg.ErrLicenseValidation != nil && request.URL.Path != "/api/server/status" {
-			logic.ReturnErrorResponse(writer, request, logic.FormatError(servercfg.ErrLicenseValidation, "forbidden"))
-			return
+		if servercfg.ErrLicenseValidation != nil {
+			if _, ok := limitedApis[request.URL.Path]; !ok {
+				logic.ReturnErrorResponse(writer, request, logic.FormatError(servercfg.ErrLicenseValidation, "forbidden"))
+				return
+			}
 		}
 		}
 		handler.ServeHTTP(writer, request)
 		handler.ServeHTTP(writer, request)
 	})
 	})

+ 1 - 1
pro/license.go

@@ -246,7 +246,7 @@ func validateLicenseKey(encryptedData []byte, publicKey *[32]byte) ([]byte, bool
 	slog.Warn(err.Error())
 	slog.Warn(err.Error())
 
 
 	// try to use cache if we had a temporary error
 	// try to use cache if we had a temporary error
-	if code == http.StatusServiceUnavailable || code == http.StatusGatewayTimeout {
+	if code == http.StatusServiceUnavailable || code == http.StatusGatewayTimeout || code == http.StatusBadGateway {
 		slog.Warn("Netmaker API may be down, will retry later...", "code", code)
 		slog.Warn("Netmaker API may be down, will retry later...", "code", code)
 		return nil, true, nil
 		return nil, true, nil
 	}
 	}

+ 8 - 19
pro/logic/metrics.go

@@ -104,7 +104,6 @@ func MQUpdateMetrics(client mqtt.Client, msg mqtt.Message) {
 }
 }
 
 
 func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) {
 func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) {
-
 	oldMetrics, err := logic.GetMetrics(currentNode.ID.String())
 	oldMetrics, err := logic.GetMetrics(currentNode.ID.String())
 	if err != nil {
 	if err != nil {
 		slog.Error("error finding old metrics for node", "id", currentNode.ID, "error", err)
 		slog.Error("error finding old metrics for node", "id", currentNode.ID, "error", err)
@@ -121,21 +120,13 @@ func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) {
 	if newMetrics.Connectivity == nil {
 	if newMetrics.Connectivity == nil {
 		newMetrics.Connectivity = make(map[string]models.Metric)
 		newMetrics.Connectivity = make(map[string]models.Metric)
 	}
 	}
-	if len(attachedClients) > 0 {
-		// associate ext clients with IDs
-		for i := range attachedClients {
-			extMetric := newMetrics.Connectivity[attachedClients[i].PublicKey]
-			if len(extMetric.NodeName) == 0 &&
-				len(newMetrics.Connectivity[attachedClients[i].ClientID].NodeName) > 0 { // cover server clients
-				extMetric = newMetrics.Connectivity[attachedClients[i].ClientID]
-				if extMetric.TotalReceived > 0 && extMetric.TotalSent > 0 {
-					extMetric.Connected = true
-				}
-			}
-			extMetric.NodeName = attachedClients[i].ClientID
-			delete(newMetrics.Connectivity, attachedClients[i].PublicKey)
-			newMetrics.Connectivity[attachedClients[i].ClientID] = extMetric
-		}
+	for i := range attachedClients {
+		slog.Debug("[metrics] processing attached client", "client", attachedClients[i].ClientID, "public key", attachedClients[i].PublicKey)
+		clientMetric := newMetrics.Connectivity[attachedClients[i].PublicKey]
+		clientMetric.NodeName = attachedClients[i].ClientID
+		newMetrics.Connectivity[attachedClients[i].ClientID] = clientMetric
+		delete(newMetrics.Connectivity, attachedClients[i].PublicKey)
+		slog.Debug("[metrics] attached client metric", "metric", clientMetric)
 	}
 	}
 
 
 	// run through metrics for each peer
 	// run through metrics for each peer
@@ -168,7 +159,5 @@ func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) {
 
 
 	}
 	}
 
 
-	for k := range oldMetrics.Connectivity { // cleanup any left over data, self healing
-		delete(newMetrics.Connectivity, k)
-	}
+	slog.Debug("[metrics] node metrics data", "node ID", currentNode.ID, "metrics", newMetrics)
 }
 }

+ 1 - 4
pro/trial.go

@@ -42,10 +42,7 @@ const trial_data_key = "trialdata"
 
 
 // stores trial end date
 // stores trial end date
 func initTrial() error {
 func initTrial() error {
-	telData, err := logic.FetchTelemetryData()
-	if err != nil {
-		return err
-	}
+	telData, _ := logic.FetchTelemetryData()
 	if telData.Hosts > 0 || telData.Networks > 0 || telData.Users > 0 {
 	if telData.Hosts > 0 || telData.Networks > 0 || telData.Users > 0 {
 		return nil // database is already populated, so skip creating trial
 		return nil // database is already populated, so skip creating trial
 	}
 	}

+ 3 - 1
scripts/netmaker.default.env

@@ -53,6 +53,8 @@ TELEMETRY=on
 # OAuth section
 # OAuth section
 #
 #
 ###
 ###
+# only mentioned domains will be allowded to signup using oauth, by default all domains are allowed
+ALLOWED_EMAIL_DOMAINS=*
 # "<azure-ad|github|google|oidc>"
 # "<azure-ad|github|google|oidc>"
 AUTH_PROVIDER=
 AUTH_PROVIDER=
 # "<client id of your oauth provider>"
 # "<client id of your oauth provider>"
@@ -72,4 +74,4 @@ RAC_AUTO_DISABLE=true
 # if turned on data will be cached on to improve performance significantly (IMPORTANT: If HA set to `false` )
 # if turned on data will be cached on to improve performance significantly (IMPORTANT: If HA set to `false` )
 CACHING_ENABLED=true
 CACHING_ENABLED=true
 # if turned on netclient checks if peers are reachable over private/LAN address, and choose that as peer endpoint
 # if turned on netclient checks if peers are reachable over private/LAN address, and choose that as peer endpoint
-ENDPOINT_DETECTION=true
+ENDPOINT_DETECTION=true

+ 5 - 19
scripts/nm-quick.sh

@@ -248,7 +248,7 @@ save_config() { (
 	local toCopy=("SERVER_HOST" "MASTER_KEY" "MQ_USERNAME" "MQ_PASSWORD"
 	local toCopy=("SERVER_HOST" "MASTER_KEY" "MQ_USERNAME" "MQ_PASSWORD"
 		"INSTALL_TYPE" "NODE_ID" "DNS_MODE" "NETCLIENT_AUTO_UPDATE" "API_PORT"
 		"INSTALL_TYPE" "NODE_ID" "DNS_MODE" "NETCLIENT_AUTO_UPDATE" "API_PORT"
 		"CORS_ALLOWED_ORIGIN" "DISPLAY_KEYS" "DATABASE" "SERVER_BROKER_ENDPOINT" "VERBOSITY"
 		"CORS_ALLOWED_ORIGIN" "DISPLAY_KEYS" "DATABASE" "SERVER_BROKER_ENDPOINT" "VERBOSITY"
-		"DEBUG_MODE"  "REST_BACKEND" "DISABLE_REMOTE_IP_CHECK" "TELEMETRY" "AUTH_PROVIDER" "CLIENT_ID" "CLIENT_SECRET"
+		"DEBUG_MODE"  "REST_BACKEND" "DISABLE_REMOTE_IP_CHECK" "TELEMETRY" "ALLOWED_EMAIL_DOMAINS" "AUTH_PROVIDER" "CLIENT_ID" "CLIENT_SECRET"
 		"FRONTEND_URL" "AZURE_TENANT" "OIDC_ISSUER" "EXPORTER_API_PORT" "JWT_VALIDITY_DURATION" "RAC_AUTO_DISABLE" "CACHING_ENABLED" "ENDPOINT_DETECTION")
 		"FRONTEND_URL" "AZURE_TENANT" "OIDC_ISSUER" "EXPORTER_API_PORT" "JWT_VALIDITY_DURATION" "RAC_AUTO_DISABLE" "CACHING_ENABLED" "ENDPOINT_DETECTION")
 	for name in "${toCopy[@]}"; do
 	for name in "${toCopy[@]}"; do
 		save_config_item $name "${!name}"
 		save_config_item $name "${!name}"
@@ -470,23 +470,11 @@ set_install_vars() {
 
 
 	wait_seconds 1
 	wait_seconds 1
 
 
-
 	unset GET_EMAIL
 	unset GET_EMAIL
-	unset RAND_EMAIL
-	RAND_EMAIL="$(echo $RANDOM | md5sum | head -c 16)@email.com"
-	# suggest the prev email or a random one
-	EMAIL_SUGGESTED=${NM_EMAIL:-$RAND_EMAIL}
-	read -p "Email Address for Domain Registration (click 'enter' to use $EMAIL_SUGGESTED): " GET_EMAIL
-	if [ -z "$GET_EMAIL" ]; then
-		EMAIL="$EMAIL_SUGGESTED"
-		if [ "$EMAIL" = "$NM_EMAIL" ]; then
-			echo "using config email"
-		else
-			echo "using rand email"
-		fi
-	else
-		EMAIL="$GET_EMAIL"
-	fi
+	while [ -z "$GET_EMAIL" ]; do
+		read -p "Email Address for Domain Registration: " GET_EMAIL
+	done
+	EMAIL="$GET_EMAIL"
 
 
 	wait_seconds 1
 	wait_seconds 1
 
 
@@ -592,8 +580,6 @@ install_netmaker() {
 
 
 	echo "Starting containers..."
 	echo "Starting containers..."
 
 
-	
-
 	# start docker and rebuild containers / networks
 	# start docker and rebuild containers / networks
 	cd "${SCRIPT_DIR}"
 	cd "${SCRIPT_DIR}"
 	docker-compose up -d --force-recreate
 	docker-compose up -d --force-recreate

+ 11 - 0
servercfg/serverconf.go

@@ -714,3 +714,14 @@ func GetEmqxAppID() string {
 func GetEmqxAppSecret() string {
 func GetEmqxAppSecret() string {
 	return os.Getenv("EMQX_APP_SECRET")
 	return os.Getenv("EMQX_APP_SECRET")
 }
 }
+
+// GetAllowedEmailDomains - gets the allowed email domains for oauth signup
+func GetAllowedEmailDomains() string {
+	allowedDomains := "*"
+	if os.Getenv("ALLOWED_EMAIL_DOMAINS") != "" {
+		allowedDomains = os.Getenv("ALLOWED_EMAIL_DOMAINS")
+	} else if config.Config.Server.AllowedEmailDomains != "" {
+		allowedDomains = config.Config.Server.AllowedEmailDomains
+	}
+	return allowedDomains
+}

+ 142 - 9
swagger.yml

@@ -69,7 +69,7 @@ definitions:
                 x-go-name: ID
                 x-go-name: ID
             interfaces:
             interfaces:
                 items:
                 items:
-                    $ref: '#/definitions/Iface'
+                    $ref: '#/definitions/ApiIface'
                 type: array
                 type: array
                 x-go-name: Interfaces
                 x-go-name: Interfaces
             isdefault:
             isdefault:
@@ -123,6 +123,139 @@ definitions:
                 x-go-name: WgPublicListenPort
                 x-go-name: WgPublicListenPort
         type: object
         type: object
         x-go-package: github.com/gravitl/netmaker/models
         x-go-package: github.com/gravitl/netmaker/models
+    ApiIface:
+        description: |-
+            ApiIface - the interface struct for API usage
+            The original Iface struct contains a net.Address, which does not get marshalled correctly
+        properties:
+            addressString:
+                type: string
+                x-go-name: AddressString
+            name:
+                type: string
+                x-go-name: Name
+        type: object
+        x-go-package: github.com/gravitl/netmaker/models
+    ApiNode:
+        description: ApiNode is a stripped down Node DTO that exposes only required fields to external systems
+        properties:
+            address:
+                type: string
+                x-go-name: Address
+            address6:
+                type: string
+                x-go-name: Address6
+            allowedips:
+                items:
+                    type: string
+                type: array
+                x-go-name: AllowedIPs
+            connected:
+                type: boolean
+                x-go-name: Connected
+            defaultacl:
+                description: == PRO ==
+                type: string
+                x-go-name: DefaultACL
+            dnson:
+                type: boolean
+                x-go-name: DNSOn
+            egressgatewaynatenabled:
+                type: boolean
+                x-go-name: EgressGatewayNatEnabled
+            egressgatewayranges:
+                items:
+                    type: string
+                type: array
+                x-go-name: EgressGatewayRanges
+            expdatetime:
+                format: int64
+                type: integer
+                x-go-name: ExpirationDateTime
+            fail_over_peers:
+                additionalProperties:
+                    type: object
+                type: object
+                x-go-name: FailOverPeers
+            failed_over_by:
+                format: uuid
+                type: string
+                x-go-name: FailedOverBy
+            hostid:
+                type: string
+                x-go-name: HostID
+            id:
+                type: string
+                x-go-name: ID
+            inet_node_req:
+                $ref: '#/definitions/InetNodeReq'
+            ingressdns:
+                type: string
+                x-go-name: IngressDns
+            internetgw_node_id:
+                type: string
+                x-go-name: InternetGwID
+            is_fail_over:
+                type: boolean
+                x-go-name: IsFailOver
+            isegressgateway:
+                type: boolean
+                x-go-name: IsEgressGateway
+            isingressgateway:
+                type: boolean
+                x-go-name: IsIngressGateway
+            isinternetgateway:
+                type: boolean
+                x-go-name: IsInternetGateway
+            isrelay:
+                type: boolean
+                x-go-name: IsRelay
+            isrelayed:
+                type: boolean
+                x-go-name: IsRelayed
+            lastcheckin:
+                format: int64
+                type: integer
+                x-go-name: LastCheckIn
+            lastmodified:
+                format: int64
+                type: integer
+                x-go-name: LastModified
+            lastpeerupdate:
+                format: int64
+                type: integer
+                x-go-name: LastPeerUpdate
+            localaddress:
+                type: string
+                x-go-name: LocalAddress
+            metadata:
+                type: string
+                x-go-name: Metadata
+            network:
+                type: string
+                x-go-name: Network
+            networkrange:
+                type: string
+                x-go-name: NetworkRange
+            networkrange6:
+                type: string
+                x-go-name: NetworkRange6
+            pendingdelete:
+                type: boolean
+                x-go-name: PendingDelete
+            relayedby:
+                type: string
+                x-go-name: RelayedBy
+            relaynodes:
+                items:
+                    type: string
+                type: array
+                x-go-name: RelayedNodes
+            server:
+                type: string
+                x-go-name: Server
+        type: object
+        x-go-package: github.com/gravitl/netmaker/models
     AuthParams:
     AuthParams:
         description: AuthParams - struct for auth params
         description: AuthParams - struct for auth params
         properties:
         properties:
@@ -1642,7 +1775,7 @@ paths:
             operationId: getHosts
             operationId: getHosts
             responses:
             responses:
                 "200":
                 "200":
-                    $ref: '#/responses/apiHostResponse'
+                    $ref: '#/responses/apiHostSliceResponse'
             schemes:
             schemes:
                 - https
                 - https
             summary: Lists all hosts.
             summary: Lists all hosts.
@@ -1902,12 +2035,6 @@ paths:
                   required: true
                   required: true
                   type: string
                   type: string
                   x-go-name: Networkname
                   x-go-name: Networkname
-                - description: ACL Container
-                  in: body
-                  name: acl_container
-                  schema:
-                    $ref: '#/definitions/ACLContainer'
-                  x-go-name: ACLContainer
             responses:
             responses:
                 "200":
                 "200":
                     $ref: '#/responses/aclContainerResponse'
                     $ref: '#/responses/aclContainerResponse'
@@ -2725,6 +2852,12 @@ responses:
         description: ""
         description: ""
         schema:
         schema:
             $ref: '#/definitions/ApiHost'
             $ref: '#/definitions/ApiHost'
+    apiHostSliceResponse:
+        description: ""
+        schema:
+            items:
+                $ref: '#/definitions/ApiHost'
+            type: array
     byteArrayResponse:
     byteArrayResponse:
         description: ""
         description: ""
         schema:
         schema:
@@ -2776,7 +2909,7 @@ responses:
         description: ""
         description: ""
         schema:
         schema:
             items:
             items:
-                $ref: '#/definitions/LegacyNode'
+                $ref: '#/definitions/ApiNode'
             type: array
             type: array
     okResponse:
     okResponse:
         description: ""
         description: ""