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 {
 		return ""
 	}
-	var _, err = fetchPassValue(logic.RandomString(64))
+	var _, err = FetchPassValue(logic.RandomString(64))
 	if err != nil {
 		logger.Log(0, err.Error())
 		return ""
@@ -156,7 +156,7 @@ func HandleAuthLogin(w http.ResponseWriter, r *http.Request) {
 
 // IsOauthUser - returns
 func IsOauthUser(user *models.User) error {
-	var currentValue, err = fetchPassValue("")
+	var currentValue, err = FetchPassValue("")
 	if err != nil {
 		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)
 		return err
 	} // generate random password to adapt to current model
-	var newPass, fetchErr = fetchPassValue("")
+	var newPass, fetchErr = FetchPassValue("")
 	if fetchErr != nil {
 		return fetchErr
 	}
@@ -272,7 +272,7 @@ func addUser(email string) error {
 	return nil
 }
 
-func fetchPassValue(newValue string) (string, error) {
+func FetchPassValue(newValue string) (string, error) {
 
 	type valueHolder struct {
 		Value string `json:"value" bson:"value"`
@@ -334,3 +334,23 @@ func isStateCached(state string) bool {
 	_, err := netcache.Get(state)
 	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"
 	"net/http"
 
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
@@ -60,13 +61,33 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		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)
-	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
 		}
 	}
-	user, err := logic.GetUser(content.Email)
+	user, err := logic.GetUser(content.UserPrincipalName)
 	if err != nil {
 		handleOauthUserNotFound(w)
 		return
@@ -75,7 +96,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		return
 	}
-	var newPass, fetchErr = fetchPassValue("")
+	var newPass, fetchErr = FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}

+ 26 - 1
auth/error.go

@@ -13,7 +13,8 @@ const oauthNotConfigured = `<!DOCTYPE html><html>
 const userNotAllowed = `<!DOCTYPE html><html>
 <body>
 <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>
 </html>
 `
@@ -23,6 +24,18 @@ const userNotFound = `<!DOCTYPE html><html>
 </body>
 </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) {
 	response.Header().Set("Content-Type", "text/html; charset=utf-8")
 	response.WriteHeader(http.StatusNotFound)
@@ -35,9 +48,21 @@ func handleOauthUserNotAllowed(response http.ResponseWriter) {
 	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
 func handleOauthNotConfigured(response http.ResponseWriter) {
 	response.Header().Set("Content-Type", "text/html; charset=utf-8")
 	response.WriteHeader(http.StatusInternalServerError)
 	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"
 	"net/http"
 
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
@@ -60,13 +61,33 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		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)
-	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
 		}
 	}
-	user, err := logic.GetUser(content.Email)
+	user, err := logic.GetUser(content.Login)
 	if err != nil {
 		handleOauthUserNotFound(w)
 		return
@@ -75,7 +96,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		return
 	}
-	var newPass, fetchErr = fetchPassValue("")
+	var newPass, fetchErr = FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}

+ 24 - 3
auth/google.go

@@ -8,6 +8,7 @@ import (
 	"net/http"
 	"time"
 
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
@@ -62,9 +63,29 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		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)
-	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
 		}
 	}
@@ -77,7 +98,7 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		return
 	}
-	var newPass, fetchErr = fetchPassValue("")
+	var newPass, fetchErr = FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}

+ 13 - 8
auth/headless_callback.go

@@ -50,19 +50,24 @@ func HandleHeadlessSSOCallback(w http.ResponseWriter, r *http.Request) {
 		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 {
 		return
 	}
 	jwt, jwtErr := logic.VerifyAuthRequest(models.UserAuthParams{
-		UserName: userClaims.getUserName(),
+		UserName: user.UserName,
 		Password: newPass,
 	})
 	if jwtErr != nil {

+ 24 - 3
auth/oidc.go

@@ -7,6 +7,7 @@ import (
 	"time"
 
 	"github.com/coreos/go-oidc/v3/oidc"
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
@@ -73,9 +74,29 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		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)
-	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
 		}
 	}
@@ -88,7 +109,7 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		return
 	}
-	var newPass, fetchErr = fetchPassValue("")
+	var newPass, fetchErr = FetchPassValue("")
 	if fetchErr != nil {
 		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/enrollment_key"
 	"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/metrics"
 	"github.com/gravitl/netmaker/cli/cmd/network"
@@ -53,4 +54,5 @@ func init() {
 	rootCmd.AddCommand(metrics.GetRoot())
 	rootCmd.AddCommand(host.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"`
 	CacheEnabled               string        `yaml:"caching_enabled"`
 	EndpointDetection          bool          `json:"endpoint_detection"`
+	AllowedEmailDomains        string        `yaml:"allowed_email_domains"`
 }
 
 // SQLConfig - Generic SQL Config

+ 9 - 2
controllers/docs.go

@@ -49,6 +49,12 @@ type hasAdmin struct {
 	Admin bool
 }
 
+// swagger:response apiHostSliceResponse
+type apiHostSliceResponse struct {
+	// in: body
+	Host []models.ApiHost
+}
+
 // swagger:response apiHostResponse
 type apiHostResponse struct {
 	// in: body
@@ -251,7 +257,7 @@ type networkBodyResponse struct {
 	Network models.Network `json:"network"`
 }
 
-// swagger:parameters updateNetworkACL getNetworkACL
+// swagger:parameters updateNetworkACL
 type aclContainerBodyParam struct {
 	// ACL Container
 	// in: body
@@ -269,7 +275,7 @@ type aclContainerResponse struct {
 type nodeSliceResponse struct {
 	// Nodes
 	// in: body
-	Nodes []models.LegacyNode `json:"nodes"`
+	Nodes []models.ApiNode `json:"nodes"`
 }
 
 // swagger:response nodeResponse
@@ -453,6 +459,7 @@ func useUnused() bool {
 	_ = userAuthBodyParam{}
 	_ = usernamePathParam{}
 	_ = hasAdmin{}
+	_ = apiHostSliceResponse{}
 	_ = apiHostResponse{}
 	_ = fileResponse{}
 	_ = 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/{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/emqx/hosts", logic.SecurityCheck(true, http.HandlerFunc(delEmqxHosts))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/v1/auth-register/host", socketHandler)
 }
 
@@ -61,7 +62,7 @@ func upgradeHost(w http.ResponseWriter, r *http.Request) {
 //	  		oauth
 //
 //			Responses:
-//				200: apiHostResponse
+//				200: apiHostSliceResponse
 func getHosts(w http.ResponseWriter, r *http.Request) {
 	currentHosts, err := logic.GetAllHosts()
 	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)
 	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/websocket"
 	"github.com/gravitl/netmaker/auth"
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"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/headless", auth.HandleHeadlessSSO)
 	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
@@ -583,3 +589,136 @@ func socketHandler(w http.ResponseWriter, r *http.Request) {
 	// Start handling the session
 	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"
 	// HOST_ACTIONS_TABLE_NAME - table name for enrollmentkeys
 	HOST_ACTIONS_TABLE_NAME = "hostactions"
-
+	// PENDING_USERS_TABLE_NAME - table name for pending users
+	PENDING_USERS_TABLE_NAME = "pending_users"
 	// == ERROR CONSTS ==
 	// NO_RECORD - no singular result found
 	NO_RECORD = "no result found"
@@ -144,6 +145,7 @@ func createTables() {
 	CreateTable(HOSTS_TABLE_NAME)
 	CreateTable(ENROLLMENT_KEYS_TABLE_NAME)
 	CreateTable(HOST_ACTIONS_TABLE_NAME)
+	CreateTable(PENDING_USERS_TABLE_NAME)
 }
 
 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 {
 			return "", false, false, err
 		}
-
 		if user.UserName != "" {
 			return user.UserName, user.IsSuperAdmin, user.IsAdmin, nil
 		}

+ 34 - 12
logic/telemetry.go

@@ -2,6 +2,7 @@ package logic
 
 import (
 	"encoding/json"
+	"os"
 	"time"
 
 	"github.com/gravitl/netmaker/database"
@@ -9,6 +10,7 @@ import (
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/posthog/posthog-go"
+	"golang.org/x/exp/slog"
 )
 
 // flags to keep for telemetry
@@ -39,14 +41,18 @@ func sendTelemetry() error {
 	// get telemetry data
 	d, err := FetchTelemetryData()
 	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})
 	if err != nil {
 		return err
 	}
 	defer client.Close()
 
+	slog.Info("sending telemetry data to posthog", "data", d)
+
 	// send to posthog
 	return client.Enqueue(posthog.Capture{
 		DistinctId: telRecord.UUID,
@@ -67,7 +73,11 @@ func sendTelemetry() error {
 			Set("k8s", d.Count.K8S).
 			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_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.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
 }
 
@@ -162,16 +181,19 @@ func getDBLength(dbname string) int {
 
 // telemetryData - What data to send to posthog
 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

+ 44 - 0
logic/users.go

@@ -75,3 +75,47 @@ func GetSuperAdmin() (models.ReturnUser, error) {
 	}
 	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
 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
@@ -38,9 +45,12 @@ func (h *Host) ConvertNMHostToAPI() *ApiHost {
 	a.EndpointIP = h.EndpointIP.String()
 	a.FirewallInUse = h.FirewallInUse
 	a.ID = h.ID.String()
-	a.Interfaces = h.Interfaces
+	a.Interfaces = make([]ApiIface, len(h.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.IsStatic = h.IsStatic

+ 2 - 1
mq/mq.go

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

+ 5 - 2
pro/controllers/metrics.go

@@ -2,9 +2,11 @@ package controllers
 
 import (
 	"encoding/json"
-	proLogic "github.com/gravitl/netmaker/pro/logic"
 	"net/http"
 
+	proLogic "github.com/gravitl/netmaker/pro/logic"
+	"golang.org/x/exp/slog"
+
 	"github.com/gorilla/mux"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
@@ -122,12 +124,13 @@ func getNetworkExtMetrics(w http.ResponseWriter, r *http.Request) {
 				continue
 			}
 			// 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]
 			}
 		}
 	}
 
+	slog.Debug("sending collected client metrics", "metrics", networkMetrics.Connectivity)
 	logger.Log(1, r.Header.Get("user"), "fetched ext client metrics for network", network)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(networkMetrics.Connectivity)

+ 13 - 4
pro/controllers/middleware.go

@@ -1,16 +1,25 @@
 package controllers
 
 import (
+	"net/http"
+
 	"github.com/gravitl/netmaker/logic"
 	"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 {
 	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)
 	})

+ 1 - 1
pro/license.go

@@ -246,7 +246,7 @@ func validateLicenseKey(encryptedData []byte, publicKey *[32]byte) ([]byte, bool
 	slog.Warn(err.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)
 		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) {
-
 	oldMetrics, err := logic.GetMetrics(currentNode.ID.String())
 	if err != nil {
 		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 {
 		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
@@ -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
 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 {
 		return nil // database is already populated, so skip creating trial
 	}

+ 3 - 1
scripts/netmaker.default.env

@@ -53,6 +53,8 @@ TELEMETRY=on
 # 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>"
 AUTH_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` )
 CACHING_ENABLED=true
 # 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"
 		"INSTALL_TYPE" "NODE_ID" "DNS_MODE" "NETCLIENT_AUTO_UPDATE" "API_PORT"
 		"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")
 	for name in "${toCopy[@]}"; do
 		save_config_item $name "${!name}"
@@ -470,23 +470,11 @@ set_install_vars() {
 
 	wait_seconds 1
 
-
 	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
 
@@ -592,8 +580,6 @@ install_netmaker() {
 
 	echo "Starting containers..."
 
-	
-
 	# start docker and rebuild containers / networks
 	cd "${SCRIPT_DIR}"
 	docker-compose up -d --force-recreate

+ 11 - 0
servercfg/serverconf.go

@@ -714,3 +714,14 @@ func GetEmqxAppID() string {
 func GetEmqxAppSecret() string {
 	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
             interfaces:
                 items:
-                    $ref: '#/definitions/Iface'
+                    $ref: '#/definitions/ApiIface'
                 type: array
                 x-go-name: Interfaces
             isdefault:
@@ -123,6 +123,139 @@ definitions:
                 x-go-name: WgPublicListenPort
         type: object
         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:
         description: AuthParams - struct for auth params
         properties:
@@ -1642,7 +1775,7 @@ paths:
             operationId: getHosts
             responses:
                 "200":
-                    $ref: '#/responses/apiHostResponse'
+                    $ref: '#/responses/apiHostSliceResponse'
             schemes:
                 - https
             summary: Lists all hosts.
@@ -1902,12 +2035,6 @@ paths:
                   required: true
                   type: string
                   x-go-name: Networkname
-                - description: ACL Container
-                  in: body
-                  name: acl_container
-                  schema:
-                    $ref: '#/definitions/ACLContainer'
-                  x-go-name: ACLContainer
             responses:
                 "200":
                     $ref: '#/responses/aclContainerResponse'
@@ -2725,6 +2852,12 @@ responses:
         description: ""
         schema:
             $ref: '#/definitions/ApiHost'
+    apiHostSliceResponse:
+        description: ""
+        schema:
+            items:
+                $ref: '#/definitions/ApiHost'
+            type: array
     byteArrayResponse:
         description: ""
         schema:
@@ -2776,7 +2909,7 @@ responses:
         description: ""
         schema:
             items:
-                $ref: '#/definitions/LegacyNode'
+                $ref: '#/definitions/ApiNode'
             type: array
     okResponse:
         description: ""