Browse Source

NET-1064: Oauth User SignUp Approval Flow (#2874)

* add pending users api

* insert user to pending users on first time oauth login

* add pending user check on headless login

* fix conflicting apis

* no records error

* add allowed emails domains for oauth singup to config

* check if user is allowed to signup
Abhishek K 1 year ago
parent
commit
0d4552db5e

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

+ 24 - 3
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,9 +61,29 @@ 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
 		}
 		}
 	}
 	}
@@ -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))
+}

+ 24 - 3
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,9 +61,29 @@ 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
 		}
 		}
 	}
 	}
@@ -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
 	}
 	}

+ 1 - 0
config/config.go

@@ -92,6 +92,7 @@ type ServerConfig struct {
 	JwtValidityDuration        time.Duration `yaml:"jwt_validity_duration"`
 	JwtValidityDuration        time.Duration `yaml:"jwt_validity_duration"`
 	RacAutoDisable             bool          `yaml:"rac_auto_disable"`
 	RacAutoDisable             bool          `yaml:"rac_auto_disable"`
 	CacheEnabled               string        `yaml:"caching_enabled"`
 	CacheEnabled               string        `yaml:"caching_enabled"`
+	AllowedEmailDomains        string        `yaml:"allowed_email_domains"`
 }
 }
 
 
 // SQLConfig - Generic SQL Config
 // SQLConfig - Generic SQL Config

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

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

+ 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>"
@@ -70,4 +72,4 @@ JWT_VALIDITY_DURATION=43200
 # Auto disable a user's connecteds clients bassed on JWT token expiration
 # Auto disable a user's connecteds clients bassed on JWT token expiration
 RAC_AUTO_DISABLE=true
 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

+ 1 - 1
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")
 		"FRONTEND_URL" "AZURE_TENANT" "OIDC_ISSUER" "EXPORTER_API_PORT" "JWT_VALIDITY_DURATION" "RAC_AUTO_DISABLE" "CACHING_ENABLED")
 	for name in "${toCopy[@]}"; do
 	for name in "${toCopy[@]}"; do
 		save_config_item $name "${!name}"
 		save_config_item $name "${!name}"

+ 11 - 0
servercfg/serverconf.go

@@ -703,3 +703,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
+}