Forráskód Böngészése

Merge branch 'develop' of https://github.com/gravitl/netmaker into NET-1082

abhishek9686 1 éve
szülő
commit
9cdfebf024

+ 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
+}

+ 24 - 3
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,9 +61,29 @@ 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) {
+		handleOauthUserSignUpApprovalPending(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
+			}
+			handleFirstTimeOauthUserSignUp(w)
+			return
+		} else {
+			handleSomethingWentWrong(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
 	}

+ 51 - 1
auth/error.go

@@ -12,17 +12,44 @@ const oauthNotConfigured = `<!DOCTYPE html><html>
 
 const userNotAllowed = `<!DOCTYPE html><html>
 <body>
-<h3>Only Admins are allowed to access Dashboard.</h3>
+<h3>Only administrators can access the Dashboard. Please contact your administrator to elevate your account.</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>
 </body>
 </html>
 `
+
+const userFirstTimeSignUp = `<!DOCTYPE html><html>
+<body>
+<h3>Thank you for signing up. Please contact your administrator for access.</h3>
+</body>
+</html>
+`
+
+const userSignUpApprovalPending = `<!DOCTYPE html><html>
+<body>
+<h3>Your account is yet to be approved. Please contact your administrator for access.</h3>
+</body>
+</html>
+`
+
 const userNotFound = `<!DOCTYPE html><html>
 <body>
 <h3>User Not Found.</h3>
 </body>
 </html>`
 
+const somethingwentwrong = `<!DOCTYPE html><html>
+<body>
+<h3>Something went wrong. Contact Admin.</h3>
+</body>
+</html>`
+
+const notallowedtosignup = `<!DOCTYPE html><html>
+<body>
+<h3>Your email is not allowed. Please contact your administrator.</h3>
+</body>
+</html>`
+
 func handleOauthUserNotFound(response http.ResponseWriter) {
 	response.Header().Set("Content-Type", "text/html; charset=utf-8")
 	response.WriteHeader(http.StatusNotFound)
@@ -34,6 +61,23 @@ func handleOauthUserNotAllowed(response http.ResponseWriter) {
 	response.WriteHeader(http.StatusForbidden)
 	response.Write([]byte(userNotAllowed))
 }
+func handleFirstTimeOauthUserSignUp(response http.ResponseWriter) {
+	response.Header().Set("Content-Type", "text/html; charset=utf-8")
+	response.WriteHeader(http.StatusForbidden)
+	response.Write([]byte(userFirstTimeSignUp))
+}
+
+func handleOauthUserSignUpApprovalPending(response http.ResponseWriter) {
+	response.Header().Set("Content-Type", "text/html; charset=utf-8")
+	response.WriteHeader(http.StatusForbidden)
+	response.Write([]byte(userSignUpApprovalPending))
+}
+
+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) {
@@ -41,3 +85,9 @@ func handleOauthNotConfigured(response http.ResponseWriter) {
 	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))
+}

+ 24 - 3
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,9 +61,29 @@ 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) {
+		handleOauthUserSignUpApprovalPending(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
+			}
+			handleFirstTimeOauthUserSignUp(w)
+			return
+		} else {
+			handleSomethingWentWrong(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) {
+		handleOauthUserSignUpApprovalPending(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
+			}
+			handleFirstTimeOauthUserSignUp(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) {
+		handleOauthUserSignUpApprovalPending(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
+			}
+			handleFirstTimeOauthUserSignUp(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
 	}

+ 2 - 0
config/config.go

@@ -92,6 +92,8 @@ type ServerConfig struct {
 	JwtValidityDuration        time.Duration `yaml:"jwt_validity_duration"`
 	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{}

+ 13 - 12
controllers/hosts.go

@@ -62,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 {
@@ -134,17 +134,18 @@ func pull(w http.ResponseWriter, r *http.Request) {
 
 	serverConf.TrafficKey = key
 	response := models.HostPull{
-		Host:            *host,
-		Nodes:           logic.GetHostNodes(host),
-		ServerConfig:    serverConf,
-		Peers:           hPU.Peers,
-		PeerIDs:         hPU.PeerIDs,
-		HostNetworkInfo: hPU.HostNetworkInfo,
-		EgressRoutes:    hPU.EgressRoutes,
-		FwUpdate:        hPU.FwUpdate,
-		ChangeDefaultGw: hPU.ChangeDefaultGw,
-		DefaultGwIp:     hPU.DefaultGwIp,
-		IsInternetGw:    hPU.IsInternetGw,
+		Host:              *host,
+		Nodes:             logic.GetHostNodes(host),
+		ServerConfig:      serverConf,
+		Peers:             hPU.Peers,
+		PeerIDs:           hPU.PeerIDs,
+		HostNetworkInfo:   hPU.HostNetworkInfo,
+		EgressRoutes:      hPU.EgressRoutes,
+		FwUpdate:          hPU.FwUpdate,
+		ChangeDefaultGw:   hPU.ChangeDefaultGw,
+		DefaultGwIp:       hPU.DefaultGwIp,
+		IsInternetGw:      hPU.IsInternetGw,
+		EndpointDetection: servercfg.IsEndpointDetectionEnabled(),
 	}
 
 	logger.Log(1, hostID, "completed a pull")

+ 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
 		}

+ 5 - 4
logic/peers.go

@@ -72,10 +72,11 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 		FwUpdate: models.FwUpdate{
 			EgressInfo: make(map[string]models.EgressInfo),
 		},
-		PeerIDs:         make(models.PeerMap, 0),
-		Peers:           []wgtypes.PeerConfig{},
-		NodePeers:       []wgtypes.PeerConfig{},
-		HostNetworkInfo: models.HostInfoMap{},
+		PeerIDs:           make(models.PeerMap, 0),
+		Peers:             []wgtypes.PeerConfig{},
+		NodePeers:         []wgtypes.PeerConfig{},
+		HostNetworkInfo:   models.HostInfoMap{},
+		EndpointDetection: servercfg.IsEndpointDetectionEnabled(),
 	}
 
 	slog.Debug("peer update for host", "hostId", host.ID.String())

+ 7 - 15
logic/telemetry.go

@@ -39,10 +39,7 @@ func sendTelemetry() error {
 		return err
 	}
 	// get telemetry data
-	d, err := FetchTelemetryData()
-	if err != nil {
-		slog.Error("error fetching telemetry data", "error", err)
-	}
+	d := FetchTelemetryData()
 	// get tenant admin email
 	adminEmail := os.Getenv("NM_EMAIL")
 	client, err := posthog.NewWithConfig(posthog_pub_key, posthog.Config{Endpoint: posthog_endpoint})
@@ -82,7 +79,7 @@ func sendTelemetry() error {
 }
 
 // FetchTelemetryData - fetches telemetry data: count of various object types in DB
-func FetchTelemetryData() (telemetryData, error) {
+func FetchTelemetryData() telemetryData {
 	var data telemetryData
 
 	data.IsPro = servercfg.IsPro
@@ -92,21 +89,16 @@ func FetchTelemetryData() (telemetryData, error) {
 	data.Hosts = getDBLength(database.HOSTS_TABLE_NAME)
 	data.Version = servercfg.GetVersion()
 	data.Servers = getServerCount()
-	nodes, err := GetAllNodes()
-	if err == nil {
-		data.Nodes = len(nodes)
-		data.Count = getClientCount(nodes)
-	}
-	endDate, err := GetTrialEndDate()
-	if err != nil {
-		logger.Log(0, "error getting trial end date", err.Error())
-	}
+	nodes, _ := GetAllNodes()
+	data.Nodes = len(nodes)
+	data.Count = getClientCount(nodes)
+	endDate, _ := GetTrialEndDate()
 	data.ProTrialEndDate = endDate
 	if endDate.After(time.Now()) {
 		data.IsProTrial = true
 	}
 	data.IsSaasTenant = servercfg.DeployedByOperator()
-	return data, err
+	return data
 }
 
 // getServerCount returns number of servers from database

+ 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
+}

+ 1 - 1
migrate/migrate.go

@@ -177,7 +177,7 @@ func removeInterGw(egressRanges []string) ([]string, bool) {
 func updateAcls() {
 	// get all networks
 	networks, err := logic.GetNetworks()
-	if err != nil {
+	if err != nil && !database.IsEmptyRecord(err) {
 		slog.Error("acls migration failed. error getting networks", "error", err)
 		return
 	}

+ 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

+ 16 - 15
models/mqtt.go

@@ -8,21 +8,22 @@ import (
 
 // HostPeerUpdate - struct for host peer updates
 type HostPeerUpdate struct {
-	Host            Host                 `json:"host" bson:"host" yaml:"host"`
-	ChangeDefaultGw bool                 `json:"change_default_gw"`
-	DefaultGwIp     net.IP               `json:"default_gw_ip"`
-	IsInternetGw    bool                 `json:"is_inet_gw"`
-	NodeAddrs       []net.IPNet          `json:"nodes_addrs" yaml:"nodes_addrs"`
-	Server          string               `json:"server" bson:"server" yaml:"server"`
-	ServerVersion   string               `json:"serverversion" bson:"serverversion" yaml:"serverversion"`
-	ServerAddrs     []ServerAddr         `json:"serveraddrs" bson:"serveraddrs" yaml:"serveraddrs"`
-	NodePeers       []wgtypes.PeerConfig `json:"peers" bson:"peers" yaml:"peers"`
-	Peers           []wgtypes.PeerConfig
-	PeerIDs         PeerMap               `json:"peerids" bson:"peerids" yaml:"peerids"`
-	HostNetworkInfo HostInfoMap           `json:"host_network_info,omitempty" bson:"host_network_info,omitempty" yaml:"host_network_info,omitempty"`
-	EgressRoutes    []EgressNetworkRoutes `json:"egress_network_routes"`
-	FwUpdate        FwUpdate              `json:"fw_update"`
-	ReplacePeers    bool                  `json:"replace_peers"`
+	Host              Host                 `json:"host" bson:"host" yaml:"host"`
+	ChangeDefaultGw   bool                 `json:"change_default_gw"`
+	DefaultGwIp       net.IP               `json:"default_gw_ip"`
+	IsInternetGw      bool                 `json:"is_inet_gw"`
+	NodeAddrs         []net.IPNet          `json:"nodes_addrs" yaml:"nodes_addrs"`
+	Server            string               `json:"server" bson:"server" yaml:"server"`
+	ServerVersion     string               `json:"serverversion" bson:"serverversion" yaml:"serverversion"`
+	ServerAddrs       []ServerAddr         `json:"serveraddrs" bson:"serveraddrs" yaml:"serveraddrs"`
+	NodePeers         []wgtypes.PeerConfig `json:"peers" bson:"peers" yaml:"peers"`
+	Peers             []wgtypes.PeerConfig
+	PeerIDs           PeerMap               `json:"peerids" bson:"peerids" yaml:"peerids"`
+	HostNetworkInfo   HostInfoMap           `json:"host_network_info,omitempty" bson:"host_network_info,omitempty" yaml:"host_network_info,omitempty"`
+	EgressRoutes      []EgressNetworkRoutes `json:"egress_network_routes"`
+	FwUpdate          FwUpdate              `json:"fw_update"`
+	ReplacePeers      bool                  `json:"replace_peers"`
+	EndpointDetection bool                  `json:"endpoint_detection"`
 }
 
 // IngressInfo - struct for ingress info

+ 12 - 11
models/structs.go

@@ -232,17 +232,18 @@ type TrafficKeys struct {
 
 // HostPull - response of a host's pull
 type HostPull struct {
-	Host            Host                  `json:"host" yaml:"host"`
-	Nodes           []Node                `json:"nodes" yaml:"nodes"`
-	Peers           []wgtypes.PeerConfig  `json:"peers" yaml:"peers"`
-	ServerConfig    ServerConfig          `json:"server_config" yaml:"server_config"`
-	PeerIDs         PeerMap               `json:"peer_ids,omitempty" yaml:"peer_ids,omitempty"`
-	HostNetworkInfo HostInfoMap           `json:"host_network_info,omitempty"  yaml:"host_network_info,omitempty"`
-	EgressRoutes    []EgressNetworkRoutes `json:"egress_network_routes"`
-	FwUpdate        FwUpdate              `json:"fw_update"`
-	ChangeDefaultGw bool                  `json:"change_default_gw"`
-	DefaultGwIp     net.IP                `json:"default_gw_ip"`
-	IsInternetGw    bool                  `json:"is_inet_gw"`
+	Host              Host                  `json:"host" yaml:"host"`
+	Nodes             []Node                `json:"nodes" yaml:"nodes"`
+	Peers             []wgtypes.PeerConfig  `json:"peers" yaml:"peers"`
+	ServerConfig      ServerConfig          `json:"server_config" yaml:"server_config"`
+	PeerIDs           PeerMap               `json:"peer_ids,omitempty" yaml:"peer_ids,omitempty"`
+	HostNetworkInfo   HostInfoMap           `json:"host_network_info,omitempty"  yaml:"host_network_info,omitempty"`
+	EgressRoutes      []EgressNetworkRoutes `json:"egress_network_routes"`
+	FwUpdate          FwUpdate              `json:"fw_update"`
+	ChangeDefaultGw   bool                  `json:"change_default_gw"`
+	DefaultGwIp       net.IP                `json:"default_gw_ip"`
+	IsInternetGw      bool                  `json:"is_inet_gw"`
+	EndpointDetection bool                  `json:"endpoint_detection"`
 }
 
 type DefaultGwInfo struct {

+ 1 - 1
pro/trial.go

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

+ 4 - 0
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>"
@@ -71,3 +73,5 @@ JWT_VALIDITY_DURATION=43200
 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

+ 2 - 2
scripts/nm-quick.sh

@@ -248,8 +248,8 @@ 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"
-		"FRONTEND_URL" "AZURE_TENANT" "OIDC_ISSUER" "EXPORTER_API_PORT" "JWT_VALIDITY_DURATION" "RAC_AUTO_DISABLE" "CACHING_ENABLED")
+		"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}"
 	done

+ 22 - 0
servercfg/serverconf.go

@@ -674,6 +674,17 @@ func DeployedByOperator() bool {
 	return config.Config.Server.DeployedByOperator
 }
 
+// IsEndpointDetectionEnabled - returns true if endpoint detection enabled
+func IsEndpointDetectionEnabled() bool {
+	var enabled = true //default
+	if os.Getenv("ENDPOINT_DETECTION") != "" {
+		enabled = os.Getenv("ENDPOINT_DETECTION") == "true"
+	} else {
+		enabled = config.Config.Server.EndpointDetection
+	}
+	return enabled
+}
+
 // GetEnvironment returns the environment the server is running in (e.g. dev, staging, prod...)
 func GetEnvironment() string {
 	if env := os.Getenv("ENVIRONMENT"); env != "" {
@@ -703,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: ""