Bläddra i källkod

NET-1227: User Mgmt V2 (#3055)

* user mgmt models

* define user roles

* define models for new user mgmt and groups

* oauth debug log

* initialize user role after db conn

* print oauth token in debug log

* user roles CRUD apis

* user groups CRUD Apis

* additional api checks

* add additional scopes

* add additional scopes url

* add additional scopes url

* rm additional scopes url

* setup middlleware permission checks

* integrate permission check into middleware

* integrate permission check into middleware

* check for headers for subjects

* refactor user role models

* refactor user groups models

* add new user to pending user via RAC login

* untracked

* allow multiple groups for an user

* change json tag

* add debug headers

* refer network controls form roles, add debug headers

* refer network controls form roles, add debug headers

* replace auth checks, add network id to role model

* nodes handler

* migration funcs

* invoke sync users migration func

* add debug logs

* comment middleware

* fix get all nodes api

* add debug logs

* fix middleware error nil check

* add new func to get username from jwt

* fix jwt parsing

* abort on error

* allow multiple network roles

* allow multiple network roles

* add migration func

* return err if jwt parsing fails

* set global check to true when accessing user apis

* set netid for acls api calls

* set netid for acls api calls

* update role and groups routes

* add validation checks

* add invite flow apis and magic links

* add invited user via oauth signup automatically

* create invited user on oauth signup, with groups in the invite

* add group validation for user invite

* update create user handler with new role mgmt

* add validation checks

* create user invites tables

* add error logging for email invite

* fix invite singup url

* debug log

* get query params from url

* get query params from url

* add query escape

* debug log

* debug log

* fix user signup via invite api

* set admin field for backward compatbility

* use new role id for user apis

* deprecate use of old admin fields

* deprecate usage of old user fields

* add user role as service user if empty

* setup email sender

* delete invite after user singup

* add plaform user role

* redirect on invite verification link

* fix invite redirect

* temporary redirect

* fix invite redirect

* point invite link to frontend

* fix query params lookup

* add resend support, configure email interface types

* fix groups and user creation

* validate user groups, add check for metrics api in middleware

* add invite url to invite model

* migrate rac apis to new user mgmt

* handle network nodes

* add platform user to default role

* fix user role migration

* add default on rag creation and cleanup after deletion

* fix rac apis

* change to invite code param

* filter nodes and hosts based on user network access

* extend create user group req to accomodate users

* filter network based on user access

* format oauth error

* move user roles and groups

* fix get user v1 api

* move user mgmt func to pro

* add user auth type to user model

* fix roles init

* remove platform role from group object

* list only platform roles

* add network roles to invite req

* create default groups and roles

* fix middleware for global access

* create default role

* fix nodes filter with global network roles

* block selfupdate of groups and network roles

* delete netID if net roles are empty

* validate user roles nd groups on update

* set extclient permission scope when rag vpn access is set

* allow deletion of roles and groups

* replace _ with - in role naming convention

* fix failover middleware mgmt

* format oauth templates

* fetch route temaplate

* return err if user wrong login type

* check user groups on rac apis

* fix rac apis

* fix resp msg

* add validation checks for admin invite

* return oauth type

* format group err msg

* fix html tag

* clean up default groups

* create default rag role

* add UI name to roles

* remove default net group from user when deleted

* reorder migration funcs

* fix duplicacy of hosts

* check old field for migration

* from pro to ce make all secondary users admins

* from pro to ce make all secondary users admins

* revert: from pro to ce make all secondary users admins

* make sure downgrades work

* fix pending users approval

* fix duplicate hosts

* fix duplicate hosts entries

* fix cache reference issue

* feat: configure FRONTEND_URL during installation

* disable user vpn access when network roles are modified

* rm vpn acces when roles or groups are deleted

* add http to frontend url

* revert crypto version

* downgrade crytpo version

* add platform id check on user invites

---------

Co-authored-by: the_aceix <[email protected]>
Abhishek K 1 år sedan
förälder
incheckning
2e8d95e80e

+ 3 - 87
auth/auth.go

@@ -1,15 +1,8 @@
 package auth
 
 import (
-	"encoding/base64"
-	"encoding/json"
-	"fmt"
-
-	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
-	"golang.org/x/crypto/bcrypt"
-	"golang.org/x/exp/slog"
 	"golang.org/x/oauth2"
 )
 
@@ -22,88 +15,11 @@ var (
 	auth_provider *oauth2.Config
 )
 
-// IsOauthUser - returns
-func IsOauthUser(user *models.User) error {
-	var currentValue, err = FetchPassValue("")
-	if err != nil {
-		return err
-	}
-	var bCryptErr = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(currentValue))
-	return bCryptErr
-}
-
-func FetchPassValue(newValue string) (string, error) {
-
-	type valueHolder struct {
-		Value string `json:"value" bson:"value"`
-	}
-	newValueHolder := valueHolder{}
-	var currentValue, err = logic.FetchAuthSecret()
-	if err != nil {
-		return "", err
-	}
-	var unmarshErr = json.Unmarshal([]byte(currentValue), &newValueHolder)
-	if unmarshErr != nil {
-		return "", unmarshErr
-	}
-
-	var b64CurrentValue, b64Err = base64.StdEncoding.DecodeString(newValueHolder.Value)
-	if b64Err != nil {
-		logger.Log(0, "could not decode pass")
-		return "", nil
-	}
-	return string(b64CurrentValue), nil
-}
-
-// == private ==
-
-func addUser(email string) error {
-	var hasSuperAdmin, err = logic.HasSuperAdmin()
-	if err != nil {
-		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("")
-	if fetchErr != nil {
-		slog.Error("failed to get password", "error", fetchErr.Error())
-		return fetchErr
-	}
-	var newUser = models.User{
-		UserName: email,
-		Password: newPass,
-	}
-	if !hasSuperAdmin { // must be first attempt, create a superadmin
-		logger.Log(0, "creating superadmin")
-		if err = logic.CreateSuperAdmin(&newUser); err != nil {
-			slog.Error("error creating super admin from user", "email", email, "error", err)
-		} else {
-			slog.Info("superadmin created from user", "email", email)
-		}
-	} else { // otherwise add to db as admin..?
-		// TODO: add ability to add users with preemptive permissions
-		newUser.IsAdmin = false
-		if err = logic.CreateUser(&newUser); err != nil {
-			logger.Log(0, "error creating user,", email, "; user not added", "error", err.Error())
-		} else {
-			logger.Log(0, "user created from ", email)
-		}
-	}
-	return nil
-}
-
-func isUserIsAllowed(username, network string, shouldAddUser bool) (*models.User, error) {
+func isUserIsAllowed(username, network string) (*models.User, error) {
 
 	user, err := logic.GetUser(username)
-	if err != nil && shouldAddUser { // user must not exist, so try to make one
-		if err = addUser(username); err != nil {
-			logger.Log(0, "failed to add user", username, "during a node SSO network join on network", network)
-			// response := returnErrTemplate(user.UserName, "failed to add user", state, reqKeyIf)
-			// w.WriteHeader(http.StatusInternalServerError)
-			// w.Write(response)
-			return nil, fmt.Errorf("failed to add user to system")
-		}
-		logger.Log(0, "user", username, "was added during a node SSO network join on network", network)
-		user, _ = logic.GetUser(username)
+	if err != nil { // user must not exist, so try to make one
+		return &models.User{}, err
 	}
 
 	return user, nil

+ 19 - 19
auth/host_session.go

@@ -85,24 +85,24 @@ func SessionHandler(conn *websocket.Conn) {
 			return
 		}
 		req.Pass = req.Host.ID.String()
-		user, err := logic.GetUser(req.User)
-		if err != nil {
-			logger.Log(0, "failed to get user", req.User, "from database")
-			err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
-			if err != nil {
-				logger.Log(0, "error during message writing:", err.Error())
-			}
-			return
-		}
-		if !user.IsAdmin && !user.IsSuperAdmin {
-			logger.Log(0, "user", req.User, "is neither an admin or superadmin. denying registeration")
-			conn.WriteMessage(messageType, []byte("cannot register with a non-admin or non-superadmin"))
-			err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
-			if err != nil {
-				logger.Log(0, "error during message writing:", err.Error())
-			}
-			return
-		}
+		// user, err := logic.GetUser(req.User)
+		// if err != nil {
+		// 	logger.Log(0, "failed to get user", req.User, "from database")
+		// 	err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
+		// 	if err != nil {
+		// 		logger.Log(0, "error during message writing:", err.Error())
+		// 	}
+		// 	return
+		// }
+		// if !user.IsAdmin && !user.IsSuperAdmin {
+		// 	logger.Log(0, "user", req.User, "is neither an admin or superadmin. denying registeration")
+		// 	conn.WriteMessage(messageType, []byte("cannot register with a non-admin or non-superadmin"))
+		// 	err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
+		// 	if err != nil {
+		// 		logger.Log(0, "error during message writing:", err.Error())
+		// 	}
+		// 	return
+		// }
 
 		if err = netcache.Set(stateStr, req); err != nil { // give the user's host access in the DB
 			logger.Log(0, "machine failed to complete join on network,", registerMessage.Network, "-", err.Error())
@@ -197,7 +197,7 @@ func SessionHandler(conn *websocket.Conn) {
 		for _, newNet := range currentNetworks {
 			if !logic.StringSliceContains(hostNets, newNet) {
 				if len(result.User) > 0 {
-					_, err := isUserIsAllowed(result.User, newNet, false)
+					_, err := isUserIsAllowed(result.User, newNet)
 					if err != nil {
 						logger.Log(0, "unauthorized user", result.User, "attempted to register to network", newNet)
 						handleHostRegErr(conn, err)

+ 5 - 0
config/config.go

@@ -94,6 +94,11 @@ type ServerConfig struct {
 	CacheEnabled               string        `yaml:"caching_enabled"`
 	EndpointDetection          bool          `json:"endpoint_detection"`
 	AllowedEmailDomains        string        `yaml:"allowed_email_domains"`
+	EmailSenderAddr            string        `json:"email_sender_addr"`
+	EmailSenderAuth            string        `json:"email_sender_auth"`
+	EmailSenderType            string        `json:"email_sender_type"`
+	SmtpHost                   string        `json:"smtp_host"`
+	SmtpPort                   int           `json:"smtp_port"`
 	MetricInterval             string        `yaml:"metric_interval"`
 }
 

+ 3 - 2
controllers/controller.go

@@ -17,7 +17,9 @@ import (
 )
 
 // HttpMiddlewares - middleware functions for REST interactions
-var HttpMiddlewares []mux.MiddlewareFunc
+var HttpMiddlewares = []mux.MiddlewareFunc{
+	userMiddleWare,
+}
 
 // HttpHandlers - handler functions for REST interactions
 var HttpHandlers = []interface{}{
@@ -39,7 +41,6 @@ func HandleRESTRequests(wg *sync.WaitGroup, ctx context.Context) {
 	defer wg.Done()
 
 	r := mux.NewRouter()
-
 	// Currently allowed dev origin is all. Should change in prod
 	// should consider analyzing the allowed methods further
 	headersOk := handlers.AllowedHeaders(

+ 0 - 51
controllers/ext_client.go

@@ -128,18 +128,6 @@ func getExtClient(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-	if !logic.IsUserAllowedAccessToExtClient(r.Header.Get("user"), client) {
-		// check if user has access to extclient
-		slog.Error("failed to get extclient", "network", network, "clientID",
-			clientid, "error", errors.New("access is denied"))
-		logic.ReturnErrorResponse(
-			w,
-			r,
-			logic.FormatError(errors.New("access is denied"), "forbidden"),
-		)
-		return
-
-	}
 
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(client)
@@ -170,16 +158,6 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-	if !logic.IsUserAllowedAccessToExtClient(r.Header.Get("user"), client) {
-		slog.Error("failed to get extclient", "network", networkid, "clientID",
-			clientid, "error", errors.New("access is denied"))
-		logic.ReturnErrorResponse(
-			w,
-			r,
-			logic.FormatError(errors.New("access is denied"), "forbidden"),
-		)
-		return
-	}
 
 	gwnode, err := logic.GetNodeByID(client.IngressGatewayID)
 	if err != nil {
@@ -445,12 +423,6 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 		userName = caller.UserName
-		if _, ok := caller.RemoteGwIDs[nodeid]; (!caller.IsAdmin && !caller.IsSuperAdmin) && !ok {
-			err = errors.New("permission denied")
-			slog.Error("failed to create extclient", "error", err)
-			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
-			return
-		}
 		// check if user has a config already for remote access client
 		extclients, err := logic.GetNetworkExtClients(node.Network)
 		if err != nil {
@@ -567,7 +539,6 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	clientid := params["clientid"]
-	network := params["network"]
 	oldExtClient, err := logic.GetExtClientByName(clientid)
 	if err != nil {
 		slog.Error(
@@ -582,18 +553,6 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-	if !logic.IsUserAllowedAccessToExtClient(r.Header.Get("user"), oldExtClient) {
-		// check if user has access to extclient
-		slog.Error("failed to get extclient", "network", network, "clientID",
-			clientid, "error", errors.New("access is denied"))
-		logic.ReturnErrorResponse(
-			w,
-			r,
-			logic.FormatError(errors.New("access is denied"), "forbidden"),
-		)
-		return
-
-	}
 	if oldExtClient.ClientID == update.ClientID {
 		if err := validateCustomExtClient(&update, false); err != nil {
 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
@@ -729,16 +688,6 @@ func deleteExtClient(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-	if !logic.IsUserAllowedAccessToExtClient(r.Header.Get("user"), extclient) {
-		slog.Error("user not allowed to delete", "network", network, "clientID",
-			clientid, "error", errors.New("access is denied"))
-		logic.ReturnErrorResponse(
-			w,
-			r,
-			logic.FormatError(errors.New("access is denied"), "forbidden"),
-		)
-		return
-	}
 	ingressnode, err := logic.GetNodeByID(extclient.IngressGatewayID)
 	if err != nil {
 		logger.Log(

+ 57 - 3
controllers/hosts.go

@@ -79,12 +79,53 @@ func upgradeHost(w http.ResponseWriter, r *http.Request) {
 // @Success     200 {array} models.ApiHost
 // @Failure     500 {object} models.ErrorResponse
 func getHosts(w http.ResponseWriter, r *http.Request) {
-	currentHosts, err := logic.GetAllHosts()
+	w.Header().Set("Content-Type", "application/json")
+	currentHosts := []models.Host{}
+	username := r.Header.Get("user")
+	user, err := logic.GetUser(username)
+	if err != nil {
+		return
+	}
+	userPlatformRole, err := logic.GetRole(user.PlatformRoleID)
 	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
 	}
+	respHostsMap := make(map[string]struct{})
+	if !userPlatformRole.FullAccess {
+		nodes, err := logic.GetAllNodes()
+		if err != nil {
+			logger.Log(0, "error fetching all nodes info: ", err.Error())
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+			return
+		}
+		filteredNodes := logic.GetFilteredNodesByUserAccess(*user, nodes)
+		if len(filteredNodes) > 0 {
+			currentHostsMap, err := logic.GetHostsMap()
+			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 _, node := range filteredNodes {
+				if _, ok := respHostsMap[node.HostID.String()]; ok {
+					continue
+				}
+				if host, ok := currentHostsMap[node.HostID.String()]; ok {
+					currentHosts = append(currentHosts, host)
+					respHostsMap[host.ID.String()] = struct{}{}
+				}
+			}
+
+		}
+	} else {
+		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
+		}
+	}
+
 	apiHosts := logic.GetAllHostsAPI(currentHosts[:])
 	logger.Log(2, r.Header.Get("user"), "fetched all hosts")
 	logic.SortApiHosts(apiHosts[:])
@@ -194,6 +235,19 @@ func updateHost(w http.ResponseWriter, r *http.Request) {
 
 	newHost := newHostData.ConvertAPIHostToNMHost(currHost)
 
+	if newHost.Name != currHost.Name {
+		// update any rag role ids
+		for _, nodeID := range newHost.Nodes {
+			node, err := logic.GetNodeByID(nodeID)
+			if err == nil && node.IsIngressGateway {
+				role, err := logic.GetRole(models.GetRAGRoleID(node.Network, currHost.ID.String()))
+				if err == nil {
+					role.UiName = models.GetRAGRoleName(node.Network, newHost.Name)
+					logic.UpdateRole(role)
+				}
+			}
+		}
+	}
 	logic.UpdateHost(newHost, currHost) // update the in memory struct values
 	if err = logic.UpsertHost(newHost); err != nil {
 		logger.Log(0, r.Header.Get("user"), "failed to update a host:", err.Error())

+ 105 - 0
controllers/middleware.go

@@ -0,0 +1,105 @@
+package controller
+
+import (
+	"net/http"
+	"net/url"
+	"strings"
+
+	"github.com/gorilla/mux"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/models"
+)
+
+func userMiddleWare(handler http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		var params = mux.Vars(r)
+		route, err := mux.CurrentRoute(r).GetPathTemplate()
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+			return
+		}
+		r.Header.Set("IS_GLOBAL_ACCESS", "no")
+		r.Header.Set("TARGET_RSRC", "")
+		r.Header.Set("RSRC_TYPE", "")
+		r.Header.Set("TARGET_RSRC_ID", "")
+		r.Header.Set("NET_ID", params["network"])
+		if strings.Contains(route, "hosts") || strings.Contains(route, "nodes") {
+			r.Header.Set("TARGET_RSRC", models.HostRsrc.String())
+		}
+		if strings.Contains(route, "dns") {
+			r.Header.Set("TARGET_RSRC", models.DnsRsrc.String())
+		}
+		if strings.Contains(route, "users") {
+			r.Header.Set("TARGET_RSRC", models.UserRsrc.String())
+		}
+		if strings.Contains(route, "ingress") {
+			r.Header.Set("TARGET_RSRC", models.RemoteAccessGwRsrc.String())
+		}
+		if strings.Contains(route, "createrelay") || strings.Contains(route, "deleterelay") {
+			r.Header.Set("TARGET_RSRC", models.RelayRsrc.String())
+		}
+
+		if strings.Contains(route, "gateway") {
+			r.Header.Set("TARGET_RSRC", models.EgressGwRsrc.String())
+		}
+		if strings.Contains(route, "networks") {
+			r.Header.Set("TARGET_RSRC", models.NetworkRsrc.String())
+		}
+		if strings.Contains(route, "acls") {
+			r.Header.Set("TARGET_RSRC", models.AclRsrc.String())
+		}
+		if strings.Contains(route, "extclients") {
+			r.Header.Set("TARGET_RSRC", models.ExtClientsRsrc.String())
+		}
+		if strings.Contains(route, "enrollment-keys") {
+			r.Header.Set("TARGET_RSRC", models.EnrollmentKeysRsrc.String())
+		}
+		if strings.Contains(route, "metrics") {
+			r.Header.Set("TARGET_RSRC", models.MetricRsrc.String())
+		}
+		if keyID, ok := params["keyID"]; ok {
+			r.Header.Set("TARGET_RSRC_ID", keyID)
+		}
+		if nodeID, ok := params["nodeid"]; ok && r.Header.Get("TARGET_RSRC") != models.ExtClientsRsrc.String() {
+			r.Header.Set("TARGET_RSRC_ID", nodeID)
+		}
+		if strings.Contains(route, "failover") {
+			r.Header.Set("TARGET_RSRC", models.FailOverRsrc.String())
+			nodeID := r.Header.Get("TARGET_RSRC_ID")
+			node, _ := logic.GetNodeByID(nodeID)
+			r.Header.Set("NET_ID", node.Network)
+
+		}
+		if hostID, ok := params["hostid"]; ok {
+			r.Header.Set("TARGET_RSRC_ID", hostID)
+		}
+		if clientID, ok := params["clientid"]; ok {
+			r.Header.Set("TARGET_RSRC_ID", clientID)
+		}
+		if netID, ok := params["networkname"]; ok {
+			if !strings.Contains(route, "acls") {
+				r.Header.Set("TARGET_RSRC_ID", netID)
+			}
+			r.Header.Set("NET_ID", params["networkname"])
+		}
+
+		if userID, ok := params["username"]; ok {
+			r.Header.Set("TARGET_RSRC_ID", userID)
+		} else {
+			username, _ := url.QueryUnescape(r.URL.Query().Get("username"))
+			if username != "" {
+				r.Header.Set("TARGET_RSRC_ID", username)
+			}
+		}
+		if r.Header.Get("NET_ID") == "" && (r.Header.Get("TARGET_RSRC_ID") == "" ||
+			r.Header.Get("TARGET_RSRC") == models.EnrollmentKeysRsrc.String() ||
+			r.Header.Get("TARGET_RSRC") == models.UserRsrc.String()) {
+			r.Header.Set("IS_GLOBAL_ACCESS", "yes")
+		}
+
+		r.Header.Set("RSRC_TYPE", r.Header.Get("TARGET_RSRC"))
+		logger.Log(0, "URL ------> ", route)
+		handler.ServeHTTP(w, r)
+	})
+}

+ 9 - 1
controllers/network.go

@@ -58,7 +58,13 @@ func getNetworks(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-
+	username := r.Header.Get("user")
+	user, err := logic.GetUser(username)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	allnetworks = logic.FilterNetworksByRole(allnetworks, *user)
 	logger.Log(2, r.Header.Get("user"), "fetched networks.")
 	logic.SortNetworks(allnetworks[:])
 	w.WriteHeader(http.StatusOK)
@@ -402,6 +408,7 @@ func deleteNetwork(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, errtype))
 		return
 	}
+	go logic.DeleteNetworkRoles(network)
 	//delete network from allocated ip map
 	go logic.RemoveNetworkFromAllocatedIpMap(network)
 
@@ -476,6 +483,7 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
+	logic.CreateDefaultNetworkRolesAndGroups(models.NetworkID(network.NetID))
 
 	//add new network to allocated ip map
 	go logic.AddNetworkToAllocatedIpMap(network.NetID)

+ 3 - 3
controllers/network_test.go

@@ -26,9 +26,9 @@ func TestMain(m *testing.M) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
 	logic.CreateSuperAdmin(&models.User{
-		UserName: "admin",
-		Password: "password",
-		IsAdmin:  true,
+		UserName:       "admin",
+		Password:       "password",
+		PlatformRoleID: models.SuperAdminRole,
 	})
 	peerUpdate := make(chan *models.Node)
 	go logic.ManageZombies(context.Background(), peerUpdate)

+ 80 - 49
controllers/node.go

@@ -21,24 +21,15 @@ var hostIDHeader = "host-id"
 
 func nodeHandlers(r *mux.Router) {
 
-	r.HandleFunc("/api/nodes", Authorize(false, false, "user", http.HandlerFunc(getAllNodes))).
-		Methods(http.MethodGet)
-	r.HandleFunc("/api/nodes/{network}", Authorize(false, true, "network", http.HandlerFunc(getNetworkNodes))).
-		Methods(http.MethodGet)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}", Authorize(true, true, "node", http.HandlerFunc(getNode))).
-		Methods(http.MethodGet)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}", logic.SecurityCheck(true, http.HandlerFunc(updateNode))).
-		Methods(http.MethodPut)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}", Authorize(true, true, "node", http.HandlerFunc(deleteNode))).
-		Methods(http.MethodDelete)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}/creategateway", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceEgress, http.HandlerFunc(createEgressGateway)))).
-		Methods(http.MethodPost)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}/deletegateway", logic.SecurityCheck(true, http.HandlerFunc(deleteEgressGateway))).
-		Methods(http.MethodDelete)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}/createingress", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceIngress, http.HandlerFunc(createIngressGateway)))).
-		Methods(http.MethodPost)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}/deleteingress", logic.SecurityCheck(true, http.HandlerFunc(deleteIngressGateway))).
-		Methods(http.MethodDelete)
+	r.HandleFunc("/api/nodes", logic.SecurityCheck(true, http.HandlerFunc(getAllNodes))).Methods(http.MethodGet)
+	r.HandleFunc("/api/nodes/{network}", logic.SecurityCheck(true, http.HandlerFunc(getNetworkNodes))).Methods(http.MethodGet)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}", Authorize(true, true, "node", http.HandlerFunc(getNode))).Methods(http.MethodGet)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}", logic.SecurityCheck(true, http.HandlerFunc(updateNode))).Methods(http.MethodPut)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}", Authorize(true, true, "node", http.HandlerFunc(deleteNode))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/creategateway", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceEgress, http.HandlerFunc(createEgressGateway)))).Methods(http.MethodPost)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/deletegateway", logic.SecurityCheck(true, http.HandlerFunc(deleteEgressGateway))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/createingress", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceIngress, http.HandlerFunc(createIngressGateway)))).Methods(http.MethodPost)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/deleteingress", logic.SecurityCheck(true, http.HandlerFunc(deleteIngressGateway))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/nodes/adm/{network}/authenticate", authenticate).Methods(http.MethodPost)
 	r.HandleFunc("/api/v1/nodes/migrate", migrate).Methods(http.MethodPost)
 }
@@ -277,6 +268,61 @@ func getNetworkNodes(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+	username := r.Header.Get("user")
+	user, err := logic.GetUser(username)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	userPlatformRole, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	filteredNodes := []models.Node{}
+	if !userPlatformRole.FullAccess {
+		nodesMap := make(map[string]struct{})
+		networkRoles := user.NetworkRoles[models.NetworkID(networkName)]
+		for networkRoleID := range networkRoles {
+			userPermTemplate, err := logic.GetRole(networkRoleID)
+			if err != nil {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+				return
+			}
+			if userPermTemplate.FullAccess {
+				break
+			}
+			if rsrcPerms, ok := userPermTemplate.NetworkLevelAccess[models.RemoteAccessGwRsrc]; ok {
+				if _, ok := rsrcPerms[models.AllRemoteAccessGwRsrcID]; ok {
+					for _, node := range nodes {
+						if _, ok := nodesMap[node.ID.String()]; ok {
+							continue
+						}
+						if node.IsIngressGateway {
+							nodesMap[node.ID.String()] = struct{}{}
+							filteredNodes = append(filteredNodes, node)
+						}
+					}
+				} else {
+					for gwID, scope := range rsrcPerms {
+						if _, ok := nodesMap[gwID.String()]; ok {
+							continue
+						}
+						if scope.Read {
+							gwNode, err := logic.GetNodeByID(gwID.String())
+							if err == nil && gwNode.IsIngressGateway {
+								filteredNodes = append(filteredNodes, gwNode)
+							}
+						}
+					}
+				}
+			}
+
+		}
+	}
+	if len(filteredNodes) > 0 {
+		nodes = filteredNodes
+	}
 
 	// returns all the nodes in JSON/API format
 	apiNodes := logic.GetAllNodesAPI(nodes[:])
@@ -294,22 +340,26 @@ func getNetworkNodes(w http.ResponseWriter, r *http.Request) {
 // Not quite sure if this is necessary. Probably necessary based on front end but may want to review after iteration 1 if it's being used or not
 func getAllNodes(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
-	user, err := logic.GetUser(r.Header.Get("user"))
-	if err != nil && r.Header.Get("ismasterkey") != "yes" {
-		logger.Log(0, r.Header.Get("user"),
-			"error fetching user info: ", err.Error())
+	var nodes []models.Node
+	nodes, err := logic.GetAllNodes()
+	if err != nil {
+		logger.Log(0, "error fetching all nodes info: ", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-	var nodes []models.Node
-	if user.IsAdmin || r.Header.Get("ismasterkey") == "yes" {
-		nodes, err = logic.GetAllNodes()
-		if err != nil {
-			logger.Log(0, "error fetching all nodes info: ", err.Error())
-			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-			return
-		}
+	username := r.Header.Get("user")
+	user, err := logic.GetUser(username)
+	if err != nil {
+		return
 	}
+	userPlatformRole, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		return
+	}
+	if !userPlatformRole.FullAccess {
+		nodes = logic.GetFilteredNodesByUserAccess(*user, nodes)
+	}
+
 	// return all the nodes in JSON/API format
 	apiNodes := logic.GetAllNodesAPI(nodes[:])
 	logger.Log(3, r.Header.Get("user"), "fetched all nodes they have access to")
@@ -561,25 +611,6 @@ func deleteIngressGateway(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if servercfg.IsPro {
-		go func() {
-			users, err := logic.GetUsersDB()
-			if err == nil {
-				for _, user := range users {
-					if _, ok := user.RemoteGwIDs[nodeid]; ok {
-						delete(user.RemoteGwIDs, nodeid)
-						err = logic.UpsertUser(user)
-						if err != nil {
-							slog.Error("failed to get user", "user", user.UserName, "error", err)
-						}
-					}
-				}
-			} else {
-				slog.Error("failed to get users", "error", err)
-			}
-		}()
-	}
-
 	apiNode := node.ConvertToAPINode()
 	logger.Log(1, r.Header.Get("user"), "deleted ingress gateway", nodeid)
 	w.WriteHeader(http.StatusOK)

+ 2 - 2
controllers/server.go

@@ -38,10 +38,10 @@ func serverHandlers(r *mux.Router) {
 	).Methods(http.MethodPost)
 	r.HandleFunc("/api/server/getconfig", allowUsers(http.HandlerFunc(getConfig))).
 		Methods(http.MethodGet)
-	r.HandleFunc("/api/server/getserverinfo", Authorize(true, false, "node", http.HandlerFunc(getServerInfo))).
+	r.HandleFunc("/api/server/getserverinfo", logic.SecurityCheck(true, http.HandlerFunc(getServerInfo))).
 		Methods(http.MethodGet)
 	r.HandleFunc("/api/server/status", getStatus).Methods(http.MethodGet)
-	r.HandleFunc("/api/server/usage", Authorize(true, false, "user", http.HandlerFunc(getUsage))).
+	r.HandleFunc("/api/server/usage", logic.SecurityCheck(false, http.HandlerFunc(getUsage))).
 		Methods(http.MethodGet)
 }
 

+ 157 - 280
controllers/user.go

@@ -5,11 +5,12 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"net/url"
+	"reflect"
 
 	"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"
@@ -28,24 +29,12 @@ func userHandlers(r *mux.Router) {
 	r.HandleFunc("/api/users/adm/transfersuperadmin/{username}", logic.SecurityCheck(true, http.HandlerFunc(transferSuperAdmin))).
 		Methods(http.MethodPost)
 	r.HandleFunc("/api/users/adm/authenticate", authenticateUser).Methods(http.MethodPost)
-	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, http.HandlerFunc(updateUser))).
-		Methods(http.MethodPut)
-	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceUsers, http.HandlerFunc(createUser)))).
-		Methods(http.MethodPost)
-	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, http.HandlerFunc(deleteUser))).
-		Methods(http.MethodDelete)
-	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUser)))).
-		Methods(http.MethodGet)
-	r.HandleFunc("/api/users", logic.SecurityCheck(true, http.HandlerFunc(getUsers))).
-		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)
+	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, http.HandlerFunc(updateUser))).Methods(http.MethodPut)
+	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceUsers, http.HandlerFunc(createUser)))).Methods(http.MethodPost)
+	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, http.HandlerFunc(deleteUser))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUser)))).Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/users", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUserV1)))).Methods(http.MethodGet)
+	r.HandleFunc("/api/users", logic.SecurityCheck(true, http.HandlerFunc(getUsers))).Methods(http.MethodGet)
 
 }
 
@@ -94,15 +83,25 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 			logic.ReturnErrorResponse(response, request, logic.FormatError(err, "unauthorized"))
 			return
 		}
-		if !(user.IsAdmin || user.IsSuperAdmin) {
-			logic.ReturnErrorResponse(
-				response,
-				request,
-				logic.FormatError(errors.New("only admins can access dashboard"), "unauthorized"),
-			)
+		role, err := logic.GetRole(user.PlatformRoleID)
+		if err != nil {
+			logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("access denied to dashboard"), "unauthorized"))
+			return
+		}
+		if role.DenyDashboardAccess {
+			logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("access denied to dashboard"), "unauthorized"))
 			return
 		}
 	}
+	user, err := logic.GetUser(authRequest.UserName)
+	if err != nil {
+		logic.ReturnErrorResponse(response, request, logic.FormatError(err, "unauthorized"))
+		return
+	}
+	if logic.IsOauthUser(user) == nil {
+		logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("user is registered via SSO"), "badrequest"))
+		return
+	}
 	username := authRequest.UserName
 	jwt, err := logic.VerifyAuthRequest(authRequest)
 	if err != nil {
@@ -224,11 +223,55 @@ func getUser(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(user)
 }
 
-// @Summary     Get all users
-// @Router      /api/users [get]
-// @Tags        Users
-// @Success     200 {array} models.User
-// @Failure     500 {object} models.ErrorResponse
+// swagger:route GET /api/v1/users user getUserV1
+//
+// Get an individual user with role info.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: ReturnUserWithRolesAndGroups
+func getUserV1(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+	usernameFetched, _ := url.QueryUnescape(r.URL.Query().Get("username"))
+	if usernameFetched == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("username is required"), "badrequest"))
+		return
+	}
+	user, err := logic.GetReturnUser(usernameFetched)
+	if err != nil {
+		logger.Log(0, usernameFetched, "failed to fetch user: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	userRoleTemplate, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	resp := models.ReturnUserWithRolesAndGroups{
+		ReturnUser:   user,
+		PlatformRole: userRoleTemplate,
+	}
+	logger.Log(2, r.Header.Get("user"), "fetched user", usernameFetched)
+	logic.ReturnSuccessResponseWithJson(w, r, resp, "fetched user with role info")
+}
+
+// swagger:route GET /api/users user getUsers
+//
+// Get all users.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
 func getUsers(w http.ResponseWriter, r *http.Request) {
 	// set header.
 	w.Header().Set("Content-Type", "application/json")
@@ -297,15 +340,8 @@ func transferSuperAdmin(w http.ResponseWriter, r *http.Request) {
 	if err != nil {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 	}
-	if !caller.IsSuperAdmin {
-		logic.ReturnErrorResponse(
-			w,
-			r,
-			logic.FormatError(
-				errors.New("only superadmin can assign the superadmin role to another user"),
-				"forbidden",
-			),
-		)
+	if caller.PlatformRoleID != models.SuperAdminRole {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only superadmin can assign the superadmin role to another user"), "forbidden"))
 		return
 	}
 	var params = mux.Vars(r)
@@ -316,15 +352,8 @@ func transferSuperAdmin(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
-	if !u.IsAdmin {
-		logic.ReturnErrorResponse(
-			w,
-			r,
-			logic.FormatError(
-				errors.New("only admins can be promoted to superadmin role"),
-				"forbidden",
-			),
-		)
+	if u.PlatformRoleID != models.AdminRole {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only admins can be promoted to superadmin role"), "forbidden"))
 		return
 	}
 	if !servercfg.IsBasicAuthEnabled() {
@@ -336,16 +365,14 @@ func transferSuperAdmin(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	u.IsSuperAdmin = true
-	u.IsAdmin = false
+	u.PlatformRoleID = models.SuperAdminRole
 	err = logic.UpsertUser(*u)
 	if err != nil {
 		slog.Error("error updating user to superadmin: ", "user", u.UserName, "error", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-	caller.IsSuperAdmin = false
-	caller.IsAdmin = true
+	caller.PlatformRoleID = models.AdminRole
 	err = logic.UpsertUser(*caller)
 	if err != nil {
 		slog.Error("error demoting user to admin: ", "user", caller.UserName, "error", err.Error())
@@ -369,7 +396,7 @@ func createUser(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	caller, err := logic.GetUser(r.Header.Get("user"))
 	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
 	var user models.User
@@ -380,27 +407,34 @@ func createUser(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
-	if !caller.IsSuperAdmin && user.IsAdmin {
-		err = errors.New("only superadmin can create admin users")
+
+	if user.PlatformRoleID == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("platform role is missing"), "badrequest"))
+		return
+	}
+	userRole, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		err = errors.New("error fetching role " + user.PlatformRoleID.String() + " " + err.Error())
 		slog.Error("error creating new user: ", "user", user.UserName, "error", err)
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
-	if user.IsSuperAdmin {
+	if userRole.ID == models.SuperAdminRole {
 		err = errors.New("additional superadmins cannot be created")
 		slog.Error("error creating new user: ", "user", user.UserName, "error", err)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
 		return
 	}
-	if !servercfg.IsPro && !user.IsAdmin {
-		logic.ReturnErrorResponse(
-			w,
-			r,
-			logic.FormatError(
-				errors.New("non-admins users can only be created on Pro version"),
-				"forbidden",
-			),
-		)
+
+	if caller.PlatformRoleID != models.SuperAdminRole && user.PlatformRoleID == models.AdminRole {
+		err = errors.New("only superadmin can create admin users")
+		slog.Error("error creating new user: ", "user", user.UserName, "error", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
+		return
+	}
+
+	if !servercfg.IsPro && user.PlatformRoleID != models.AdminRole {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("non-admins users can only be created on Pro version"), "forbidden"))
 		return
 	}
 
@@ -410,6 +444,8 @@ func createUser(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
+	logic.DeleteUserInvite(user.UserName)
+	logic.DeletePendingUser(user.UserName)
 	slog.Info("user was created", "username", user.UserName)
 	json.NewEncoder(w).Encode(logic.ToReturnUser(user))
 }
@@ -472,55 +508,22 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if !ismaster && !selfUpdate {
-		if caller.IsAdmin && user.IsSuperAdmin {
-			slog.Error(
-				"non-superadmin user",
-				"caller",
-				caller.UserName,
-				"attempted to update superadmin user",
-				username,
-			)
-			logic.ReturnErrorResponse(
-				w,
-				r,
-				logic.FormatError(errors.New("cannot update superadmin user"), "forbidden"),
-			)
+		if caller.PlatformRoleID == models.AdminRole && user.PlatformRoleID == models.SuperAdminRole {
+			slog.Error("non-superadmin user", "caller", caller.UserName, "attempted to update superadmin user", username)
+			logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot update superadmin user"), "forbidden"))
 			return
 		}
-		if !caller.IsAdmin && !caller.IsSuperAdmin {
-			slog.Error(
-				"operation not allowed",
-				"caller",
-				caller.UserName,
-				"attempted to update user",
-				username,
-			)
-			logic.ReturnErrorResponse(
-				w,
-				r,
-				logic.FormatError(errors.New("cannot update superadmin user"), "forbidden"),
-			)
+		if caller.PlatformRoleID != models.AdminRole && caller.PlatformRoleID != models.SuperAdminRole {
+			slog.Error("operation not allowed", "caller", caller.UserName, "attempted to update user", username)
+			logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot update superadmin user"), "forbidden"))
 			return
 		}
-		if caller.IsAdmin && user.IsAdmin {
-			slog.Error(
-				"admin user cannot update another admin",
-				"caller",
-				caller.UserName,
-				"attempted to update admin user",
-				username,
-			)
-			logic.ReturnErrorResponse(
-				w,
-				r,
-				logic.FormatError(
-					errors.New("admin user cannot update another admin"),
-					"forbidden",
-				),
-			)
+		if caller.PlatformRoleID == models.AdminRole && user.PlatformRoleID == models.AdminRole {
+			slog.Error("admin user cannot update another admin", "caller", caller.UserName, "attempted to update admin user", username)
+			logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("admin user cannot update another admin"), "forbidden"))
 			return
 		}
-		if caller.IsAdmin && userchange.IsAdmin {
+		if caller.PlatformRoleID == models.AdminRole && userchange.PlatformRoleID == models.AdminRole {
 			err = errors.New("admin user cannot update role of an another user to admin")
 			slog.Error(
 				"failed to update user",
@@ -537,45 +540,39 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
 
 	}
 	if !ismaster && selfUpdate {
-		if user.IsAdmin != userchange.IsAdmin || user.IsSuperAdmin != userchange.IsSuperAdmin {
-			slog.Error(
-				"user cannot change his own role",
-				"caller",
-				caller.UserName,
-				"attempted to update user role",
-				username,
-			)
-			logic.ReturnErrorResponse(
-				w,
-				r,
-				logic.FormatError(errors.New("user not allowed to self assign role"), "forbidden"),
-			)
+		if user.PlatformRoleID != userchange.PlatformRoleID {
+			slog.Error("user cannot change his own role", "caller", caller.UserName, "attempted to update user role", username)
+			logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("user not allowed to self assign role"), "forbidden"))
 			return
 
 		}
+		if servercfg.IsPro {
+			// user cannot update his own roles and groups
+			if len(user.NetworkRoles) != len(userchange.NetworkRoles) || !reflect.DeepEqual(user.NetworkRoles, userchange.NetworkRoles) {
+				err = errors.New("user cannot update self update their network roles")
+				slog.Error("failed to update user", "caller", caller.UserName, "attempted to update user", username, "error", err)
+				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
+				return
+			}
+			// user cannot update his own roles and groups
+			if len(user.UserGroups) != len(userchange.UserGroups) || !reflect.DeepEqual(user.UserGroups, userchange.UserGroups) {
+				err = errors.New("user cannot update self update their groups")
+				slog.Error("failed to update user", "caller", caller.UserName, "attempted to update user", username, "error", err)
+				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
+				return
+			}
+		}
+
 	}
 	if ismaster {
-		if !user.IsSuperAdmin && userchange.IsSuperAdmin {
-			slog.Error(
-				"operation not allowed",
-				"caller",
-				logic.MasterUser,
-				"attempted to update user role to superadmin",
-				username,
-			)
-			logic.ReturnErrorResponse(
-				w,
-				r,
-				logic.FormatError(
-					errors.New("attempted to update user role to superadmin"),
-					"forbidden",
-				),
-			)
+		if user.PlatformRoleID != models.SuperAdminRole && userchange.PlatformRoleID == models.SuperAdminRole {
+			slog.Error("operation not allowed", "caller", logic.MasterUser, "attempted to update user role to superadmin", username)
+			logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("attempted to update user role to superadmin"), "forbidden"))
 			return
 		}
 	}
 
-	if auth.IsOauthUser(user) == nil && userchange.Password != "" {
+	if logic.IsOauthUser(user) == nil && userchange.Password != "" {
 		err := fmt.Errorf("cannot update user's password for an oauth user %s", username)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
 		return
@@ -608,6 +605,12 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
 	if err != nil {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 	}
+	callerUserRole, err := logic.GetRole(caller.PlatformRoleID)
+	if err != nil {
+		slog.Error("failed to get role ", "role", callerUserRole.ID, "error", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
 	username := params["username"]
 	user, err := logic.GetUser(username)
 	if err != nil {
@@ -616,7 +619,13 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-	if user.IsSuperAdmin {
+	userRole, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		slog.Error("failed to get role ", "role", userRole.ID, "error", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	if userRole.ID == models.SuperAdminRole {
 		slog.Error(
 			"failed to delete user: ", "user", username, "error", "superadmin cannot be deleted")
 		logic.ReturnErrorResponse(
@@ -626,8 +635,8 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
 		)
 		return
 	}
-	if !caller.IsSuperAdmin {
-		if caller.IsAdmin && user.IsAdmin {
+	if callerUserRole.ID != models.SuperAdminRole {
+		if callerUserRole.ID == models.AdminRole && userRole.ID == models.AdminRole {
 			slog.Error(
 				"failed to delete user: ",
 				"user",
@@ -667,10 +676,14 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
 		}
 		for _, extclient := range extclients {
 			if extclient.OwnerID == user.UserName {
-				err = logic.DeleteExtClient(extclient.Network, extclient.ClientID)
+				err = logic.DeleteExtClientAndCleanup(extclient)
 				if err != nil {
 					slog.Error("failed to delete extclient",
-						"id", extclient.ClientID, "owner", user.UserName, "error", err)
+						"id", extclient.ClientID, "owner", username, "error", err)
+				} else {
+					if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil {
+						slog.Error("error setting ext peers: " + err.Error())
+					}
 				}
 			}
 		}
@@ -697,139 +710,3 @@ func socketHandler(w http.ResponseWriter, r *http.Request) {
 	// Start handling the session
 	go auth.SessionHandler(conn)
 }
-
-// @Summary     Get all pending users
-// @Router      /api/users_pending [get]
-// @Tags        Users
-// @Success     200 {array} models.User
-// @Failure     500 {object} models.ErrorResponse
-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)
-}
-
-// @Summary     Approve a pending user
-// @Router      /api/users_pending/user/{username} [post]
-// @Tags        Users
-// @Param       username path string true "Username of the pending user to approve"
-// @Success     200 {string} string
-// @Failure     500 {object} models.ErrorResponse
-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)
-}
-
-// @Summary     Delete a pending user
-// @Router      /api/users_pending/user/{username} [delete]
-// @Tags        Users
-// @Param       username path string true "Username of the pending user to delete"
-// @Success     200 {string} string
-// @Failure     500 {object} models.ErrorResponse
-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)
-}
-
-// @Summary     Delete all pending users
-// @Router      /api/users_pending [delete]
-// @Tags        Users
-// @Success     200 {string} string
-// @Failure     500 {object} models.ErrorResponse
-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")
-}

+ 13 - 13
controllers/user_test.go

@@ -66,7 +66,7 @@ func prepareUserRequest(t *testing.T, userForBody models.User, userNameForParam
 func haveOnlyOneUser(t *testing.T, user models.User) {
 	deleteAllUsers(t)
 	var err error
-	if user.IsSuperAdmin {
+	if user.PlatformRoleID == models.SuperAdminRole {
 		err = logic.CreateSuperAdmin(&user)
 	} else {
 		err = logic.CreateUser(&user)
@@ -104,7 +104,7 @@ func TestHasSuperAdmin(t *testing.T) {
 		assert.False(t, found)
 	})
 	t.Run("superadmin user", func(t *testing.T) {
-		var user = models.User{UserName: "superadmin", Password: "password", IsSuperAdmin: true}
+		var user = models.User{UserName: "superadmin", Password: "password", PlatformRoleID: models.SuperAdminRole}
 		err := logic.CreateUser(&user)
 		assert.Nil(t, err)
 		found, err := logic.HasSuperAdmin()
@@ -112,7 +112,7 @@ func TestHasSuperAdmin(t *testing.T) {
 		assert.True(t, found)
 	})
 	t.Run("multiple superadmins", func(t *testing.T) {
-		var user = models.User{UserName: "superadmin1", Password: "password", IsSuperAdmin: true}
+		var user = models.User{UserName: "superadmin1", Password: "password", PlatformRoleID: models.SuperAdminRole}
 		err := logic.CreateUser(&user)
 		assert.Nil(t, err)
 		found, err := logic.HasSuperAdmin()
@@ -123,7 +123,7 @@ func TestHasSuperAdmin(t *testing.T) {
 
 func TestCreateUser(t *testing.T) {
 	deleteAllUsers(t)
-	user := models.User{UserName: "admin", Password: "password", IsAdmin: true}
+	user := models.User{UserName: "admin", Password: "password", PlatformRoleID: models.AdminRole}
 	t.Run("NoUser", func(t *testing.T) {
 		err := logic.CreateUser(&user)
 		assert.Nil(t, err)
@@ -161,7 +161,7 @@ func TestDeleteUser(t *testing.T) {
 		assert.False(t, deleted)
 	})
 	t.Run("Existing User", func(t *testing.T) {
-		user := models.User{UserName: "admin", Password: "password", IsAdmin: true}
+		user := models.User{UserName: "admin", Password: "password", PlatformRoleID: models.AdminRole}
 		if err := logic.CreateUser(&user); err != nil {
 			t.Fatal(err)
 		}
@@ -221,7 +221,7 @@ func TestValidateUser(t *testing.T) {
 func TestGetUser(t *testing.T) {
 	deleteAllUsers(t)
 
-	user := models.User{UserName: "admin", Password: "password", IsAdmin: true}
+	user := models.User{UserName: "admin", Password: "password", PlatformRoleID: models.AdminRole}
 
 	t.Run("NonExistantUser", func(t *testing.T) {
 		admin, err := logic.GetUser("admin")
@@ -241,8 +241,8 @@ func TestGetUser(t *testing.T) {
 func TestGetUsers(t *testing.T) {
 	deleteAllUsers(t)
 
-	adminUser := models.User{UserName: "admin", Password: "password", IsAdmin: true}
-	user := models.User{UserName: "admin", Password: "password", IsAdmin: false}
+	adminUser := models.User{UserName: "admin", Password: "password", PlatformRoleID: models.AdminRole}
+	user := models.User{UserName: "admin", Password: "password", PlatformRoleID: models.AdminRole}
 
 	t.Run("NonExistantUser", func(t *testing.T) {
 		admin, err := logic.GetUsers()
@@ -269,7 +269,7 @@ func TestGetUsers(t *testing.T) {
 				assert.Equal(t, true, u.IsAdmin)
 			} else {
 				assert.Equal(t, user.UserName, u.UserName)
-				assert.Equal(t, user.IsAdmin, u.IsAdmin)
+				assert.Equal(t, user.PlatformRoleID, u.PlatformRoleID)
 			}
 		}
 	})
@@ -278,8 +278,8 @@ func TestGetUsers(t *testing.T) {
 
 func TestUpdateUser(t *testing.T) {
 	deleteAllUsers(t)
-	user := models.User{UserName: "admin", Password: "password", IsAdmin: true}
-	newuser := models.User{UserName: "hello", Password: "world", IsAdmin: true}
+	user := models.User{UserName: "admin", Password: "password", PlatformRoleID: models.AdminRole}
+	newuser := models.User{UserName: "hello", Password: "world", PlatformRoleID: models.AdminRole}
 	t.Run("NonExistantUser", func(t *testing.T) {
 		admin, err := logic.UpdateUser(&newuser, &user)
 		assert.EqualError(t, err, "could not find any records")
@@ -322,7 +322,7 @@ func TestUpdateUser(t *testing.T) {
 
 func TestVerifyAuthRequest(t *testing.T) {
 	deleteAllUsers(t)
-	user := models.User{UserName: "admin", Password: "password", IsSuperAdmin: false, IsAdmin: true}
+	user := models.User{UserName: "admin", Password: "password", PlatformRoleID: models.AdminRole}
 	var authRequest models.UserAuthParams
 	t.Run("EmptyUserName", func(t *testing.T) {
 		authRequest.UserName = ""
@@ -346,7 +346,7 @@ func TestVerifyAuthRequest(t *testing.T) {
 		assert.EqualError(t, err, "incorrect credentials")
 	})
 	t.Run("Non-Admin", func(t *testing.T) {
-		user.IsAdmin = false
+		user.PlatformRoleID = models.ServiceUser
 		user.Password = "somepass"
 		user.UserName = "nonadmin"
 		if err := logic.CreateUser(&user); err != nil {

+ 6 - 0
database/database.go

@@ -25,6 +25,8 @@ const (
 	DELETED_NODES_TABLE_NAME = "deletednodes"
 	// USERS_TABLE_NAME - users table
 	USERS_TABLE_NAME = "users"
+	// USER_PERMISSIONS_TABLE_NAME - user permissions table
+	USER_PERMISSIONS_TABLE_NAME = "user_permissions"
 	// CERTS_TABLE_NAME - certificates table
 	CERTS_TABLE_NAME = "certs"
 	// DNS_TABLE_NAME - dns table
@@ -63,6 +65,8 @@ const (
 	HOST_ACTIONS_TABLE_NAME = "hostactions"
 	// PENDING_USERS_TABLE_NAME - table name for pending users
 	PENDING_USERS_TABLE_NAME = "pending_users"
+	// USER_INVITES - table for user invites
+	USER_INVITES_TABLE_NAME = "user_invites"
 	// == ERROR CONSTS ==
 	// NO_RECORD - no singular result found
 	NO_RECORD = "no result found"
@@ -146,6 +150,8 @@ func createTables() {
 	CreateTable(ENROLLMENT_KEYS_TABLE_NAME)
 	CreateTable(HOST_ACTIONS_TABLE_NAME)
 	CreateTable(PENDING_USERS_TABLE_NAME)
+	CreateTable(USER_PERMISSIONS_TABLE_NAME)
+	CreateTable(USER_INVITES_TABLE_NAME)
 }
 
 func CreateTable(tableName string) error {

+ 3 - 3
functions/helpers_test.go

@@ -26,9 +26,9 @@ func TestMain(m *testing.M) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
 	logic.CreateSuperAdmin(&models.User{
-		UserName:     "superadmin",
-		Password:     "password",
-		IsSuperAdmin: true,
+		UserName:       "superadmin",
+		Password:       "password",
+		PlatformRoleID: models.SuperAdminRole,
 	})
 	peerUpdate := make(chan *models.Node)
 	go logic.ManageZombies(context.Background(), peerUpdate)

+ 3 - 0
go.mod

@@ -42,7 +42,9 @@ require (
 	github.com/guumaster/tablewriter v0.0.10
 	github.com/matryer/is v1.4.1
 	github.com/olekukonko/tablewriter v0.0.5
+	github.com/resendlabs/resend-go v1.7.0
 	github.com/spf13/cobra v1.8.1
+	gopkg.in/mail.v2 v2.3.1
 )
 
 require (
@@ -52,6 +54,7 @@ require (
 	github.com/rivo/uniseg v0.2.0 // indirect
 	github.com/seancfoley/bintree v1.3.1 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
+	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
 )
 
 require (

+ 6 - 0
go.sum

@@ -61,6 +61,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/posthog/posthog-go v1.2.18 h1:2CBA0LOB0up+gon+xpeXuhFw69gZpjAYxQoBBGwiDWw=
 github.com/posthog/posthog-go v1.2.18/go.mod h1:QjlpryJtfYLrZF2GUkAhejH4E7WlDbdKkvOi5hLmkdg=
+github.com/resendlabs/resend-go v1.7.0 h1:DycOqSXtw2q7aB+Nt9DDJUDtaYcrNPGn1t5RFposas0=
+github.com/resendlabs/resend-go v1.7.0/go.mod h1:yip1STH7Bqfm4fD0So5HgyNbt5taG5Cplc4xXxETyLI=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -137,8 +139,12 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb h1:9aqVcYEDHmSNb0uOWukxV5lHV09WqiSiCuhEgWNETLY=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ=
+gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
+gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
+gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 68 - 15
logic/auth.go

@@ -49,8 +49,7 @@ func HasSuperAdmin() (bool, error) {
 		if err != nil {
 			continue
 		}
-		if user.IsSuperAdmin {
-			superUser = user
+		if user.PlatformRoleID == models.SuperAdminRole || user.IsSuperAdmin {
 			return true, nil
 		}
 	}
@@ -106,18 +105,58 @@ func GetUsers() ([]models.ReturnUser, error) {
 	return users, err
 }
 
+// IsOauthUser - returns
+func IsOauthUser(user *models.User) error {
+	var currentValue, err = FetchPassValue("")
+	if err != nil {
+		return err
+	}
+	var bCryptErr = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(currentValue))
+	return bCryptErr
+}
+
+func FetchPassValue(newValue string) (string, error) {
+
+	type valueHolder struct {
+		Value string `json:"value" bson:"value"`
+	}
+	newValueHolder := valueHolder{}
+	var currentValue, err = FetchAuthSecret()
+	if err != nil {
+		return "", err
+	}
+	var unmarshErr = json.Unmarshal([]byte(currentValue), &newValueHolder)
+	if unmarshErr != nil {
+		return "", unmarshErr
+	}
+
+	var b64CurrentValue, b64Err = base64.StdEncoding.DecodeString(newValueHolder.Value)
+	if b64Err != nil {
+		logger.Log(0, "could not decode pass")
+		return "", nil
+	}
+	return string(b64CurrentValue), nil
+}
+
 // CreateUser - creates a user
 func CreateUser(user *models.User) error {
 	// check if user exists
 	if _, err := GetUser(user.UserName); err == nil {
 		return errors.New("user exists")
 	}
+	SetUserDefaults(user)
+	if err := IsGroupsValid(user.UserGroups); err != nil {
+		return errors.New("invalid groups: " + err.Error())
+	}
+	if err := IsNetworkRolesValid(user.NetworkRoles); err != nil {
+		return errors.New("invalid network roles: " + err.Error())
+	}
+
 	var err = ValidateUser(user)
 	if err != nil {
 		logger.Log(0, "failed to validate user", err.Error())
 		return err
 	}
-
 	// encrypt that password so we never see it again
 	hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 5)
 	if err != nil {
@@ -126,15 +165,16 @@ func CreateUser(user *models.User) error {
 	}
 	// set password to encrypted password
 	user.Password = string(hash)
-
-	tokenString, _ := CreateUserJWT(user.UserName, user.IsSuperAdmin, user.IsAdmin)
-	if tokenString == "" {
-		logger.Log(0, "failed to generate token")
+	user.AuthType = models.BasicAuth
+	if IsOauthUser(user) == nil {
+		user.AuthType = models.OAuth
+	}
+	_, err = CreateUserJWT(user.UserName, user.PlatformRoleID)
+	if err != nil {
+		logger.Log(0, "failed to generate token", err.Error())
 		return err
 	}
 
-	SetUserDefaults(user)
-
 	// connect db
 	data, err := json.Marshal(user)
 	if err != nil {
@@ -159,8 +199,7 @@ func CreateSuperAdmin(u *models.User) error {
 	if hassuperadmin {
 		return errors.New("superadmin user already exists")
 	}
-	u.IsSuperAdmin = true
-	u.IsAdmin = false
+	u.PlatformRoleID = models.SuperAdminRole
 	return CreateUser(u)
 }
 
@@ -189,7 +228,7 @@ func VerifyAuthRequest(authRequest models.UserAuthParams) (string, error) {
 	}
 
 	// Create a new JWT for the node
-	tokenString, err := CreateUserJWT(authRequest.UserName, result.IsSuperAdmin, result.IsAdmin)
+	tokenString, err := CreateUserJWT(authRequest.UserName, result.PlatformRoleID)
 	if err != nil {
 		slog.Error("error creating jwt", "error", err)
 		return "", err
@@ -250,8 +289,17 @@ func UpdateUser(userchange, user *models.User) (*models.User, error) {
 
 		user.Password = userchange.Password
 	}
-	user.IsAdmin = userchange.IsAdmin
-
+	if err := IsGroupsValid(userchange.UserGroups); err != nil {
+		return userchange, errors.New("invalid groups: " + err.Error())
+	}
+	if err := IsNetworkRolesValid(userchange.NetworkRoles); err != nil {
+		return userchange, errors.New("invalid network roles: " + err.Error())
+	}
+	// Reset Gw Access for service users
+	go UpdateUserGwAccess(*user, *userchange)
+	user.PlatformRoleID = userchange.PlatformRoleID
+	user.UserGroups = userchange.UserGroups
+	user.NetworkRoles = userchange.NetworkRoles
 	err := ValidateUser(user)
 	if err != nil {
 		return &models.User{}, err
@@ -274,12 +322,17 @@ func UpdateUser(userchange, user *models.User) (*models.User, error) {
 // ValidateUser - validates a user model
 func ValidateUser(user *models.User) error {
 
+	// check if role is valid
+	_, err := GetRole(user.PlatformRoleID)
+	if err != nil {
+		return err
+	}
 	v := validator.New()
 	_ = v.RegisterValidation("in_charset", func(fl validator.FieldLevel) bool {
 		isgood := user.NameInCharSet()
 		return isgood
 	})
-	err := v.Struct(user)
+	err = v.Struct(user)
 
 	if err != nil {
 		for _, e := range err.(validator.ValidationErrors) {

+ 31 - 4
logic/gateway.go

@@ -178,6 +178,30 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq
 	if err != nil {
 		return models.Node{}, err
 	}
+	// create network role for this gateway
+	CreateRole(models.UserRolePermissionTemplate{
+		ID:        models.GetRAGRoleID(node.Network, host.ID.String()),
+		UiName:    models.GetRAGRoleName(node.Network, host.Name),
+		NetworkID: models.NetworkID(node.Network),
+		Default:   true,
+		NetworkLevelAccess: map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope{
+			models.RemoteAccessGwRsrc: {
+				models.RsrcID(node.ID.String()): models.RsrcPermissionScope{
+					Read:      true,
+					VPNaccess: true,
+				},
+			},
+			models.ExtClientsRsrc: {
+				models.AllExtClientsRsrcID: models.RsrcPermissionScope{
+					Read:     true,
+					Create:   true,
+					Update:   true,
+					Delete:   true,
+					SelfOnly: true,
+				},
+			},
+		},
+	})
 	err = SetNetworkNodesLastModified(netid)
 	return node, err
 }
@@ -231,6 +255,11 @@ func DeleteIngressGateway(nodeid string) (models.Node, []models.ExtClient, error
 	if err != nil {
 		return models.Node{}, removedClients, err
 	}
+	host, err := GetHost(node.HostID.String())
+	if err != nil {
+		return models.Node{}, removedClients, err
+	}
+	go DeleteRole(models.GetRAGRoleID(node.Network, host.ID.String()), true)
 	err = SetNetworkNodesLastModified(node.Network)
 	return node, removedClients, err
 }
@@ -264,10 +293,8 @@ func IsUserAllowedAccessToExtClient(username string, client models.ExtClient) bo
 	if err != nil {
 		return false
 	}
-	if !user.IsAdmin && !user.IsSuperAdmin {
-		if user.UserName != client.OwnerID {
-			return false
-		}
+	if user.UserName != client.OwnerID {
+		return false
 	}
 	return true
 }

+ 13 - 0
logic/hosts.go

@@ -269,6 +269,19 @@ func UpdateHostFromClient(newHost, currHost *models.Host) (sendPeerUpdate bool)
 	currHost.IsStaticPort = newHost.IsStaticPort
 	currHost.IsStatic = newHost.IsStatic
 	currHost.MTU = newHost.MTU
+	if newHost.Name != currHost.Name {
+		// update any rag role ids
+		for _, nodeID := range newHost.Nodes {
+			node, err := GetNodeByID(nodeID)
+			if err == nil && node.IsIngressGateway {
+				role, err := GetRole(models.GetRAGRoleID(node.Network, currHost.ID.String()))
+				if err == nil {
+					role.UiName = models.GetRAGRoleName(node.Network, newHost.Name)
+					UpdateRole(role)
+				}
+			}
+		}
+	}
 	currHost.Name = newHost.Name
 	if len(newHost.NatType) > 0 && newHost.NatType != currHost.NatType {
 		currHost.NatType = newHost.NatType

+ 46 - 5
logic/jwts.go

@@ -53,12 +53,11 @@ func CreateJWT(uuid string, macAddress string, network string) (response string,
 }
 
 // CreateUserJWT - creates a user jwt token
-func CreateUserJWT(username string, issuperadmin, isadmin bool) (response string, err error) {
+func CreateUserJWT(username string, role models.UserRoleID) (response string, err error) {
 	expirationTime := time.Now().Add(servercfg.GetServerConfig().JwtValidityDuration)
 	claims := &models.UserClaims{
-		UserName:     username,
-		IsSuperAdmin: issuperadmin,
-		IsAdmin:      isadmin,
+		UserName: username,
+		Role:     role,
 		RegisteredClaims: jwt.RegisteredClaims{
 			Issuer:    "Netmaker",
 			Subject:   fmt.Sprintf("user|%s", username),
@@ -87,6 +86,47 @@ func VerifyJWT(bearerToken string) (username string, issuperadmin, isadmin bool,
 	return VerifyUserToken(token)
 }
 
+func GetUserNameFromToken(authtoken string) (username string, err error) {
+	claims := &models.UserClaims{}
+	var tokenSplit = strings.Split(authtoken, " ")
+	var tokenString = ""
+
+	if len(tokenSplit) < 2 {
+		return "", Unauthorized_Err
+	} else {
+		tokenString = tokenSplit[1]
+	}
+	if tokenString == servercfg.GetMasterKey() && servercfg.GetMasterKey() != "" {
+		return MasterUser, nil
+	}
+
+	token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
+		return jwtSecretKey, nil
+	})
+	if err != nil {
+		return "", Unauthorized_Err
+	}
+
+	if token != nil && token.Valid {
+		var user *models.User
+		// check that user exists
+		user, err = GetUser(claims.UserName)
+		if err != nil {
+			return "", err
+		}
+		if user.UserName != "" {
+			return user.UserName, nil
+		}
+		if user.PlatformRoleID != claims.Role {
+			return "", Unauthorized_Err
+		}
+		err = errors.New("user does not exist")
+	} else {
+		err = Unauthorized_Err
+	}
+	return "", err
+}
+
 // VerifyUserToken func will used to Verify the JWT Token while using APIS
 func VerifyUserToken(tokenString string) (username string, issuperadmin, isadmin bool, err error) {
 	claims := &models.UserClaims{}
@@ -107,7 +147,8 @@ func VerifyUserToken(tokenString string) (username string, issuperadmin, isadmin
 			return "", false, false, err
 		}
 		if user.UserName != "" {
-			return user.UserName, user.IsSuperAdmin, user.IsAdmin, nil
+			return user.UserName, user.PlatformRoleID == models.SuperAdminRole,
+				user.PlatformRoleID == models.AdminRole, nil
 		}
 		err = errors.New("user does not exist")
 	}

+ 4 - 0
logic/nodes.go

@@ -196,6 +196,10 @@ func DeleteNode(node *models.Node, purge bool) error {
 		if err := DeleteGatewayExtClients(node.ID.String(), node.Network); err != nil {
 			slog.Error("failed to delete ext clients", "nodeid", node.ID.String(), "error", err.Error())
 		}
+		host, err := GetHost(node.HostID.String())
+		if err == nil {
+			go DeleteRole(models.GetRAGRoleID(node.Network, host.ID.String()), true)
+		}
 	}
 	if node.IsRelayed {
 		// cleanup node from relayednodes on relay node

+ 30 - 2
logic/security.go

@@ -2,9 +2,11 @@ package logic
 
 import (
 	"net/http"
+	"net/url"
 	"strings"
 
 	"github.com/gorilla/mux"
+	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/servercfg"
 )
@@ -17,20 +19,42 @@ const (
 	Unauthorized_Err = models.Error(Unauthorized_Msg)
 )
 
+var NetworkPermissionsCheck = func(username string, r *http.Request) error { return nil }
+var GlobalPermissionsCheck = func(username string, r *http.Request) error { return nil }
+
 // SecurityCheck - Check if user has appropriate permissions
 func SecurityCheck(reqAdmin bool, next http.Handler) http.HandlerFunc {
 
 	return func(w http.ResponseWriter, r *http.Request) {
 		r.Header.Set("ismaster", "no")
+		logger.Log(0, "next", r.URL.String())
+		isGlobalAccesss := r.Header.Get("IS_GLOBAL_ACCESS") == "yes"
 		bearerToken := r.Header.Get("Authorization")
-		username, err := UserPermissions(reqAdmin, bearerToken)
+		username, err := GetUserNameFromToken(bearerToken)
 		if err != nil {
-			ReturnErrorResponse(w, r, FormatError(err, err.Error()))
+			logger.Log(0, "next 1", r.URL.String(), err.Error())
+			ReturnErrorResponse(w, r, FormatError(err, "unauthorized"))
 			return
 		}
 		// detect masteradmin
 		if username == MasterUser {
 			r.Header.Set("ismaster", "yes")
+		} else {
+			if isGlobalAccesss {
+				err = GlobalPermissionsCheck(username, r)
+			} else {
+				err = NetworkPermissionsCheck(username, r)
+			}
+		}
+		w.Header().Set("TARGET_RSRC", r.Header.Get("TARGET_RSRC"))
+		w.Header().Set("TARGET_RSRC_ID", r.Header.Get("TARGET_RSRC_ID"))
+		w.Header().Set("RSRC_TYPE", r.Header.Get("RSRC_TYPE"))
+		w.Header().Set("IS_GLOBAL_ACCESS", r.Header.Get("IS_GLOBAL_ACCESS"))
+		w.Header().Set("Access-Control-Allow-Origin", "*")
+		if err != nil {
+			w.Header().Set("ACCESS_PERM", err.Error())
+			ReturnErrorResponse(w, r, FormatError(err, "forbidden"))
+			return
 		}
 		r.Header.Set("user", username)
 		next.ServeHTTP(w, r)
@@ -75,7 +99,11 @@ func ContinueIfUserMatch(next http.Handler) http.HandlerFunc {
 		}
 		var params = mux.Vars(r)
 		var requestedUser = params["username"]
+		if requestedUser == "" {
+			requestedUser, _ = url.QueryUnescape(r.URL.Query().Get("username"))
+		}
 		if requestedUser != r.Header.Get("user") {
+			logger.Log(0, "next 2", r.URL.String(), errorResponse.Message)
 			ReturnErrorResponse(w, r, errorResponse)
 			return
 		}

+ 75 - 0
logic/user_mgmt.go

@@ -0,0 +1,75 @@
+package logic
+
+import (
+	"encoding/json"
+
+	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/models"
+)
+
+// Pre-Define Permission Templates for default Roles
+var SuperAdminPermissionTemplate = models.UserRolePermissionTemplate{
+	ID:         models.SuperAdminRole,
+	Default:    true,
+	FullAccess: true,
+}
+
+var AdminPermissionTemplate = models.UserRolePermissionTemplate{
+	ID:         models.AdminRole,
+	Default:    true,
+	FullAccess: true,
+}
+
+var GetFilteredNodesByUserAccess = func(user models.User, nodes []models.Node) (filteredNodes []models.Node) {
+	return
+}
+
+var CreateRole = func(r models.UserRolePermissionTemplate) error {
+	return nil
+}
+
+var DeleteRole = func(r models.UserRoleID, force bool) error {
+	return nil
+}
+
+var FilterNetworksByRole = func(allnetworks []models.Network, user models.User) []models.Network {
+	return allnetworks
+}
+
+var IsGroupsValid = func(groups map[models.UserGroupID]struct{}) error {
+	return nil
+}
+var IsNetworkRolesValid = func(networkRoles map[models.NetworkID]map[models.UserRoleID]struct{}) error {
+	return nil
+}
+
+var UpdateUserGwAccess = func(currentUser, changeUser models.User) {}
+
+var UpdateRole = func(r models.UserRolePermissionTemplate) error { return nil }
+
+var InitialiseRoles = userRolesInit
+var DeleteNetworkRoles = func(netID string) {}
+var CreateDefaultNetworkRolesAndGroups = func(netID models.NetworkID) {}
+
+// GetRole - fetches role template by id
+func GetRole(roleID models.UserRoleID) (models.UserRolePermissionTemplate, error) {
+	// check if role already exists
+	data, err := database.FetchRecord(database.USER_PERMISSIONS_TABLE_NAME, roleID.String())
+	if err != nil {
+		return models.UserRolePermissionTemplate{}, err
+	}
+	ur := models.UserRolePermissionTemplate{}
+	err = json.Unmarshal([]byte(data), &ur)
+	if err != nil {
+		return ur, err
+	}
+	return ur, nil
+}
+
+func userRolesInit() {
+	d, _ := json.Marshal(SuperAdminPermissionTemplate)
+	database.Insert(SuperAdminPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+	d, _ = json.Marshal(AdminPermissionTemplate)
+	database.Insert(AdminPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+
+}

+ 76 - 4
logic/users.go

@@ -41,10 +41,13 @@ func GetReturnUser(username string) (models.ReturnUser, error) {
 // ToReturnUser - gets a user as a return user
 func ToReturnUser(user models.User) models.ReturnUser {
 	return models.ReturnUser{
-		UserName:     user.UserName,
-		IsSuperAdmin: user.IsSuperAdmin,
-		IsAdmin:      user.IsAdmin,
-		RemoteGwIDs:  user.RemoteGwIDs,
+		UserName:       user.UserName,
+		PlatformRoleID: user.PlatformRoleID,
+		AuthType:       user.AuthType,
+		UserGroups:     user.UserGroups,
+		NetworkRoles:   user.NetworkRoles,
+		RemoteGwIDs:    user.RemoteGwIDs,
+		LastLoginTime:  user.LastLoginTime,
 	}
 }
 
@@ -53,6 +56,12 @@ func SetUserDefaults(user *models.User) {
 	if user.RemoteGwIDs == nil {
 		user.RemoteGwIDs = make(map[string]struct{})
 	}
+	if len(user.NetworkRoles) == 0 {
+		user.NetworkRoles = make(map[models.NetworkID]map[models.UserRoleID]struct{})
+	}
+	if len(user.UserGroups) == 0 {
+		user.UserGroups = make(map[models.UserGroupID]struct{})
+	}
 }
 
 // SortUsers - Sorts slice of Users by username
@@ -119,3 +128,66 @@ func ListPendingUsers() ([]models.ReturnUser, error) {
 	}
 	return pendingUsers, nil
 }
+
+func GetUserMap() (map[string]models.User, error) {
+	userMap := make(map[string]models.User)
+	records, err := database.FetchRecords(database.USERS_TABLE_NAME)
+	if err != nil && !database.IsEmptyRecord(err) {
+		return userMap, err
+	}
+	for _, record := range records {
+		u := models.User{}
+		err = json.Unmarshal([]byte(record), &u)
+		if err == nil {
+			userMap[u.UserName] = u
+		}
+	}
+	return userMap, nil
+}
+
+func InsertUserInvite(invite models.UserInvite) error {
+	data, err := json.Marshal(invite)
+	if err != nil {
+		return err
+	}
+	return database.Insert(invite.Email, string(data), database.USER_INVITES_TABLE_NAME)
+}
+
+func GetUserInvite(email string) (in models.UserInvite, err error) {
+	d, err := database.FetchRecord(database.USER_INVITES_TABLE_NAME, email)
+	if err != nil {
+		return
+	}
+	err = json.Unmarshal([]byte(d), &in)
+	return
+}
+
+func ListUserInvites() ([]models.UserInvite, error) {
+	invites := []models.UserInvite{}
+	records, err := database.FetchRecords(database.USER_INVITES_TABLE_NAME)
+	if err != nil && !database.IsEmptyRecord(err) {
+		return invites, err
+	}
+	for _, record := range records {
+		in := models.UserInvite{}
+		err = json.Unmarshal([]byte(record), &in)
+		if err == nil {
+			invites = append(invites, in)
+		}
+	}
+	return invites, nil
+}
+
+func DeleteUserInvite(email string) error {
+	return database.DeleteRecord(database.USER_INVITES_TABLE_NAME, email)
+}
+func ValidateAndApproveUserInvite(email, code string) error {
+	in, err := GetUserInvite(email)
+	if err != nil {
+		return err
+	}
+	if code != in.InviteCode {
+		return errors.New("invalid code")
+	}
+	return nil
+}

+ 1 - 1
main.go

@@ -102,7 +102,7 @@ func initialize() { // Client Mode Prereq Check
 	migrate.Run()
 
 	logic.SetJWTSecret()
-
+	logic.InitialiseRoles()
 	err = serverctl.SetDefaults()
 	if err != nil {
 		logger.FatalLog("error setting defaults: ", err.Error())

+ 109 - 3
migrate/migrate.go

@@ -21,6 +21,7 @@ import (
 func Run() {
 	updateEnrollmentKeys()
 	assignSuperAdmin()
+	syncUsers()
 	updateHosts()
 	updateNodes()
 	updateAcls()
@@ -43,8 +44,7 @@ func assignSuperAdmin() {
 		if err != nil {
 			log.Fatal("error getting user", "user", owner, "error", err.Error())
 		}
-		user.IsSuperAdmin = true
-		user.IsAdmin = false
+		user.PlatformRoleID = models.SuperAdminRole
 		err = logic.UpsertUser(*user)
 		if err != nil {
 			log.Fatal(
@@ -64,8 +64,8 @@ func assignSuperAdmin() {
 				slog.Error("error getting user", "user", u.UserName, "error", err.Error())
 				continue
 			}
+			user.PlatformRoleID = models.SuperAdminRole
 			user.IsSuperAdmin = true
-			user.IsAdmin = false
 			err = logic.UpsertUser(*user)
 			if err != nil {
 				slog.Error(
@@ -311,3 +311,109 @@ func MigrateEmqx() {
 	}
 
 }
+
+func syncUsers() {
+	// create default network user roles for existing networks
+	if servercfg.IsPro {
+		networks, _ := logic.GetNetworks()
+		nodes, err := logic.GetAllNodes()
+		if err == nil {
+			for _, netI := range networks {
+				networkNodes := logic.GetNetworkNodesMemory(nodes, netI.NetID)
+				for _, networkNodeI := range networkNodes {
+					if networkNodeI.IsIngressGateway {
+						h, err := logic.GetHost(networkNodeI.HostID.String())
+						if err == nil {
+							logic.CreateRole(models.UserRolePermissionTemplate{
+								ID:        models.GetRAGRoleID(networkNodeI.Network, h.ID.String()),
+								UiName:    models.GetRAGRoleName(networkNodeI.Network, h.Name),
+								NetworkID: models.NetworkID(netI.NetID),
+								NetworkLevelAccess: map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope{
+									models.RemoteAccessGwRsrc: {
+										models.RsrcID(networkNodeI.ID.String()): models.RsrcPermissionScope{
+											Read:      true,
+											VPNaccess: true,
+										},
+									},
+									models.ExtClientsRsrc: {
+										models.AllExtClientsRsrcID: models.RsrcPermissionScope{
+											Read:     true,
+											Create:   true,
+											Update:   true,
+											Delete:   true,
+											SelfOnly: true,
+										},
+									},
+								},
+							})
+						}
+
+					}
+				}
+			}
+		}
+	}
+
+	users, err := logic.GetUsersDB()
+	if err == nil {
+		for _, user := range users {
+			user := user
+			if user.PlatformRoleID == models.AdminRole && !user.IsAdmin {
+				user.IsAdmin = true
+				logic.UpsertUser(user)
+			}
+			if user.PlatformRoleID == models.SuperAdminRole && !user.IsSuperAdmin {
+				user.IsSuperAdmin = true
+				logic.UpsertUser(user)
+			}
+			if user.PlatformRoleID.String() != "" {
+				continue
+			}
+			user.AuthType = models.BasicAuth
+			if logic.IsOauthUser(&user) == nil {
+				user.AuthType = models.OAuth
+			}
+			if len(user.NetworkRoles) == 0 {
+				user.NetworkRoles = make(map[models.NetworkID]map[models.UserRoleID]struct{})
+			}
+			if len(user.UserGroups) == 0 {
+				user.UserGroups = make(map[models.UserGroupID]struct{})
+			}
+			if user.IsSuperAdmin {
+				user.PlatformRoleID = models.SuperAdminRole
+
+			} else if user.IsAdmin {
+				user.PlatformRoleID = models.AdminRole
+			} else {
+				user.PlatformRoleID = models.ServiceUser
+			}
+			logic.UpsertUser(user)
+			if len(user.RemoteGwIDs) > 0 {
+				// define user roles for network
+				// assign relevant network role to user
+				for remoteGwID := range user.RemoteGwIDs {
+					gwNode, err := logic.GetNodeByID(remoteGwID)
+					if err != nil {
+						continue
+					}
+					h, err := logic.GetHost(gwNode.HostID.String())
+					if err != nil {
+						continue
+					}
+					r, err := logic.GetRole(models.GetRAGRoleID(gwNode.Network, h.ID.String()))
+					if err != nil {
+						continue
+					}
+					if netRoles, ok := user.NetworkRoles[models.NetworkID(gwNode.Network)]; ok {
+						netRoles[r.ID] = struct{}{}
+					} else {
+						user.NetworkRoles[models.NetworkID(gwNode.Network)] = map[models.UserRoleID]struct{}{
+							r.ID: {},
+						}
+					}
+				}
+				logic.UpsertUser(user)
+			}
+		}
+	}
+}

+ 5 - 33
models/structs.go

@@ -23,39 +23,6 @@ type AuthParams struct {
 	Password   string `json:"password"`
 }
 
-// User struct - struct for Users
-type User struct {
-	UserName      string              `json:"username" bson:"username" validate:"min=3,max=40,in_charset|email"`
-	Password      string              `json:"password" bson:"password" validate:"required,min=5"`
-	IsAdmin       bool                `json:"isadmin" bson:"isadmin"`
-	IsSuperAdmin  bool                `json:"issuperadmin"`
-	RemoteGwIDs   map[string]struct{} `json:"remote_gw_ids"`
-	LastLoginTime time.Time           `json:"last_login_time"`
-}
-
-// ReturnUser - return user struct
-type ReturnUser struct {
-	UserName      string              `json:"username"`
-	IsAdmin       bool                `json:"isadmin"`
-	IsSuperAdmin  bool                `json:"issuperadmin"`
-	RemoteGwIDs   map[string]struct{} `json:"remote_gw_ids"`
-	LastLoginTime time.Time           `json:"last_login_time"`
-}
-
-// UserAuthParams - user auth params struct
-type UserAuthParams struct {
-	UserName string `json:"username"`
-	Password string `json:"password"`
-}
-
-// UserClaims - user claims struct
-type UserClaims struct {
-	IsAdmin      bool
-	IsSuperAdmin bool
-	UserName     string
-	jwt.RegisteredClaims
-}
-
 // IngressGwUsers - struct to hold users on a ingress gw
 type IngressGwUsers struct {
 	NodeID  string       `json:"node_id"`
@@ -381,3 +348,8 @@ const (
 type GetClientConfReqDto struct {
 	PreferredIp string `json:"preferred_ip"`
 }
+
+type RsrcURLInfo struct {
+	Method string
+	Path   string
+}

+ 199 - 0
models/user_mgmt.go

@@ -0,0 +1,199 @@
+package models
+
+import (
+	"fmt"
+	"time"
+
+	jwt "github.com/golang-jwt/jwt/v4"
+)
+
+type NetworkID string
+type RsrcType string
+type RsrcID string
+type UserRoleID string
+type UserGroupID string
+type AuthType string
+
+var (
+	BasicAuth AuthType = "basic_auth"
+	OAuth     AuthType = "oauth"
+)
+
+func (r RsrcType) String() string {
+	return string(r)
+}
+
+func (rid RsrcID) String() string {
+	return string(rid)
+}
+
+func GetRAGRoleName(netID, hostName string) string {
+	return fmt.Sprintf("netID-%s-rag-%s", netID, hostName)
+}
+
+func GetRAGRoleID(netID, hostID string) UserRoleID {
+	return UserRoleID(fmt.Sprintf("netID-%s-rag-%s", netID, hostID))
+}
+
+var RsrcTypeMap = map[RsrcType]struct{}{
+	HostRsrc:           {},
+	RelayRsrc:          {},
+	RemoteAccessGwRsrc: {},
+	ExtClientsRsrc:     {},
+	InetGwRsrc:         {},
+	EgressGwRsrc:       {},
+	NetworkRsrc:        {},
+	EnrollmentKeysRsrc: {},
+	UserRsrc:           {},
+	AclRsrc:            {},
+	DnsRsrc:            {},
+	FailOverRsrc:       {},
+}
+
+const AllNetworks NetworkID = "all_networks"
+const (
+	HostRsrc           RsrcType = "hosts"
+	RelayRsrc          RsrcType = "relays"
+	RemoteAccessGwRsrc RsrcType = "remote_access_gw"
+	ExtClientsRsrc     RsrcType = "extclients"
+	InetGwRsrc         RsrcType = "inet_gw"
+	EgressGwRsrc       RsrcType = "egress"
+	NetworkRsrc        RsrcType = "networks"
+	EnrollmentKeysRsrc RsrcType = "enrollment_key"
+	UserRsrc           RsrcType = "users"
+	AclRsrc            RsrcType = "acl"
+	DnsRsrc            RsrcType = "dns"
+	FailOverRsrc       RsrcType = "fail_over"
+	MetricRsrc         RsrcType = "metrics"
+)
+
+const (
+	AllHostRsrcID           RsrcID = "all_host"
+	AllRelayRsrcID          RsrcID = "all_relay"
+	AllRemoteAccessGwRsrcID RsrcID = "all_remote_access_gw"
+	AllExtClientsRsrcID     RsrcID = "all_extclients"
+	AllInetGwRsrcID         RsrcID = "all_inet_gw"
+	AllEgressGwRsrcID       RsrcID = "all_egress"
+	AllNetworkRsrcID        RsrcID = "all_network"
+	AllEnrollmentKeysRsrcID RsrcID = "all_enrollment_key"
+	AllUserRsrcID           RsrcID = "all_user"
+	AllDnsRsrcID            RsrcID = "all_dns"
+	AllFailOverRsrcID       RsrcID = "all_fail_over"
+	AllAclsRsrcID           RsrcID = "all_acls"
+)
+
+// Pre-Defined User Roles
+
+const (
+	SuperAdminRole UserRoleID = "super-admin"
+	AdminRole      UserRoleID = "admin"
+	ServiceUser    UserRoleID = "service-user"
+	PlatformUser   UserRoleID = "platform-user"
+	NetworkAdmin   UserRoleID = "network-admin"
+	NetworkUser    UserRoleID = "network-user"
+)
+
+func (r UserRoleID) String() string {
+	return string(r)
+}
+
+func (g UserGroupID) String() string {
+	return string(g)
+}
+
+func (n NetworkID) String() string {
+	return string(n)
+}
+
+type RsrcPermissionScope struct {
+	Create    bool `json:"create"`
+	Read      bool `json:"read"`
+	Update    bool `json:"update"`
+	Delete    bool `json:"delete"`
+	VPNaccess bool `json:"vpn_access"`
+	SelfOnly  bool `json:"self_only"`
+}
+
+type UserRolePermissionTemplate struct {
+	ID                  UserRoleID                                  `json:"id"`
+	UiName              string                                      `json:"ui_name"`
+	Default             bool                                        `json:"default"`
+	DenyDashboardAccess bool                                        `json:"deny_dashboard_access"`
+	FullAccess          bool                                        `json:"full_access"`
+	NetworkID           NetworkID                                   `json:"network_id"`
+	NetworkLevelAccess  map[RsrcType]map[RsrcID]RsrcPermissionScope `json:"network_level_access"`
+	GlobalLevelAccess   map[RsrcType]map[RsrcID]RsrcPermissionScope `json:"global_level_access"`
+}
+
+type CreateGroupReq struct {
+	Group   UserGroup `json:"user_group"`
+	Members []string  `json:"members"`
+}
+
+type UserGroup struct {
+	ID           UserGroupID                           `json:"id"`
+	NetworkRoles map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
+	MetaData     string                                `json:"meta_data"`
+}
+
+// User struct - struct for Users
+type User struct {
+	UserName       string                                `json:"username" bson:"username" validate:"min=3,max=40,in_charset|email"`
+	Password       string                                `json:"password" bson:"password" validate:"required,min=5"`
+	IsAdmin        bool                                  `json:"isadmin" bson:"isadmin"` // deprecated
+	IsSuperAdmin   bool                                  `json:"issuperadmin"`           // deprecated
+	RemoteGwIDs    map[string]struct{}                   `json:"remote_gw_ids"`          // deprecated
+	AuthType       AuthType                              `json:"auth_type"`
+	UserGroups     map[UserGroupID]struct{}              `json:"user_group_ids"`
+	PlatformRoleID UserRoleID                            `json:"platform_role_id"`
+	NetworkRoles   map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
+	LastLoginTime  time.Time                             `json:"last_login_time"`
+}
+
+type ReturnUserWithRolesAndGroups struct {
+	ReturnUser
+	PlatformRole UserRolePermissionTemplate `json:"platform_role"`
+}
+
+// ReturnUser - return user struct
+type ReturnUser struct {
+	UserName       string                                `json:"username"`
+	IsAdmin        bool                                  `json:"isadmin"`
+	IsSuperAdmin   bool                                  `json:"issuperadmin"`
+	AuthType       AuthType                              `json:"auth_type"`
+	RemoteGwIDs    map[string]struct{}                   `json:"remote_gw_ids"` // deprecated
+	UserGroups     map[UserGroupID]struct{}              `json:"user_group_ids"`
+	PlatformRoleID UserRoleID                            `json:"platform_role_id"`
+	NetworkRoles   map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
+	LastLoginTime  time.Time                             `json:"last_login_time"`
+}
+
+// UserAuthParams - user auth params struct
+type UserAuthParams struct {
+	UserName string `json:"username"`
+	Password string `json:"password"`
+}
+
+// UserClaims - user claims struct
+type UserClaims struct {
+	Role     UserRoleID
+	UserName string
+	jwt.RegisteredClaims
+}
+
+type InviteUsersReq struct {
+	UserEmails     []string                              `json:"user_emails"`
+	PlatformRoleID string                                `json:"platform_role_id"`
+	UserGroups     map[UserGroupID]struct{}              `json:"user_group_ids"`
+	NetworkRoles   map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
+}
+
+// UserInvite - model for user invite
+type UserInvite struct {
+	Email          string                                `json:"email"`
+	PlatformRoleID string                                `json:"platform_role_id"`
+	UserGroups     map[UserGroupID]struct{}              `json:"user_group_ids"`
+	NetworkRoles   map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
+	InviteCode     string                                `json:"invite_code"`
+	InviteURL      string                                `json:"invite_url"`
+}

+ 42 - 14
pro/auth/azure-ad.go

@@ -8,11 +8,11 @@ import (
 	"net/http"
 	"strings"
 
-	"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"
+	proLogic "github.com/gravitl/netmaker/pro/logic"
 	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/oauth2"
 	"golang.org/x/oauth2/microsoft"
@@ -67,27 +67,50 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		return
 	}
-	if !isEmailAllowed(content.UserPrincipalName) {
-		handleOauthUserNotAllowedToSignUp(w)
-		return
+
+	var inviteExists bool
+	// check if invite exists for User
+	in, err := logic.GetUserInvite(content.UserPrincipalName)
+	if err == nil {
+		inviteExists = true
 	}
 	// check if user approval is already pending
-	if logic.IsPendingUser(content.UserPrincipalName) {
+	if !inviteExists && logic.IsPendingUser(content.UserPrincipalName) {
 		handleOauthUserSignUpApprovalPending(w)
 		return
 	}
+
 	_, err = logic.GetUser(content.UserPrincipalName)
 	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)
+			if inviteExists {
+				// create user
+				user, err := proLogic.PrepareOauthUserFromInvite(in)
+				if err != nil {
+					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+					return
+				}
+				if err = logic.CreateUser(&user); err != nil {
+					handleSomethingWentWrong(w)
+					return
+				}
+				logic.DeleteUserInvite(user.UserName)
+				logic.DeletePendingUser(content.UserPrincipalName)
+			} else {
+				if !isEmailAllowed(content.UserPrincipalName) {
+					handleOauthUserNotAllowedToSignUp(w)
+					return
+				}
+				err = logic.InsertPendingUser(&models.User{
+					UserName: content.UserPrincipalName,
+				})
+				if err != nil {
+					handleSomethingWentWrong(w)
+					return
+				}
+				handleFirstTimeOauthUserSignUp(w)
 				return
 			}
-			handleFirstTimeOauthUserSignUp(w)
-			return
 		} else {
 			handleSomethingWentWrong(w)
 			return
@@ -98,11 +121,16 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotFound(w)
 		return
 	}
-	if !(user.IsSuperAdmin || user.IsAdmin) {
+	userRole, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		handleSomethingWentWrong(w)
+		return
+	}
+	if userRole.DenyDashboardAccess {
 		handleOauthUserNotAllowed(w)
 		return
 	}
-	var newPass, fetchErr = auth.FetchPassValue("")
+	var newPass, fetchErr = logic.FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}

+ 98 - 44
pro/auth/error.go

@@ -1,60 +1,114 @@
 package auth
 
-import "net/http"
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/gravitl/netmaker/servercfg"
+)
+
+var htmlBaseTemplate = `<!DOCTYPE html>
+<html lang="en">
+
+<head>
+	<meta charset="UTF-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+	<meta http-equiv="X-UA-Compatible" content="ie=edge">
+	<title>Netmaker :: SSO</title>
+	<script type="text/javascript">
+	function redirect()
+    {
+    	window.location.href="` + servercfg.GetFrontendURL() + `";
+    }
+	</script>
+	<style>
+		html,
+		body {
+			margin: 0px;
+			padding: 0px;
+		}
+
+		body {
+			height: 100vh;
+			overflow: hidden;
+			display: flex;
+			flex-flow: column nowrap;
+			justify-content: center;
+			align-items: center;
+		}
+
+		#logo {
+			width: 150px;
+		}
+
+		h3 {
+			margin-bottom: 3rem;
+			color: rgb(25, 135, 84);
+			font-size: xx-large;
+		}
+
+		h4 {
+			margin-bottom: 0px;
+		}
+
+		p {
+			margin-top: 0px;
+			margin-bottom: 0px;
+		}
+		.back-to-login-btn {
+			background: #5E5DF0;
+			border-radius: 999px;
+			box-shadow: #5E5DF0 0 10px 20px -10px;
+			box-sizing: border-box;
+			color: #FFFFFF;
+			cursor: pointer;
+			font-family: Inter,Helvetica,"Apple Color Emoji","Segoe UI Emoji",NotoColorEmoji,"Noto Color Emoji","Segoe UI Symbol","Android Emoji",EmojiSymbols,-apple-system,system-ui,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans",sans-serif;
+			font-size: 16px;
+			font-weight: 700;
+			line-height: 24px;
+			opacity: 1;
+			outline: 0 solid transparent;
+			padding: 8px 18px;
+			user-select: none;
+			-webkit-user-select: none;
+			touch-action: manipulation;
+			width: fit-content;
+			word-break: break-word;
+			border: 0;
+			margin: 20px;
+		  }
+	</style>
+</head>
 
-// == define error HTML here ==
-const oauthNotConfigured = `<!DOCTYPE html><html>
 <body>
-<h3>Your Netmaker server does not have OAuth configured.</h3>
-<p>Please visit the docs <a href="https://docs.netmaker.org/oauth.html" target="_blank" rel="noopener">here</a> to learn how to.</p>
+	<img
+		src="https://raw.githubusercontent.com/gravitl/netmaker-docs/master/images/netmaker-github/netmaker-teal.png"
+		alt="netmaker logo"
+		id="logo"
+	>
+	%s
+	<button class="back-to-login-btn" onClick="redirect()" role="button">Back To Login</button>
+	
 </body>
 </html>`
 
-const oauthStateInvalid = `<!DOCTYPE html><html>
-<body>
-<h3>Invalid OAuth Session. Please re-try again.</h3>
-</body>
-</html>`
+var oauthNotConfigured = fmt.Sprintf(htmlBaseTemplate, `<h2>Your Netmaker server does not have OAuth configured.</h2>
+<p>Please visit the docs <a href="https://docs.netmaker.org/oauth.html" target="_blank" rel="noopener">here</a> to learn how to.</p>`)
 
-const userNotAllowed = `<!DOCTYPE html><html>
-<body>
-<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>
-`
+var oauthStateInvalid = fmt.Sprintf(htmlBaseTemplate, `<h2>Invalid OAuth Session. Please re-try again.</h2>`)
 
-const userFirstTimeSignUp = `<!DOCTYPE html><html>
-<body>
-<h3>Thank you for signing up. Please contact your administrator for access.</h3>
-</body>
-</html>
-`
+var userNotAllowed = fmt.Sprintf(htmlBaseTemplate, `<h2>Your account does not have access to the dashboard. Please contact your administrator for more information about your account.</h2>
+<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>`)
 
-const userSignUpApprovalPending = `<!DOCTYPE html><html>
-<body>
-<h3>Your account is yet to be approved. Please contact your administrator for access.</h3>
-</body>
-</html>
-`
+var userFirstTimeSignUp = fmt.Sprintf(htmlBaseTemplate, `<h2>Thank you for signing up. Please contact your administrator for access.</h2>`)
 
-const userNotFound = `<!DOCTYPE html><html>
-<body>
-<h3>User Not Found.</h3>
-</body>
-</html>`
+var userSignUpApprovalPending = fmt.Sprintf(htmlBaseTemplate, `<h2>Your account is yet to be approved. Please contact your administrator for access.</h2>`)
 
-const somethingwentwrong = `<!DOCTYPE html><html>
-<body>
-<h3>Something went wrong. Contact Admin.</h3>
-</body>
-</html>`
+var userNotFound = fmt.Sprintf(htmlBaseTemplate, `<h2>User Not Found.</h2>`)
 
-const notallowedtosignup = `<!DOCTYPE html><html>
-<body>
-<h3>Your email is not allowed. Please contact your administrator.</h3>
-</body>
-</html>`
+var somethingwentwrong = fmt.Sprintf(htmlBaseTemplate, `<h2>Something went wrong. Contact Admin.</h2>`)
+
+var notallowedtosignup = fmt.Sprintf(htmlBaseTemplate, `<h2>Your email is not allowed. Please contact your administrator.</h2>`)
 
 func handleOauthUserNotFound(response http.ResponseWriter) {
 	response.Header().Set("Content-Type", "text/html; charset=utf-8")

+ 41 - 14
pro/auth/github.go

@@ -8,11 +8,11 @@ import (
 	"net/http"
 	"strings"
 
-	"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"
+	proLogic "github.com/gravitl/netmaker/pro/logic"
 	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/oauth2"
 	"golang.org/x/oauth2/github"
@@ -67,27 +67,49 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		return
 	}
-	if !isEmailAllowed(content.Login) {
-		handleOauthUserNotAllowedToSignUp(w)
-		return
+
+	var inviteExists bool
+	// check if invite exists for User
+	in, err := logic.GetUserInvite(content.Login)
+	if err == nil {
+		inviteExists = true
 	}
 	// check if user approval is already pending
-	if logic.IsPendingUser(content.Login) {
+	if !inviteExists && logic.IsPendingUser(content.Login) {
 		handleOauthUserSignUpApprovalPending(w)
 		return
 	}
 	_, err = logic.GetUser(content.Login)
 	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)
+			if inviteExists {
+				// create user
+				user, err := proLogic.PrepareOauthUserFromInvite(in)
+				if err != nil {
+					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+					return
+				}
+				if err = logic.CreateUser(&user); err != nil {
+					handleSomethingWentWrong(w)
+					return
+				}
+				logic.DeleteUserInvite(user.UserName)
+				logic.DeletePendingUser(content.Login)
+			} else {
+				if !isEmailAllowed(content.Login) {
+					handleOauthUserNotAllowedToSignUp(w)
+					return
+				}
+				err = logic.InsertPendingUser(&models.User{
+					UserName: content.Login,
+				})
+				if err != nil {
+					handleSomethingWentWrong(w)
+					return
+				}
+				handleFirstTimeOauthUserSignUp(w)
 				return
 			}
-			handleFirstTimeOauthUserSignUp(w)
-			return
 		} else {
 			handleSomethingWentWrong(w)
 			return
@@ -98,11 +120,16 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotFound(w)
 		return
 	}
-	if !(user.IsSuperAdmin || user.IsAdmin) {
+	userRole, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		handleSomethingWentWrong(w)
+		return
+	}
+	if userRole.DenyDashboardAccess {
 		handleOauthUserNotAllowed(w)
 		return
 	}
-	var newPass, fetchErr = auth.FetchPassValue("")
+	var newPass, fetchErr = logic.FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}

+ 53 - 17
pro/auth/google.go

@@ -9,11 +9,11 @@ import (
 	"strings"
 	"time"
 
-	"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"
+	proLogic "github.com/gravitl/netmaker/pro/logic"
 	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/oauth2"
 	"golang.org/x/oauth2/google"
@@ -45,7 +45,7 @@ func handleGoogleLogin(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		return
 	}
-
+	logger.Log(0, "Setting OAuth State ", oauth_state_string)
 	if err := logic.SetState(oauth_state_string); err != nil {
 		handleOauthNotConfigured(w)
 		return
@@ -58,7 +58,7 @@ func handleGoogleLogin(w http.ResponseWriter, r *http.Request) {
 func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 
 	var rState, rCode = getStateAndCode(r)
-
+	logger.Log(0, "Fetched OAuth State ", rState)
 	var content, err = getGoogleUserInfo(rState, rCode)
 	if err != nil {
 		logger.Log(1, "error when getting user info from google:", err.Error())
@@ -69,43 +69,78 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		return
 	}
-	if !isEmailAllowed(content.Email) {
-		handleOauthUserNotAllowedToSignUp(w)
-		return
-	}
+	logger.Log(0, "CALLBACK ----> 1")
+
+	logger.Log(0, "CALLBACK ----> 2")
+	var inviteExists bool
+	// check if invite exists for User
+	in, err := logic.GetUserInvite(content.Email)
+	if err == nil {
+		inviteExists = true
+	}
+	logger.Log(0, fmt.Sprintf("CALLBACK ----> 3  %v", inviteExists))
 	// check if user approval is already pending
-	if logic.IsPendingUser(content.Email) {
+	if !inviteExists && logic.IsPendingUser(content.Email) {
 		handleOauthUserSignUpApprovalPending(w)
 		return
 	}
+	logger.Log(0, "CALLBACK ----> 4")
 	_, err = logic.GetUser(content.Email)
 	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)
+			if inviteExists {
+				// create user
+				user, err := proLogic.PrepareOauthUserFromInvite(in)
+				if err != nil {
+					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+					return
+				}
+				logger.Log(0, "CALLBACK ----> 4.0")
+
+				if err = logic.CreateUser(&user); err != nil {
+					handleSomethingWentWrong(w)
+					return
+				}
+				logic.DeleteUserInvite(user.UserName)
+				logic.DeletePendingUser(content.Email)
+			} else {
+				if !isEmailAllowed(content.Email) {
+					handleOauthUserNotAllowedToSignUp(w)
+					return
+				}
+				err = logic.InsertPendingUser(&models.User{
+					UserName: content.Email,
+				})
+				if err != nil {
+					handleSomethingWentWrong(w)
+					return
+				}
+				handleFirstTimeOauthUserSignUp(w)
 				return
 			}
-			handleFirstTimeOauthUserSignUp(w)
-			return
+
 		} else {
 			handleSomethingWentWrong(w)
 			return
 		}
 	}
+	logger.Log(0, "CALLBACK ----> 6")
 	user, err := logic.GetUser(content.Email)
 	if err != nil {
 		logger.Log(0, "error fetching user: ", err.Error())
 		handleOauthUserNotFound(w)
 		return
 	}
-	if !(user.IsSuperAdmin || user.IsAdmin) {
+	userRole, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		handleSomethingWentWrong(w)
+		return
+	}
+	if userRole.DenyDashboardAccess {
 		handleOauthUserNotAllowed(w)
 		return
 	}
-	var newPass, fetchErr = auth.FetchPassValue("")
+	var newPass, fetchErr = logic.FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}
@@ -151,6 +186,7 @@ func getGoogleUserInfo(state string, code string) (*OAuthUser, error) {
 	if err != nil {
 		return nil, fmt.Errorf("failed reading response body: %s", err.Error())
 	}
+	logger.Log(0, fmt.Sprintf("---------------> USERINFO: %v, token: %s", string(contents), token.AccessToken))
 	var userInfo = &OAuthUser{}
 	if err = json.Unmarshal(contents, userInfo); err != nil {
 		return nil, fmt.Errorf("failed parsing email from response data: %s", err.Error())

+ 1 - 2
pro/auth/headless_callback.go

@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"net/http"
 
-	"github.com/gravitl/netmaker/auth"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
@@ -78,7 +77,7 @@ func HandleHeadlessSSOCallback(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 	}
-	newPass, fetchErr := auth.FetchPassValue("")
+	newPass, fetchErr := logic.FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}

+ 41 - 14
pro/auth/oidc.go

@@ -8,11 +8,11 @@ import (
 	"time"
 
 	"github.com/coreos/go-oidc/v3/oidc"
-	"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"
+	proLogic "github.com/gravitl/netmaker/pro/logic"
 	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/oauth2"
 )
@@ -80,27 +80,49 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		return
 	}
-	if !isEmailAllowed(content.Email) {
-		handleOauthUserNotAllowedToSignUp(w)
-		return
+
+	var inviteExists bool
+	// check if invite exists for User
+	in, err := logic.GetUserInvite(content.Login)
+	if err == nil {
+		inviteExists = true
 	}
 	// check if user approval is already pending
-	if logic.IsPendingUser(content.Email) {
+	if !inviteExists && logic.IsPendingUser(content.Email) {
 		handleOauthUserSignUpApprovalPending(w)
 		return
 	}
 	_, err = logic.GetUser(content.Email)
 	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)
+			if inviteExists {
+				// create user
+				user, err := proLogic.PrepareOauthUserFromInvite(in)
+				if err != nil {
+					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+					return
+				}
+				if err = logic.CreateUser(&user); err != nil {
+					handleSomethingWentWrong(w)
+					return
+				}
+				logic.DeleteUserInvite(user.UserName)
+				logic.DeletePendingUser(content.Email)
+			} else {
+				if !isEmailAllowed(content.Email) {
+					handleOauthUserNotAllowedToSignUp(w)
+					return
+				}
+				err = logic.InsertPendingUser(&models.User{
+					UserName: content.Email,
+				})
+				if err != nil {
+					handleSomethingWentWrong(w)
+					return
+				}
+				handleFirstTimeOauthUserSignUp(w)
 				return
 			}
-			handleFirstTimeOauthUserSignUp(w)
-			return
 		} else {
 			handleSomethingWentWrong(w)
 			return
@@ -111,11 +133,16 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotFound(w)
 		return
 	}
-	if !(user.IsSuperAdmin || user.IsAdmin) {
+	userRole, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		handleSomethingWentWrong(w)
+		return
+	}
+	if userRole.DenyDashboardAccess {
 		handleOauthUserNotAllowed(w)
 		return
 	}
-	var newPass, fetchErr = auth.FetchPassValue("")
+	var newPass, fetchErr = logic.FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}

+ 2 - 1
pro/auth/register_callback.go

@@ -10,6 +10,7 @@ import (
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic/pro/netcache"
+	"github.com/gravitl/netmaker/models"
 )
 
 var (
@@ -73,7 +74,7 @@ func HandleHostSSOCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotFound(w)
 		return
 	}
-	if !user.IsAdmin && !user.IsSuperAdmin {
+	if user.PlatformRoleID != models.AdminRole && user.PlatformRoleID != models.SuperAdminRole {
 		response := returnErrTemplate(userClaims.getUserName(), "only admin users can register using SSO", state, reqKeyIf)
 		w.WriteHeader(http.StatusForbidden)
 		w.Write(response)

+ 3 - 6
pro/controllers/relay.go

@@ -19,12 +19,9 @@ import (
 // RelayHandlers - handle Pro Relays
 func RelayHandlers(r *mux.Router) {
 
-	r.HandleFunc("/api/nodes/{network}/{nodeid}/createrelay", controller.Authorize(false, true, "user", http.HandlerFunc(createRelay))).
-		Methods(http.MethodPost)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}/deleterelay", controller.Authorize(false, true, "user", http.HandlerFunc(deleteRelay))).
-		Methods(http.MethodDelete)
-	r.HandleFunc("/api/v1/host/{hostid}/failoverme", controller.Authorize(true, false, "host", http.HandlerFunc(failOverME))).
-		Methods(http.MethodPost)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/createrelay", logic.SecurityCheck(true, http.HandlerFunc(createRelay))).Methods(http.MethodPost)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/deleterelay", logic.SecurityCheck(true, http.HandlerFunc(deleteRelay))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/v1/host/{hostid}/failoverme", controller.Authorize(true, false, "host", http.HandlerFunc(failOverME))).Methods(http.MethodPost)
 }
 
 // @Summary     Create a relay

+ 789 - 164
pro/controllers/users.go

@@ -1,34 +1,630 @@
 package controllers
 
 import (
+	"context"
 	"encoding/json"
 	"errors"
 	"fmt"
 	"net/http"
+	"net/url"
 
 	"github.com/gorilla/mux"
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
-	"github.com/gravitl/netmaker/pro/auth"
+	proAuth "github.com/gravitl/netmaker/pro/auth"
+	"github.com/gravitl/netmaker/pro/email"
+	proLogic "github.com/gravitl/netmaker/pro/logic"
 	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/exp/slog"
 )
 
 func UserHandlers(r *mux.Router) {
-	r.HandleFunc("/api/users/{username}/remote_access_gw/{remote_access_gateway_id}", logic.SecurityCheck(true, http.HandlerFunc(attachUserToRemoteAccessGw))).
-		Methods(http.MethodPost)
-	r.HandleFunc("/api/users/{username}/remote_access_gw/{remote_access_gateway_id}", logic.SecurityCheck(true, http.HandlerFunc(removeUserFromRemoteAccessGW))).
-		Methods(http.MethodDelete)
-	r.HandleFunc("/api/users/{username}/remote_access_gw", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUserRemoteAccessGws)))).
-		Methods(http.MethodGet)
-	r.HandleFunc("/api/users/ingress/{ingress_id}", logic.SecurityCheck(true, http.HandlerFunc(ingressGatewayUsers))).
-		Methods(http.MethodGet)
-	r.HandleFunc("/api/oauth/login", auth.HandleAuthLogin).Methods(http.MethodGet)
-	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/oauth/login", proAuth.HandleAuthLogin).Methods(http.MethodGet)
+	r.HandleFunc("/api/oauth/callback", proAuth.HandleAuthCallback).Methods(http.MethodGet)
+	r.HandleFunc("/api/oauth/headless", proAuth.HandleHeadlessSSO)
+	r.HandleFunc("/api/oauth/register/{regKey}", proAuth.RegisterHostSSO).Methods(http.MethodGet)
+
+	// User Role Handlers
+	r.HandleFunc("/api/v1/users/roles", logic.SecurityCheck(true, http.HandlerFunc(listRoles))).Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/users/role", logic.SecurityCheck(true, http.HandlerFunc(getRole))).Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/users/role", logic.SecurityCheck(true, http.HandlerFunc(createRole))).Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/users/role", logic.SecurityCheck(true, http.HandlerFunc(updateRole))).Methods(http.MethodPut)
+	r.HandleFunc("/api/v1/users/role", logic.SecurityCheck(true, http.HandlerFunc(deleteRole))).Methods(http.MethodDelete)
+
+	// User Group Handlers
+	r.HandleFunc("/api/v1/users/groups", logic.SecurityCheck(true, http.HandlerFunc(listUserGroups))).Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/users/group", logic.SecurityCheck(true, http.HandlerFunc(getUserGroup))).Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/users/group", logic.SecurityCheck(true, http.HandlerFunc(createUserGroup))).Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/users/group", logic.SecurityCheck(true, http.HandlerFunc(updateUserGroup))).Methods(http.MethodPut)
+	r.HandleFunc("/api/v1/users/group", logic.SecurityCheck(true, http.HandlerFunc(deleteUserGroup))).Methods(http.MethodDelete)
+
+	// User Invite Handlers
+	r.HandleFunc("/api/v1/users/invite", userInviteVerify).Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/users/invite-signup", userInviteSignUp).Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/users/invite", logic.SecurityCheck(true, http.HandlerFunc(inviteUsers))).Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/users/invites", logic.SecurityCheck(true, http.HandlerFunc(listUserInvites))).Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/users/invite", logic.SecurityCheck(true, http.HandlerFunc(deleteUserInvite))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/v1/users/invites", logic.SecurityCheck(true, http.HandlerFunc(deleteAllUserInvites))).Methods(http.MethodDelete)
+
+	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)
+
+	r.HandleFunc("/api/users/{username}/remote_access_gw/{remote_access_gateway_id}", logic.SecurityCheck(true, http.HandlerFunc(attachUserToRemoteAccessGw))).Methods(http.MethodPost)
+	r.HandleFunc("/api/users/{username}/remote_access_gw/{remote_access_gateway_id}", logic.SecurityCheck(true, http.HandlerFunc(removeUserFromRemoteAccessGW))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/users/{username}/remote_access_gw", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUserRemoteAccessGwsV1)))).Methods(http.MethodGet)
+	r.HandleFunc("/api/users/ingress/{ingress_id}", logic.SecurityCheck(true, http.HandlerFunc(ingressGatewayUsers))).Methods(http.MethodGet)
+
+}
+
+// swagger:route POST /api/v1/users/invite-signup user userInviteSignUp
+//
+// user signup via invite.
+//
+//	Schemes: https
+//
+//	Responses:
+//		200: ReturnSuccessResponse
+func userInviteSignUp(w http.ResponseWriter, r *http.Request) {
+	email, _ := url.QueryUnescape(r.URL.Query().Get("email"))
+	code, _ := url.QueryUnescape(r.URL.Query().Get("invite_code"))
+	in, err := logic.GetUserInvite(email)
+	if err != nil {
+		logger.Log(0, "failed to fetch users: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	if code != in.InviteCode {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("invalid invite code"), "badrequest"))
+		return
+	}
+	// check if user already exists
+	_, err = logic.GetUser(email)
+	if err == nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("user already exists"), "badrequest"))
+		return
+	}
+	var user models.User
+	err = json.NewDecoder(r.Body).Decode(&user)
+	if err != nil {
+		logger.Log(0, user.UserName, "error decoding request body: ",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	if user.UserName != email {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("username not matching with invite"), "badrequest"))
+		return
+	}
+	if user.Password == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("password cannot be empty"), "badrequest"))
+		return
+	}
+
+	user.UserGroups = in.UserGroups
+	user.PlatformRoleID = models.UserRoleID(in.PlatformRoleID)
+	if user.PlatformRoleID == "" {
+		user.PlatformRoleID = models.ServiceUser
+	}
+	user.NetworkRoles = in.NetworkRoles
+	err = logic.CreateUser(&user)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	// delete invite
+	logic.DeleteUserInvite(email)
+	logic.DeletePendingUser(email)
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+	logic.ReturnSuccessResponse(w, r, "created user successfully "+email)
+}
+
+// swagger:route GET /api/v1/users/invite user userInviteVerify
+//
+// verfies user invite.
+//
+//	Schemes: https
+//
+//	Responses:
+//		200: ReturnSuccessResponse
+func userInviteVerify(w http.ResponseWriter, r *http.Request) {
+	email, _ := url.QueryUnescape(r.URL.Query().Get("email"))
+	code, _ := url.QueryUnescape(r.URL.Query().Get("invite_code"))
+	err := logic.ValidateAndApproveUserInvite(email, code)
+	if err != nil {
+		logger.Log(0, "failed to fetch users: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	logic.ReturnSuccessResponse(w, r, "invite is valid")
+}
+
+// swagger:route POST /api/v1/users/invite user inviteUsers
+//
+// invite users.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
+func inviteUsers(w http.ResponseWriter, r *http.Request) {
+	var inviteReq models.InviteUsersReq
+	err := json.NewDecoder(r.Body).Decode(&inviteReq)
+	if err != nil {
+		slog.Error("error decoding request body", "error",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	callerUserName := r.Header.Get("user")
+	caller, err := logic.GetUser(callerUserName)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "notfound"))
+		return
+	}
+	if inviteReq.PlatformRoleID == models.SuperAdminRole.String() {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("super admin cannot be invited"), "badrequest"))
+		return
+	}
+	if inviteReq.PlatformRoleID == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("platform role id cannot be empty"), "badrequest"))
+		return
+	}
+	if (inviteReq.PlatformRoleID == models.AdminRole.String() ||
+		inviteReq.PlatformRoleID == models.SuperAdminRole.String()) && caller.PlatformRoleID != models.SuperAdminRole {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only superadmin can invite admin users"), "forbidden"))
+		return
+	}
+	//validate Req
+	err = proLogic.IsGroupsValid(inviteReq.UserGroups)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	err = proLogic.IsNetworkRolesValid(inviteReq.NetworkRoles)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	// check platform role
+	_, err = logic.GetRole(models.UserRoleID(inviteReq.PlatformRoleID))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	for _, inviteeEmail := range inviteReq.UserEmails {
+		// check if user with email exists, then ignore
+		_, err := logic.GetUser(inviteeEmail)
+		if err == nil {
+			// user exists already, so ignore
+			continue
+		}
+		invite := models.UserInvite{
+			Email:          inviteeEmail,
+			PlatformRoleID: inviteReq.PlatformRoleID,
+			UserGroups:     inviteReq.UserGroups,
+			NetworkRoles:   inviteReq.NetworkRoles,
+			InviteCode:     logic.RandomString(8),
+		}
+		u, err := url.Parse(fmt.Sprintf("%s/invite?email=%s&invite_code=%s",
+			servercfg.GetFrontendURL(), url.QueryEscape(invite.Email), url.QueryEscape(invite.InviteCode)))
+		if err != nil {
+			slog.Error("failed to parse to invite url", "error", err)
+			return
+		}
+		invite.InviteURL = u.String()
+		err = logic.InsertUserInvite(invite)
+		if err != nil {
+			slog.Error("failed to insert invite for user", "email", invite.Email, "error", err)
+		}
+		// notify user with magic link
+		go func(invite models.UserInvite) {
+			// Set E-Mail body. You can set plain text or html with text/html
+
+			e := email.UserInvitedMail{
+				BodyBuilder: &email.EmailBodyBuilderWithH1HeadlineAndImage{},
+				InviteURL:   invite.InviteURL,
+			}
+			n := email.Notification{
+				RecipientMail: invite.Email,
+			}
+			err = email.GetClient().SendEmail(context.Background(), n, e)
+			if err != nil {
+				slog.Error("failed to send email invite", "user", invite.Email, "error", err)
+			}
+		}(invite)
+	}
+
+}
+
+// swagger:route GET /api/v1/users/invites user listUserInvites
+//
+// lists all pending invited users.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: ReturnSuccessResponseWithJson
+func listUserInvites(w http.ResponseWriter, r *http.Request) {
+	usersInvites, err := logic.ListUserInvites()
+	if err != nil {
+		logger.Log(0, "failed to fetch users: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	logic.ReturnSuccessResponseWithJson(w, r, usersInvites, "fetched pending user invites")
+}
+
+// swagger:route DELETE /api/v1/users/invite user deleteUserInvite
+//
+// delete pending invite.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: ReturnSuccessResponse
+func deleteUserInvite(w http.ResponseWriter, r *http.Request) {
+	email, _ := url.QueryUnescape(r.URL.Query().Get("invitee_email"))
+	err := logic.DeleteUserInvite(email)
+	if err != nil {
+		logger.Log(0, "failed to delete user invite: ", email, err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	logic.ReturnSuccessResponse(w, r, "deleted user invite")
+}
+
+// swagger:route DELETE /api/v1/users/invites user deleteAllUserInvites
+//
+// deletes all pending invites.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: ReturnSuccessResponse
+func deleteAllUserInvites(w http.ResponseWriter, r *http.Request) {
+	err := database.DeleteAllRecords(database.USER_INVITES_TABLE_NAME)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to delete all pending user invites "+err.Error()), "internal"))
+		return
+	}
+	logic.ReturnSuccessResponse(w, r, "cleared all pending user invites")
+}
+
+// swagger:route GET /api/v1/user/groups user listUserGroups
+//
+// Get all user groups.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
+func listUserGroups(w http.ResponseWriter, r *http.Request) {
+	groups, err := proLogic.ListUserGroups()
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, models.ErrorResponse{
+			Code:    http.StatusInternalServerError,
+			Message: err.Error(),
+		})
+		return
+	}
+	logic.ReturnSuccessResponseWithJson(w, r, groups, "successfully fetched user groups")
+}
+
+// swagger:route GET /api/v1/user/group user getUserGroup
+//
+// Get user group.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
+func getUserGroup(w http.ResponseWriter, r *http.Request) {
+
+	gid, _ := url.QueryUnescape(r.URL.Query().Get("group_id"))
+	if gid == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("group id is required"), "badrequest"))
+		return
+	}
+	group, err := proLogic.GetUserGroup(models.UserGroupID(gid))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, models.ErrorResponse{
+			Code:    http.StatusInternalServerError,
+			Message: err.Error(),
+		})
+		return
+	}
+	logic.ReturnSuccessResponseWithJson(w, r, group, "successfully fetched user group")
+}
+
+// swagger:route POST /api/v1/user/group user createUserGroup
+//
+// Create user groups.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
+func createUserGroup(w http.ResponseWriter, r *http.Request) {
+	var userGroupReq models.CreateGroupReq
+	err := json.NewDecoder(r.Body).Decode(&userGroupReq)
+	if err != nil {
+		slog.Error("error decoding request body", "error",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	err = proLogic.ValidateCreateGroupReq(userGroupReq.Group)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	err = proLogic.CreateUserGroup(userGroupReq.Group)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	for _, userID := range userGroupReq.Members {
+		user, err := logic.GetUser(userID)
+		if err != nil {
+			continue
+		}
+		if len(user.UserGroups) == 0 {
+			user.UserGroups = make(map[models.UserGroupID]struct{})
+		}
+		user.UserGroups[userGroupReq.Group.ID] = struct{}{}
+		logic.UpsertUser(*user)
+	}
+	logic.ReturnSuccessResponseWithJson(w, r, userGroupReq.Group, "created user group")
+}
+
+// swagger:route PUT /api/v1/user/group user updateUserGroup
+//
+// Update user group.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
+func updateUserGroup(w http.ResponseWriter, r *http.Request) {
+	var userGroup models.UserGroup
+	err := json.NewDecoder(r.Body).Decode(&userGroup)
+	if err != nil {
+		slog.Error("error decoding request body", "error",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	// fetch curr group
+	currUserG, err := proLogic.GetUserGroup(userGroup.ID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	err = proLogic.ValidateUpdateGroupReq(userGroup)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	err = proLogic.UpdateUserGroup(userGroup)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	// reset configs for service user
+	go proLogic.UpdatesUserGwAccessOnGrpUpdates(currUserG.NetworkRoles, userGroup.NetworkRoles)
+	logic.ReturnSuccessResponseWithJson(w, r, userGroup, "updated user group")
+}
+
+// swagger:route DELETE /api/v1/user/group user deleteUserGroup
+//
+// delete user group.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
+//
+// @Summary     Delete user group.
+// @Router      /api/v1/user/group [delete]
+// @Tags        Users
+// @Param       group_id param string true "group id required to delete the role"
+// @Success     200 {string} string
+// @Failure     500 {object} models.ErrorResponse
+func deleteUserGroup(w http.ResponseWriter, r *http.Request) {
+
+	gid, _ := url.QueryUnescape(r.URL.Query().Get("group_id"))
+	if gid == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("role is required"), "badrequest"))
+		return
+	}
+	userG, err := proLogic.GetUserGroup(models.UserGroupID(gid))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("role is required"), "badrequest"))
+		return
+	}
+	err = proLogic.DeleteUserGroup(models.UserGroupID(gid))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	go proLogic.UpdatesUserGwAccessOnGrpUpdates(userG.NetworkRoles, make(map[models.NetworkID]map[models.UserRoleID]struct{}))
+	logic.ReturnSuccessResponseWithJson(w, r, nil, "deleted user group")
+}
+
+// @Summary     lists all user roles.
+// @Router      /api/v1/user/roles [get]
+// @Tags        Users
+// @Param       role_id param string true "roleid required to get the role details"
+// @Success     200 {object}  []models.UserRolePermissionTemplate
+// @Failure     500 {object} models.ErrorResponse
+func listRoles(w http.ResponseWriter, r *http.Request) {
+	platform, _ := url.QueryUnescape(r.URL.Query().Get("platform"))
+	var roles []models.UserRolePermissionTemplate
+	var err error
+	if platform == "true" {
+		roles, err = proLogic.ListPlatformRoles()
+	} else {
+		roles, err = proLogic.ListNetworkRoles()
+	}
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, models.ErrorResponse{
+			Code:    http.StatusInternalServerError,
+			Message: err.Error(),
+		})
+		return
+	}
+
+	logic.ReturnSuccessResponseWithJson(w, r, roles, "successfully fetched user roles permission templates")
+}
+
+// @Summary     Get user role permission template.
+// @Router      /api/v1/user/role [get]
+// @Tags        Users
+// @Param       role_id param string true "roleid required to get the role details"
+// @Success     200 {object} models.UserRolePermissionTemplate
+// @Failure     500 {object} models.ErrorResponse
+func getRole(w http.ResponseWriter, r *http.Request) {
+	rid, _ := url.QueryUnescape(r.URL.Query().Get("role_id"))
+	if rid == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("role is required"), "badrequest"))
+		return
+	}
+	role, err := logic.GetRole(models.UserRoleID(rid))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, models.ErrorResponse{
+			Code:    http.StatusInternalServerError,
+			Message: err.Error(),
+		})
+		return
+	}
+	logic.ReturnSuccessResponseWithJson(w, r, role, "successfully fetched user role permission templates")
+}
+
+// @Summary     Create user role permission template.
+// @Router      /api/v1/user/role [post]
+// @Tags        Users
+// @Param       body models.UserRolePermissionTemplate true "user role template"
+// @Success     200 {object}  models.UserRolePermissionTemplate
+// @Failure     500 {object} models.ErrorResponse
+func createRole(w http.ResponseWriter, r *http.Request) {
+	var userRole models.UserRolePermissionTemplate
+	err := json.NewDecoder(r.Body).Decode(&userRole)
+	if err != nil {
+		slog.Error("error decoding request body", "error",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	err = proLogic.ValidateCreateRoleReq(&userRole)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	userRole.Default = false
+	userRole.GlobalLevelAccess = make(map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope)
+	err = proLogic.CreateRole(userRole)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	logic.ReturnSuccessResponseWithJson(w, r, userRole, "created user role")
+}
+
+// @Summary     Update user role permission template.
+// @Router      /api/v1/user/role [put]
+// @Tags        Users
+// @Param       body models.UserRolePermissionTemplate true "user role template"
+// @Success     200 {object} userBodyResponse
+// @Failure     500 {object} models.ErrorResponse
+func updateRole(w http.ResponseWriter, r *http.Request) {
+	var userRole models.UserRolePermissionTemplate
+	err := json.NewDecoder(r.Body).Decode(&userRole)
+	if err != nil {
+		slog.Error("error decoding request body", "error",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	currRole, err := logic.GetRole(userRole.ID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	err = proLogic.ValidateUpdateRoleReq(&userRole)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	userRole.GlobalLevelAccess = make(map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope)
+	err = proLogic.UpdateRole(userRole)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	// reset configs for service user
+	go proLogic.UpdatesUserGwAccessOnRoleUpdates(currRole.NetworkLevelAccess, userRole.NetworkLevelAccess, string(userRole.NetworkID))
+	logic.ReturnSuccessResponseWithJson(w, r, userRole, "updated user role")
+}
+
+// @Summary     Delete user role permission template.
+// @Router      /api/v1/user/role [delete]
+// @Tags        Users
+// @Param       role_id param string true "roleid required to delete the role"
+// @Success     200 {string} string
+// @Failure     500 {object} models.ErrorResponse
+func deleteRole(w http.ResponseWriter, r *http.Request) {
+
+	rid, _ := url.QueryUnescape(r.URL.Query().Get("role_id"))
+	if rid == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("role is required"), "badrequest"))
+		return
+	}
+	role, err := logic.GetRole(models.UserRoleID(rid))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("role is required"), "badrequest"))
+		return
+	}
+	err = proLogic.DeleteRole(models.UserRoleID(rid), false)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	go proLogic.UpdatesUserGwAccessOnRoleUpdates(role.NetworkLevelAccess, make(map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope), role.NetworkID.String())
+	logic.ReturnSuccessResponseWithJson(w, r, nil, "deleted user role")
 }
 
 // @Summary     Attach user to a remote access gateway
@@ -72,15 +668,8 @@ func attachUserToRemoteAccessGw(w http.ResponseWriter, r *http.Request) {
 		)
 		return
 	}
-	if user.IsAdmin || user.IsSuperAdmin {
-		logic.ReturnErrorResponse(
-			w,
-			r,
-			logic.FormatError(
-				errors.New("superadmins/admins have access to all gateways"),
-				"badrequest",
-			),
-		)
+	if user.PlatformRoleID == models.AdminRole || user.PlatformRoleID == models.SuperAdminRole {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("superadmins/admins have access to all gateways"), "badrequest"))
 		return
 	}
 	node, err := logic.GetNodeByID(remoteGwID)
@@ -207,31 +796,30 @@ func removeUserFromRemoteAccessGW(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(logic.ToReturnUser(*user))
 }
 
-// @Summary     Get user's remote access gateways
+// @Summary     Get Users Remote Access Gw.
 // @Router      /api/users/{username}/remote_access_gw [get]
-// @Tags        PRO
-// @Accept      json
-// @Produce     json
-// @Param       username path string true "Username"
-// @Param       remote_access_clientid query string false "Remote Access Client ID"
-// @Param       from_mobile query boolean false "Request from mobile"
-// @Success     200 {array} models.UserRemoteGws
-// @Failure     400 {object} models.ErrorResponse
+// @Tags        Users
+// @Param       username path string true "Username to fetch all the gateways with access"
+// @Success     200 {object} map[string][]models.UserRemoteGws
 // @Failure     500 {object} models.ErrorResponse
-func getUserRemoteAccessGws(w http.ResponseWriter, r *http.Request) {
+func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 	// set header.
 	w.Header().Set("Content-Type", "application/json")
-
+	logger.Log(0, "------------> 1. getUserRemoteAccessGwsV1")
 	var params = mux.Vars(r)
 	username := params["username"]
 	if username == "" {
-		logic.ReturnErrorResponse(
-			w,
-			r,
-			logic.FormatError(errors.New("required params username"), "badrequest"),
-		)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("required params username"), "badrequest"))
 		return
 	}
+	logger.Log(0, "------------> 2. getUserRemoteAccessGwsV1")
+	user, err := logic.GetUser(username)
+	if err != nil {
+		logger.Log(0, username, "failed to fetch user: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to fetch user %s, error: %v", username, err), "badrequest"))
+		return
+	}
+	logger.Log(0, "------------> 3. getUserRemoteAccessGwsV1")
 	remoteAccessClientID := r.URL.Query().Get("remote_access_clientid")
 	var req models.UserRemoteGwsReq
 	if remoteAccessClientID == "" {
@@ -242,115 +830,32 @@ func getUserRemoteAccessGws(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 	}
+	logger.Log(0, "------------> 4. getUserRemoteAccessGwsV1")
 	reqFromMobile := r.URL.Query().Get("from_mobile") == "true"
 	if req.RemoteAccessClientID == "" && remoteAccessClientID == "" {
-		logic.ReturnErrorResponse(
-			w,
-			r,
-			logic.FormatError(errors.New("remote access client id cannot be empty"), "badrequest"),
-		)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("remote access client id cannot be empty"), "badrequest"))
 		return
 	}
 	if req.RemoteAccessClientID == "" {
 		req.RemoteAccessClientID = remoteAccessClientID
 	}
 	userGws := make(map[string][]models.UserRemoteGws)
-	user, err := logic.GetUser(username)
-	if err != nil {
-		logger.Log(0, username, "failed to fetch user: ", err.Error())
-		logic.ReturnErrorResponse(
-			w,
-			r,
-			logic.FormatError(
-				fmt.Errorf("failed to fetch user %s, error: %v", username, err),
-				"badrequest",
-			),
-		)
-		return
-	}
+	logger.Log(0, "------------> 5. getUserRemoteAccessGwsV1")
 	allextClients, err := logic.GetAllExtClients()
 	if err != nil {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-
-	processedAdminNodeIds := make(map[string]struct{})
+	logger.Log(0, "------------> 6. getUserRemoteAccessGwsV1")
+	userGwNodes := proLogic.GetUserRAGNodes(*user)
+	logger.Log(0, fmt.Sprintf("1. User Gw Nodes: %+v", userGwNodes))
 	for _, extClient := range allextClients {
-		if extClient.RemoteAccessClientID == req.RemoteAccessClientID &&
-			extClient.OwnerID == username {
-			node, err := logic.GetNodeByID(extClient.IngressGatewayID)
-			if err != nil {
-				continue
-			}
-			if node.PendingDelete {
-				continue
-			}
-			if !node.IsIngressGateway {
-				continue
-			}
-			host, err := logic.GetHost(node.HostID.String())
-			if err != nil {
-				continue
-			}
-			network, err := logic.GetNetwork(node.Network)
-			if err != nil {
-				slog.Error("failed to get node network", "error", err)
-			}
-
-			if _, ok := user.RemoteGwIDs[node.ID.String()]; (!user.IsAdmin && !user.IsSuperAdmin) &&
-				ok {
-				gws := userGws[node.Network]
-				extClient.AllowedIPs = logic.GetExtclientAllowedIPs(extClient)
-				gws = append(gws, models.UserRemoteGws{
-					GwID:              node.ID.String(),
-					GWName:            host.Name,
-					Network:           node.Network,
-					GwClient:          extClient,
-					Connected:         true,
-					IsInternetGateway: node.IsInternetGateway,
-					GwPeerPublicKey:   host.PublicKey.String(),
-					GwListenPort:      logic.GetPeerListenPort(host),
-					Metadata:          node.Metadata,
-					AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
-					NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
-				})
-				userGws[node.Network] = gws
-				delete(user.RemoteGwIDs, node.ID.String())
-			} else {
-				gws := userGws[node.Network]
-				extClient.AllowedIPs = logic.GetExtclientAllowedIPs(extClient)
-				gws = append(gws, models.UserRemoteGws{
-					GwID:              node.ID.String(),
-					GWName:            host.Name,
-					Network:           node.Network,
-					GwClient:          extClient,
-					Connected:         true,
-					IsInternetGateway: node.IsInternetGateway,
-					GwPeerPublicKey:   host.PublicKey.String(),
-					GwListenPort:      logic.GetPeerListenPort(host),
-					Metadata:          node.Metadata,
-					AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
-					NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
-				})
-				userGws[node.Network] = gws
-				processedAdminNodeIds[node.ID.String()] = struct{}{}
-			}
+		node, ok := userGwNodes[extClient.IngressGatewayID]
+		if !ok {
+			continue
 		}
-	}
+		if extClient.RemoteAccessClientID == req.RemoteAccessClientID && extClient.OwnerID == username {
 
-	// add remaining gw nodes to resp
-	if !user.IsAdmin && !user.IsSuperAdmin {
-		for gwID := range user.RemoteGwIDs {
-			node, err := logic.GetNodeByID(gwID)
-			if err != nil {
-				continue
-			}
-			if !node.IsIngressGateway {
-				continue
-			}
-			if node.PendingDelete {
-				continue
-			}
 			host, err := logic.GetHost(node.HostID.String())
 			if err != nil {
 				continue
@@ -359,12 +864,15 @@ func getUserRemoteAccessGws(w http.ResponseWriter, r *http.Request) {
 			if err != nil {
 				slog.Error("failed to get node network", "error", err)
 			}
-			gws := userGws[node.Network]
 
+			gws := userGws[node.Network]
+			extClient.AllowedIPs = logic.GetExtclientAllowedIPs(extClient)
 			gws = append(gws, models.UserRemoteGws{
 				GwID:              node.ID.String(),
 				GWName:            host.Name,
 				Network:           node.Network,
+				GwClient:          extClient,
+				Connected:         true,
 				IsInternetGateway: node.IsInternetGateway,
 				GwPeerPublicKey:   host.PublicKey.String(),
 				GwListenPort:      logic.GetPeerListenPort(host),
@@ -373,43 +881,49 @@ func getUserRemoteAccessGws(w http.ResponseWriter, r *http.Request) {
 				NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
 			})
 			userGws[node.Network] = gws
+			delete(userGwNodes, node.ID.String())
 		}
-	} else {
-		allNodes, err := logic.GetAllNodes()
+	}
+	logger.Log(0, fmt.Sprintf("2. User Gw Nodes: %+v", userGwNodes))
+	// add remaining gw nodes to resp
+	for gwID := range userGwNodes {
+		logger.Log(0, "RAG ---> 1")
+		node, err := logic.GetNodeByID(gwID)
 		if err != nil {
-			slog.Error("failed to fetch all nodes", "error", err)
-			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-			return
+			continue
 		}
-		for _, node := range allNodes {
-			_, ok := processedAdminNodeIds[node.ID.String()]
-			if node.IsIngressGateway && !node.PendingDelete && !ok {
-				host, err := logic.GetHost(node.HostID.String())
-				if err != nil {
-					slog.Error("failed to fetch host", "error", err)
-					continue
-				}
-				network, err := logic.GetNetwork(node.Network)
-				if err != nil {
-					slog.Error("failed to get node network", "error", err)
-				}
-				gws := userGws[node.Network]
-
-				gws = append(gws, models.UserRemoteGws{
-					GwID:              node.ID.String(),
-					GWName:            host.Name,
-					Network:           node.Network,
-					IsInternetGateway: node.IsInternetGateway,
-					GwPeerPublicKey:   host.PublicKey.String(),
-					GwListenPort:      logic.GetPeerListenPort(host),
-					Metadata:          node.Metadata,
-					AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
-					NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
-				})
-				userGws[node.Network] = gws
-			}
+		if !node.IsIngressGateway {
+			continue
+		}
+		if node.PendingDelete {
+			continue
+		}
+		logger.Log(0, "RAG ---> 2")
+		host, err := logic.GetHost(node.HostID.String())
+		if err != nil {
+			continue
+		}
+		network, err := logic.GetNetwork(node.Network)
+		if err != nil {
+			slog.Error("failed to get node network", "error", err)
 		}
+		logger.Log(0, "RAG ---> 3")
+		gws := userGws[node.Network]
+
+		gws = append(gws, models.UserRemoteGws{
+			GwID:              node.ID.String(),
+			GWName:            host.Name,
+			Network:           node.Network,
+			IsInternetGateway: node.IsInternetGateway,
+			GwPeerPublicKey:   host.PublicKey.String(),
+			GwListenPort:      logic.GetPeerListenPort(host),
+			Metadata:          node.Metadata,
+			AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
+			NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
+		})
+		userGws[node.Network] = gws
 	}
+
 	if reqFromMobile {
 		// send resp in array format
 		userGwsArr := []models.UserRemoteGws{}
@@ -478,3 +992,114 @@ func getAllowedRagEndpoints(ragNode *models.Node, ragHost *models.Host) []string
 	}
 	return endpoints
 }
+
+// @Summary     Get all pending users
+// @Router      /api/users_pending [get]
+// @Tags        Users
+// @Success     200 {array} models.User
+// @Failure     500 {object} models.ErrorResponse
+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)
+}
+
+// @Summary     Approve a pending user
+// @Router      /api/users_pending/user/{username} [post]
+// @Tags        Users
+// @Param       username path string true "Username of the pending user to approve"
+// @Success     200 {string} string
+// @Failure     500 {object} models.ErrorResponse
+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 = logic.FetchPassValue("")
+			if fetchErr != nil {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(fetchErr, "internal"))
+				return
+			}
+			if err = logic.CreateUser(&models.User{
+				UserName:       user.UserName,
+				Password:       newPass,
+				PlatformRoleID: models.ServiceUser,
+			}); 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)
+}
+
+// @Summary     Delete a pending user
+// @Router      /api/users_pending/user/{username} [delete]
+// @Tags        Users
+// @Param       username path string true "Username of the pending user to delete"
+// @Success     200 {string} string
+// @Failure     500 {object} models.ErrorResponse
+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)
+}
+
+// @Summary     Delete all pending users
+// @Router      /api/users_pending [delete]
+// @Tags        Users
+// @Success     200 {string} string
+// @Failure     500 {object} models.ErrorResponse
+func deleteAllPendingUsers(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	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")
+}

+ 53 - 0
pro/email/email.go

@@ -0,0 +1,53 @@
+package email
+
+import (
+	"context"
+
+	"github.com/gravitl/netmaker/servercfg"
+)
+
+type EmailSenderType string
+
+var client EmailSender
+
+const (
+	Smtp   EmailSenderType = "smtp"
+	Resend EmailSenderType = "resend"
+)
+
+func init() {
+	switch EmailSenderType(servercfg.EmailSenderType()) {
+	case Smtp:
+		client = &SmtpSender{
+			SmtpHost:    servercfg.GetSmtpHost(),
+			SmtpPort:    servercfg.GetSmtpPort(),
+			SenderEmail: servercfg.GetSenderEmail(),
+			SenderPass:  servercfg.GetEmaiSenderAuth(),
+		}
+	case Resend:
+		client = NewResendEmailSenderFromConfig()
+	}
+	client = GetClient()
+}
+
+// EmailSender - an interface for sending emails based on notifications and mail templates
+type EmailSender interface {
+	// SendEmail - sends an email based on a context, notification and mail template
+	SendEmail(ctx context.Context, notification Notification, email Mail) error
+}
+
+type Mail interface {
+	GetBody(info Notification) string
+	GetSubject(info Notification) string
+}
+
+// Notification - struct for notification details
+type Notification struct {
+	RecipientMail string
+	RecipientName string
+	ProductName   string
+}
+
+func GetClient() (e EmailSender) {
+	return client
+}

+ 27 - 0
pro/email/invite.go

@@ -0,0 +1,27 @@
+package email
+
+import (
+	"fmt"
+)
+
+// UserInvitedMail - mail for users that are invited to a tenant
+type UserInvitedMail struct {
+	BodyBuilder EmailBodyBuilder
+	InviteURL   string
+}
+
+// GetSubject - gets the subject of the email
+func (UserInvitedMail) GetSubject(info Notification) string {
+	return "Netmaker: Pending Invitation"
+}
+
+// GetBody - gets the body of the email
+func (invite UserInvitedMail) GetBody(info Notification) string {
+
+	return invite.BodyBuilder.
+		WithHeadline("Join Netmaker from this invite!").
+		WithParagraph("Hello from Netmaker,").
+		WithParagraph("You have been invited to join Netmaker.").
+		WithParagraph(fmt.Sprintf("Join Using This Invite Link <a href=\"%s\">Netmaker</a>", invite.InviteURL)).
+		Build()
+}

+ 55 - 0
pro/email/resend.go

@@ -0,0 +1,55 @@
+package email
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/gravitl/netmaker/servercfg"
+	"github.com/resendlabs/resend-go"
+)
+
+// ResendEmailSender - implementation of EmailSender using Resend (https://resend.com)
+type ResendEmailSender struct {
+	client ResendClient
+	from   string
+}
+
+// ResendClient - dependency interface for resend client
+type ResendClient interface {
+	Send(*resend.SendEmailRequest) (resend.SendEmailResponse, error)
+}
+
+// NewResendEmailSender - constructs a ResendEmailSender
+func NewResendEmailSender(client ResendClient, from string) ResendEmailSender {
+	return ResendEmailSender{client: client, from: from}
+}
+
+// NewResendEmailSender - constructs a ResendEmailSender from config
+// TODO let main.go handle this and use dependency injection instead of calling this function
+func NewResendEmailSenderFromConfig() ResendEmailSender {
+	key, from := servercfg.GetEmaiSenderAuth(), servercfg.GetSenderEmail()
+	resender := resend.NewClient(key)
+	return NewResendEmailSender(resender.Emails, from)
+}
+
+// SendEmail - sends an email using resend-go (https://github.com/resendlabs/resend-go)
+func (es ResendEmailSender) SendEmail(ctx context.Context, notification Notification, email Mail) error {
+	var (
+		from    = es.from
+		to      = notification.RecipientMail
+		subject = email.GetSubject(notification)
+		body    = email.GetBody(notification)
+	)
+	params := resend.SendEmailRequest{
+		From:    from,
+		To:      []string{to},
+		Subject: subject,
+		Html:    body,
+	}
+	_, err := es.client.Send(&params)
+	if err != nil {
+		return fmt.Errorf("failed sending mail via resend: %w", err)
+	}
+
+	return nil
+}

+ 42 - 0
pro/email/smtp.go

@@ -0,0 +1,42 @@
+package email
+
+import (
+	"context"
+	"crypto/tls"
+
+	gomail "gopkg.in/mail.v2"
+)
+
+type SmtpSender struct {
+	SmtpHost    string
+	SmtpPort    int
+	SenderEmail string
+	SenderPass  string
+}
+
+func (s *SmtpSender) SendEmail(ctx context.Context, n Notification, e Mail) error {
+	m := gomail.NewMessage()
+
+	// Set E-Mail sender
+	m.SetHeader("From", s.SenderEmail)
+
+	// Set E-Mail receivers
+	m.SetHeader("To", n.RecipientMail)
+	// Set E-Mail subject
+	m.SetHeader("Subject", e.GetSubject(n))
+	// Set E-Mail body. You can set plain text or html with text/html
+	m.SetBody("text/html", e.GetBody(n))
+	// Settings for SMTP server
+	d := gomail.NewDialer(s.SmtpHost, s.SmtpPort, s.SenderEmail, s.SenderPass)
+
+	// This is only needed when SSL/TLS certificate is not valid on server.
+	// In production this should be set to false.
+	d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
+
+	// Now send E-Mail
+	if err := d.DialAndSend(m); err != nil {
+		return err
+	}
+
+	return nil
+}

+ 567 - 0
pro/email/utils.go

@@ -0,0 +1,567 @@
+package email
+
+import "strings"
+
+// mail related images hosted on github
+var (
+	nLogoTeal        = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/N_Teal.png"
+	netmakerLogoTeal = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/netmaker-logo-2.png"
+	netmakerMeshLogo = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/netmaker-mesh.png"
+	linkedinIcon     = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/linkedin2x.png"
+	discordIcon      = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/discord-logo-png-7617.png"
+	githubIcon       = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/Octocat.png"
+	mailIcon         = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/icons8-mail-24.png"
+	addressIcon      = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/icons8-address-16.png"
+	linkIcon         = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/icons8-hyperlink-64.png"
+)
+
+type EmailBodyBuilder interface {
+	WithHeadline(text string) EmailBodyBuilder
+	WithParagraph(text string) EmailBodyBuilder
+	WithSignature() EmailBodyBuilder
+	Build() string
+}
+
+type EmailBodyBuilderWithH1HeadlineAndImage struct {
+	headline     string
+	paragraphs   []string
+	hasSignature bool
+}
+
+func (b *EmailBodyBuilderWithH1HeadlineAndImage) WithHeadline(text string) EmailBodyBuilder {
+	b.headline = text
+	return b
+}
+
+func (b *EmailBodyBuilderWithH1HeadlineAndImage) WithParagraph(text string) EmailBodyBuilder {
+	b.paragraphs = append(b.paragraphs, text)
+	return b
+}
+
+func (b *EmailBodyBuilderWithH1HeadlineAndImage) WithSignature() EmailBodyBuilder {
+	b.hasSignature = true
+	return b
+}
+
+func (b *EmailBodyBuilderWithH1HeadlineAndImage) Build() string {
+	// map paragraphs to styled paragraphs
+	styledParagraphsSlice := make([]string, len(b.paragraphs))
+	for i, paragraph := range b.paragraphs {
+		styledParagraphsSlice[i] = styledParagraph(paragraph)
+	}
+	// join styled paragraphs
+	styledParagraphsString := strings.Join(styledParagraphsSlice, "")
+
+	signature := ""
+	if b.hasSignature {
+		signature = styledSignature()
+	}
+
+	return `
+		<!DOCTYPE html>
+		<html xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" lang="en">
+		<head>
+		    <title></title>
+		    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+		    <meta name="viewport" content="width=device-width,initial-scale=1">
+		    <!--[if mso]>
+		    <xml>
+		        <o:OfficeDocumentSettings>
+		            <o:PixelsPerInch>96</o:PixelsPerInch>
+		            <o:AllowPNG/>
+		        </o:OfficeDocumentSettings>
+		    </xml>
+		    <![endif]-->
+		    <style>
+		        *{box-sizing:border-box}body{margin:0;padding:0}a[x-apple-data-detectors]{color:inherit!important;text-decoration:inherit!important}#MessageViewBody a{color:inherit;text-decoration:none}p{line-height:inherit}.desktop_hide,.desktop_hide table{mso-hide:all;display:none;max-height:0;overflow:hidden}@media (max-width:720px){.desktop_hide table.icons-inner{display:inline-block!important}.icons-inner{text-align:center}.icons-inner td{margin:0 auto}.image_block img.big,.row-content{width:100%!important}.mobile_hide{display:none}.stack .column{width:100%;display:block}.mobile_hide{min-height:0;max-height:0;max-width:0;overflow:hidden;font-size:0}.desktop_hide,.desktop_hide table{display:table!important;max-height:none!important}}
+		    </style>
+		</head>
+		<body style="background-color:transparent;margin:0;padding:0;-webkit-text-size-adjust:none;text-size-adjust:none">
+		<table class="nl-container" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;background-color:transparent">
+		    <tbody>
+		    <tr>
+		        <td>
+		            <table class="row row-1" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                <tbody>
+		                <tr>
+		                    <td>
+		                        <table class="row-content" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
+		                            <tbody>
+		                            <tr>
+		                                <td class="column column-1" width="50%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;border-top:0;border-right:0;border-bottom:0;border-left:0">
+		                                    <table class="image_block block-2" width="100%" border="0" cellpadding="0" cellspacing="0"
+		                                           role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad" style="padding-left:15px;padding-right:15px;width:100%;padding-top:5px">
+		                                                <div class="alignment" align="left" style="line-height:10px"><a href="https://www.netmaker.io/" target="_blank" style="outline:none" tabindex="-1"><img class="big" src="` + netmakerLogoTeal + `"
+		                                                                                                                                                                                                        style="display:block;height:auto;border:0;width:333px;max-width:100%" width="333" alt="Netmaker" title="Netmaker"></a></div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                    <table class="divider_block block-3" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad" style="padding-bottom:10px;padding-left:5px;padding-right:5px;padding-top:10px">
+		                                                <div class="alignment" align="center">
+		                                                    <table border="0" cellpadding="0" cellspacing="0"
+		                                                           role="presentation" width="100%" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                                        <tr>
+		                                                            <td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
+		                                                        </tr>
+		                                                    </table>
+		                                                </div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                </td>
+		                                <td class="column column-2" width="50%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;border-top:0;border-right:0;border-bottom:0;border-left:0">
+		                                    <table class="empty_block block-2" width="100%" border="0"
+		                                           cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad" style="padding-right:0;padding-bottom:5px;padding-left:0;padding-top:5px">
+		                                                <div></div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                </td>
+		                            </tr>
+		                            </tbody>
+		                        </table>
+		                    </td>
+		                </tr>
+		                </tbody>
+		            </table>
+		            <table class="row row-2" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                <tbody>
+		                <tr>
+		                    <td>
+		                        <table class="row-content stack" align="center"
+		                               border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
+		                            <tbody>
+		                            <tr>
+		                                <td class="column column-1" width="100%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;padding-left:10px;padding-right:10px;vertical-align:top;padding-top:10px;padding-bottom:10px;border-top:0;border-right:0;border-bottom:0;border-left:0">
+		                                    <table class="divider_block block-1" width="100%" border="0"
+		                                           cellpadding="10" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad">
+		                                                <div class="alignment" align="center">
+		                                                    <table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                                        <tr>
+		                                                            <td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
+		                                                        </tr>
+		                                                    </table>
+		                                                </div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                </td>
+		                            </tr>
+		                            </tbody>
+		                        </table>
+		                    </td>
+		                </tr>
+		                </tbody>
+		            </table>
+		            <table
+		                    class="row row-3" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                <tbody>
+		                <tr>
+		                    <td>
+		                        <table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
+		                            <tbody>
+		                            <tr>
+		                                <td class="column column-1" width="50%"
+		                                    style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;border-top:0;border-right:0;border-bottom:0;border-left:0">
+		                                    <table class="divider_block block-2" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad" style="padding-bottom:20px;padding-left:20px;padding-right:20px;padding-top:25px">
+		                                                <div class="alignment" align="center">
+		                                                    <table border="0" cellpadding="0"
+		                                                           cellspacing="0" role="presentation" width="100%" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                                        <tr>
+		                                                            <td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
+		                                                        </tr>
+		                                                    </table>
+		                                                </div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                    <table class="heading_block block-3" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad"
+		                                                style="padding-bottom:15px;padding-left:10px;padding-right:10px;padding-top:10px;text-align:center;width:100%">
+		                                                <h1 style="margin:0;color:#2b2d2d;direction:ltr;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:28px;font-weight:400;letter-spacing:normal;line-height:120%;text-align:left;margin-top:0;margin-bottom:0"><strong>` + b.headline + `</strong></h1>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                </td>
+		                                <td class="column column-2" width="50%"
+		                                    style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;border-top:0;border-right:0;border-bottom:0;border-left:0">
+		                                    <table class="image_block block-2" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad" style="width:100%;padding-right:0;padding-left:0;padding-top:5px;padding-bottom:5px">
+		                                                <div class="alignment" align="center" style="line-height:10px"><img
+		                                                        src="` + netmakerMeshLogo + `" style="display:block;height:auto;border:0;width:350px;max-width:100%" width="350" alt="Netmaker Mesh"></div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                </td>
+		                            </tr>
+		                            </tbody>
+		                        </table>
+		                    </td>
+		                </tr>
+		                </tbody>
+		            </table>
+		            <table class="row row-4" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
+		                   style="mso-table-lspace:0;mso-table-rspace:0">
+		                <tbody>
+		                <tr>
+		                    <td>
+		                        <table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
+		                            <tbody>
+		                            <tr>
+		                                <td class="column column-1" width="100%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:5px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0">
+		                                    <table class="divider_block block-1" width="100%" border="0" cellpadding="10" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad">
+		                                                <div class="alignment" align="center">
+		                                                    <table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                                        <tr>
+		                                                            <td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
+		                                                        </tr>
+		                                                    </table>
+		                                                </div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                </td>
+		                            </tr>
+		                            </tbody>
+		                        </table>
+		                    </td>
+		                </tr>
+		                </tbody>
+		            </table>
+		            <table class="row row-5" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                <tbody>
+		                <tr>
+		                    <td>
+		                        <table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
+		                               style="mso-table-lspace:0;mso-table-rspace:0;background-color:#0098a5;color:#000;border-top:2px solid transparent;border-right:2px solid transparent;border-left:2px solid transparent;border-bottom:2px solid transparent;border-radius:0;width:700px" width="700">
+		                            <tbody>
+		                            <tr>
+		                                <td class="column column-1" width="100%"
+		                                    style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;border-bottom:0 solid #000;border-left:0 solid #000;border-right:0 solid #000;border-top:0 solid #000;vertical-align:top;padding-top:25px;padding-bottom:25px">
+		                                    <table class="text_block block-3" width="100%" border="0"
+		                                           cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;word-break:break-word">
+		                                        <tr>
+		                                            <td class="pad" style="padding-bottom:10px;padding-left:50px;padding-right:50px;padding-top:10px">
+		                                                <div style="font-family:Verdana,sans-serif">
+		                                                    <div class="txtTinyMce-wrapper" style="font-size:12px;mso-line-height-alt:18px;color:#393d47;line-height:1.5;font-family:Verdana,Geneva,sans-serif">
+		
+		                                                        <p style="margin:0;font-size:12px;mso-line-height-alt:18px">&nbsp;</p>
+		                                                        ` + styledParagraphsString + `
+		                                                        <p style="margin:0;mso-line-height-alt:18px">&nbsp;</p>
+		                                                    </div>
+		                                                </div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                </td>
+		                            </tr>
+		                            </tbody>
+		                        </table>
+		                    </td>
+		                </tr>
+		                </tbody>
+		            </table>
+		            <table class="row row-6" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                <tbody>
+		                <tr>
+		                    <td>
+		                        <table
+		                                class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
+		                            <tbody>
+		                            <tr>
+		                                <td class="column column-1" width="100%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:5px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0">
+		                                    <table class="divider_block block-1" width="100%" border="0"
+		                                           cellpadding="10" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad">
+		                                                <div class="alignment" align="center">
+		                                                    <table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                                        <tr>
+		                                                            <td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
+		                                                        </tr>
+		                                                    </table>
+		                                                </div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                </td>
+		                            </tr>
+		                            </tbody>
+		                        </table>
+		                    </td>
+		                </tr>
+		                </tbody>
+		            </table>
+		            <table
+		                    class="row row-7" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;background-color:#f7fafe">
+		                <tbody>
+		                <tr>
+		                    <td>
+		                        <table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
+		                            <tbody>
+		                            <tr>
+		                                <td class="column column-1" width="100%"
+		                                    style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:25px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0">
+		                                    <table class="divider_block block-1" width="100%" border="0" cellpadding="10" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad">
+		                                                <div class="alignment" align="center">
+		                                                    <table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%"
+		                                                           style="mso-table-lspace:0;mso-table-rspace:0">
+		                                                        <tr>
+		                                                            <td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
+		                                                        </tr>
+		                                                    </table>
+		                                                </div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                </td>
+		                            </tr>
+		                            </tbody>
+		                        </table>
+		                    </td>
+		                </tr>
+		                </tbody>
+		            </table>
+		            <table class="row row-8" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;background-color:#090660">
+		                <tbody>
+		                <tr>
+		                    <td>
+		                        <table class="row-content stack"
+		                               align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
+		                            <tbody>
+		                            <tr>
+		                                <td class="column column-1" width="100%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:5px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0">
+		                                    <table class="text_block block-1" width="100%" border="0" cellpadding="0" cellspacing="0"
+		                                           role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;word-break:break-word">
+		                                        <tr>
+		                                            <td class="pad" style="padding-bottom:10px;padding-left:50px;padding-right:50px;padding-top:10px">
+		                                                <div style="font-family:sans-serif">
+		                                                    <div class="txtTinyMce-wrapper" style="font-size:12px;mso-line-height-alt:18px;color:#6f7077;line-height:1.5;font-family:Arial,Helvetica Neue,Helvetica,sans-serif">
+		                                                        <p style="margin:0;font-size:12px;mso-line-height-alt:33px">
+		                                                            <span style="color:#ffffff;font-size:22px;">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;Get In Touch With Us</span>
+		                                                        </p>
+		                                                    </div>
+		                                                </div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                    <table class="social_block block-2" width="100%" border="0" cellpadding="10" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad">
+		                                                <div class="alignment" style="text-align:center">
+		                                                    <table class="social-table"
+		                                                           width="114.49624060150376px" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;display:inline-block">
+		                                                        <tr>
+		                                                            <td style="padding:0 2px 0 2px"><a href="https://www.linkedin.com/company/netmaker-inc/" target="_blank"><img src="` + linkedinIcon + `" width="32" height="32" alt="Linkedin" title="linkedin" style="display:block;height:auto;border:0"></a></td>
+		                                                            <td
+		                                                                    style="padding:0 2px 0 2px"><a href="https://discord.gg/zRb9Vfhk8A" target="_blank"><img src="` + discordIcon + `" width="32" height="32" alt="Discord" title="Discord" style="display:block;height:auto;border:0"></a></td>
+		                                                            <td style="padding:0 2px 0 2px"><a href="https://github.com/gravitl/netmaker" target="_blank"><img
+		                                                                    src="` + githubIcon + `" width="38.49624060150376" height="32" alt="Github" title="Github" style="display:block;height:auto;border:0"></a></td>
+		                                                        </tr>
+		                                                    </table>
+		                                                </div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                </td>
+		                            </tr>
+		                            </tbody>
+		                        </table>
+		                    </td>
+		                </tr>
+		                </tbody>
+		            </table>
+		            <table class="row row-9" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                <tbody>
+		                <tr>
+		                    <td>
+		                        <table
+		                                class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
+		                            <tbody>
+		                            <tr>
+		                                <td class="column column-1" width="100%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:5px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0">
+		                                    <table class="icons_block block-1" width="100%" border="0"
+		                                           cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad" style="vertical-align:middle;padding-bottom:5px;padding-top:5px;text-align:center;color:#9d9d9d;font-family:inherit;font-size:15px">
+		                                                <table width="100%" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                                    <tr>
+		                                                        <td class="alignment" style="vertical-align:middle;text-align:center">
+		                                                            <!--[if vml]>
+		                                                            <table align="left" cellpadding="0" cellspacing="0" role="presentation" style="display:inline-block;padding-left:0px;padding-right:0px;mso-table-lspace: 0pt;mso-table-rspace: 0pt;">
+		                                                            <![endif]--><!--[if !vml]><!-->
+		                                                            <table class="icons-inner" style="mso-table-lspace:0;mso-table-rspace:0;display:inline-block;margin-right:-4px;padding-left:0;padding-right:0" cellpadding="0" cellspacing="0" role="presentation">
+		                                                                <!--<![endif]-->
+		                                                            </table>
+		                                                        </td></tr>
+		                                                </table>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                </td>
+		                            </tr>
+		                            </tbody>
+		                        </table>
+		                    </td>
+		                </tr>
+		                </tbody>
+		            </table>
+		        </td>
+		    </tr>
+		    </tbody>
+		</table>
+		<!-- End -->
+		</body>
+		` + signature + `
+		</html>`
+}
+
+func styledSignature() string {
+	return `
+	<footer style="display:block">
+	<table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
+	<tbody>
+	   <tr>
+		  <td>
+			 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
+				<tbody>
+				   <tr>
+					  <td style="vertical-align: top;">
+						 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
+							<tbody>
+							   <tr>
+								  <td class="sc-TOsTZ kjYrri" style="text-align: center;"><img src="` + nLogoTeal + `" role="presentation" width="130" class="sc-cHGsZl bHiaRe" style="max-width: 130px; display: block;"></td>
+							   </tr>
+							   <tr>
+								  <td height="30"></td>
+							   </tr>
+							   <tr>
+								  <td style="text-align: center;">
+									 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial; display: inline-block;">
+										<tbody>
+										   <tr style="text-align: center;">
+											  <td><a href="https://www.linkedin.com/company/netmaker-inc/" color="#6a78d1" class="sc-hzDkRC kpsoyz" style="display: inline-block; padding: 0px; background-color: rgb(106, 120, 209);"><img src="` + linkedinIcon + `" alt="Linkedin" color="#6a78d1" height="24" class="sc-bRBYWo ccSRck" style="background-color: rgb(106, 120, 209); max-width: 135px; display: block;"></a></td>
+											  <td width="5">
+												 <div></div>
+											  </td>
+										 
+                                    <td><a href="https://discord.gg/zRb9Vfhk8A" class="sc-hzDkRC kpsoyz" style="display: inline-block; padding: 0px;"><img src="` + discordIcon + `" alt="Discord" height="24" class="sc-bRBYWo ccSRck" style="max-width: 135px; display: block;"></a></td>
+                                    <td width="5">
+                                    <div></div>
+                                    </td>
+                              
+                                    <td><a href="https://github.com/gravitl/netmaker" class="sc-hzDkRC kpsoyz" style="display: inline-block; padding: 0px;"><img src="` + githubIcon + `" alt="Github" height="24" class="sc-bRBYWo ccSRck" style="max-width: 135px; display: block;"></a></td>
+                                    <td width="5">
+                                    <div></div>
+                                    </td>
+                                 </tr>
+										</tbody>
+									 </table>
+								  </td>
+							   </tr>
+							</tbody>
+						 </table>
+					  </td>
+					  <td width="46">
+						 <div></div>
+					  </td>
+					  <td style="padding: 0px; vertical-align: middle;">
+						 <h3 color="#000000" class="sc-fBuWsC eeihxG" style="margin: 0px; font-size: 18px; color: rgb(0, 0, 0);"><span>Alex</span><span>&nbsp;</span><span>Feiszli</span></h3>
+						 <p color="#000000" font-size="medium" class="sc-fMiknA bxZCMx" style="margin: 0px; color: rgb(0, 0, 0); font-size: 14px; line-height: 22px;"><span>Co-Founder &amp; CEO</span></p>
+						 <p color="#000000" font-size="medium" class="sc-dVhcbM fghLuF" style="margin: 0px; font-weight: 500; color: rgb(0, 0, 0); font-size: 14px; line-height: 22px;"><span>Netmaker</span></p>
+						 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial; width: 100%;">
+							<tbody>
+							   <tr>
+								  <td height="30"></td>
+							   </tr>
+							   <tr>
+								  <td color="#545af2" direction="horizontal" height="1" class="sc-jhAzac hmXDXQ" style="width: 100%; border-bottom: 1px solid rgb(84, 90, 242); border-left: none; display: block;"></td>
+							   </tr>
+							   <tr>
+								  <td height="30"></td>
+							   </tr>
+							</tbody>
+						 </table>
+						 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
+							<tbody>
+							   <tr height="25" style="vertical-align: middle;">
+								  <td width="30" style="vertical-align: middle;">
+									 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
+										<tbody>
+										   <tr>
+											  <td style="vertical-align: bottom;"><span width="11" class="sc-jlyJG bbyJzT" style="display: block"><img src="` + mailIcon + `" width="13" class="sc-iRbamj blSEcj" style="display: block;"></span></td>
+										   </tr>
+										</tbody>
+									 </table>
+								  </td>
+								  <td style="padding: 0px;"><a href="mailto:[email protected]" color="#000000" class="sc-gipzik iyhjGb" style="text-decoration: none; color: rgb(0, 0, 0); font-size: 12px;"><span>[email protected]</span></a></td>
+							   </tr>
+							   <tr height="25" style="vertical-align: middle;">
+								  <td width="30" style="vertical-align: middle;">
+									 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
+										<tbody>
+										   <tr>
+											  <td style="vertical-align: bottom;"><span width="11" class="sc-jlyJG bbyJzT" style="display: block;"><img src="` + linkIcon + `" color="#545af2" width="13" class="sc-iRbamj blSEcj" style="display: block;"></span></td>
+										   </tr>
+										</tbody>
+									 </table>
+								  </td>
+								  <td style="padding: 0px;"><a href="https://www.netmaker.io/" color="#000000" class="sc-gipzik iyhjGb" style="text-decoration: none; color: rgb(0, 0, 0); font-size: 12px;"><span>https://www.netmaker.io/</span></a></td>
+							   </tr>
+							   <tr height="25" style="vertical-align: middle;">
+								  <td width="30" style="vertical-align: middle;">
+									 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
+										<tbody>
+										   <tr>
+											  <td style="vertical-align: bottom;"><span width="11" class="sc-jlyJG bbyJzT" style="display: block;"><img src="` + addressIcon + `"  width="13" class="sc-iRbamj blSEcj" style="display: block;"></span></td>
+										   </tr>
+										</tbody>
+									 </table>
+								  </td>
+								  <td style="padding: 0px;"><span color="#000000" class="sc-csuQGl CQhxV" style="font-size: 12px; color: rgb(0, 0, 0);"><span>1465 Sand Hill Rd.Suite 2014, Candler, NC 28715</span></span></td>
+							   </tr>
+							</tbody>
+						 </table>
+						 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
+							<tbody>
+							   <tr>
+								  <td height="30"></td>
+							   </tr>
+							</tbody>
+						 </table>
+					  </td>
+				   </tr>
+				</tbody>
+			 </table>
+		  </td>
+	   </tr>
+	</tbody>
+ </table>
+</footer>`
+}
+
+func styledParagraph(text string) string {
+	return `<p style="margin:0;mso-line-height-alt:22.5px">
+	<span style="color:#ffffff;font-size:15px;">` + text + `</span>
+	</p>`
+}
+
+func GetMailSignature() string {
+	return styledSignature()
+}

+ 13 - 0
pro/initialize.go

@@ -119,6 +119,19 @@ func InitPro() {
 	logic.GetAllowedIpForInetNodeClient = proLogic.GetAllowedIpForInetNodeClient
 	mq.UpdateMetrics = proLogic.MQUpdateMetrics
 	mq.UpdateMetricsFallBack = proLogic.MQUpdateMetricsFallBack
+	logic.GetFilteredNodesByUserAccess = proLogic.GetFilteredNodesByUserAccess
+	logic.CreateRole = proLogic.CreateRole
+	logic.UpdateRole = proLogic.UpdateRole
+	logic.DeleteRole = proLogic.DeleteRole
+	logic.NetworkPermissionsCheck = proLogic.NetworkPermissionsCheck
+	logic.GlobalPermissionsCheck = proLogic.GlobalPermissionsCheck
+	logic.DeleteNetworkRoles = proLogic.DeleteNetworkRoles
+	logic.CreateDefaultNetworkRolesAndGroups = proLogic.CreateDefaultNetworkRolesAndGroups
+	logic.FilterNetworksByRole = proLogic.FilterNetworksByRole
+	logic.IsGroupsValid = proLogic.IsGroupsValid
+	logic.IsNetworkRolesValid = proLogic.IsNetworkRolesValid
+	logic.InitialiseRoles = proLogic.UserRolesInit
+	logic.UpdateUserGwAccess = proLogic.UpdateUserGwAccess
 }
 
 func retrieveProLogo() string {

+ 195 - 0
pro/logic/security.go

@@ -0,0 +1,195 @@
+package logic
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/models"
+)
+
+func NetworkPermissionsCheck(username string, r *http.Request) error {
+	// at this point global checks should be completed
+	user, err := logic.GetUser(username)
+	if err != nil {
+		return err
+	}
+	logger.Log(0, "NET MIDDL----> 1")
+	userRole, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		return errors.New("access denied")
+	}
+	if userRole.FullAccess {
+		return nil
+	}
+	logger.Log(0, "NET MIDDL----> 2")
+	// get info from header to determine the target rsrc
+	targetRsrc := r.Header.Get("TARGET_RSRC")
+	targetRsrcID := r.Header.Get("TARGET_RSRC_ID")
+	netID := r.Header.Get("NET_ID")
+	if targetRsrc == "" {
+		return errors.New("target rsrc is missing")
+	}
+	if netID == "" {
+		return errors.New("network id is missing")
+	}
+	if r.Method == "" {
+		r.Method = http.MethodGet
+	}
+	if targetRsrc == models.MetricRsrc.String() {
+		return nil
+	}
+
+	// check if user has scope for target resource
+	// TODO - differentitate between global scope and network scope apis
+	// check for global network role
+	if netRoles, ok := user.NetworkRoles[models.AllNetworks]; ok {
+		for netRoleID := range netRoles {
+			err = checkNetworkAccessPermissions(netRoleID, username, r.Method, targetRsrc, targetRsrcID, netID)
+			if err == nil {
+				return nil
+			}
+		}
+	}
+	netRoles := user.NetworkRoles[models.NetworkID(netID)]
+	for netRoleID := range netRoles {
+		err = checkNetworkAccessPermissions(netRoleID, username, r.Method, targetRsrc, targetRsrcID, netID)
+		if err == nil {
+			return nil
+		}
+	}
+	for groupID := range user.UserGroups {
+		userG, err := GetUserGroup(groupID)
+		if err == nil {
+			netRoles := userG.NetworkRoles[models.NetworkID(netID)]
+			for netRoleID := range netRoles {
+				err = checkNetworkAccessPermissions(netRoleID, username, r.Method, targetRsrc, targetRsrcID, netID)
+				if err == nil {
+					return nil
+				}
+			}
+		}
+	}
+
+	return errors.New("access denied")
+}
+
+func checkNetworkAccessPermissions(netRoleID models.UserRoleID, username, reqScope, targetRsrc, targetRsrcID, netID string) error {
+	networkPermissionScope, err := logic.GetRole(netRoleID)
+	if err != nil {
+		return err
+	}
+	logger.Log(0, "NET MIDDL----> 3", string(netRoleID))
+	if networkPermissionScope.FullAccess {
+		return nil
+	}
+	rsrcPermissionScope, ok := networkPermissionScope.NetworkLevelAccess[models.RsrcType(targetRsrc)]
+	if targetRsrc == models.HostRsrc.String() && !ok {
+		rsrcPermissionScope, ok = networkPermissionScope.NetworkLevelAccess[models.RemoteAccessGwRsrc]
+	}
+	if !ok {
+		return errors.New("access denied")
+	}
+	logger.Log(0, "NET MIDDL----> 4", string(netRoleID))
+	if allRsrcsTypePermissionScope, ok := rsrcPermissionScope[models.RsrcID(fmt.Sprintf("all_%s", targetRsrc))]; ok {
+		// handle extclient apis here
+		if models.RsrcType(targetRsrc) == models.ExtClientsRsrc && allRsrcsTypePermissionScope.SelfOnly && targetRsrcID != "" {
+			extclient, err := logic.GetExtClient(targetRsrcID, netID)
+			if err != nil {
+				return err
+			}
+			if !logic.IsUserAllowedAccessToExtClient(username, extclient) {
+				return errors.New("access denied")
+			}
+		}
+		err = checkPermissionScopeWithReqMethod(allRsrcsTypePermissionScope, reqScope)
+		if err == nil {
+			return nil
+		}
+
+	}
+	if targetRsrc == models.HostRsrc.String() {
+		if allRsrcsTypePermissionScope, ok := rsrcPermissionScope[models.RsrcID(fmt.Sprintf("all_%s", models.RemoteAccessGwRsrc))]; ok {
+			err = checkPermissionScopeWithReqMethod(allRsrcsTypePermissionScope, reqScope)
+			if err == nil {
+				return nil
+			}
+		}
+	}
+	logger.Log(0, "NET MIDDL----> 5", string(netRoleID))
+	if targetRsrcID == "" {
+		return errors.New("target rsrc id is empty")
+	}
+	if scope, ok := rsrcPermissionScope[models.RsrcID(targetRsrcID)]; ok {
+		err = checkPermissionScopeWithReqMethod(scope, reqScope)
+		if err == nil {
+			return nil
+		}
+	}
+	logger.Log(0, "NET MIDDL----> 6", string(netRoleID))
+	return errors.New("access denied")
+}
+
+func GlobalPermissionsCheck(username string, r *http.Request) error {
+	user, err := logic.GetUser(username)
+	if err != nil {
+		return err
+	}
+	userRole, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		return errors.New("access denied")
+	}
+	if userRole.FullAccess {
+		return nil
+	}
+	targetRsrc := r.Header.Get("TARGET_RSRC")
+	targetRsrcID := r.Header.Get("TARGET_RSRC_ID")
+	if targetRsrc == "" {
+		return errors.New("target rsrc is missing")
+	}
+	if r.Method == "" {
+		r.Method = http.MethodGet
+	}
+	if targetRsrc == models.MetricRsrc.String() {
+		return nil
+	}
+	if (targetRsrc == models.HostRsrc.String() || targetRsrc == models.NetworkRsrc.String()) && r.Method == http.MethodGet && targetRsrcID == "" {
+		return nil
+	}
+	if targetRsrc == models.UserRsrc.String() && username == targetRsrcID && (r.Method != http.MethodDelete) {
+		return nil
+	}
+	rsrcPermissionScope, ok := userRole.GlobalLevelAccess[models.RsrcType(targetRsrc)]
+	if !ok {
+		return fmt.Errorf("access denied to %s", targetRsrc)
+	}
+	if allRsrcsTypePermissionScope, ok := rsrcPermissionScope[models.RsrcID(fmt.Sprintf("all_%s", targetRsrc))]; ok {
+		return checkPermissionScopeWithReqMethod(allRsrcsTypePermissionScope, r.Method)
+
+	}
+	if targetRsrcID == "" {
+		return errors.New("target rsrc id is missing")
+	}
+	if scope, ok := rsrcPermissionScope[models.RsrcID(targetRsrcID)]; ok {
+		return checkPermissionScopeWithReqMethod(scope, r.Method)
+	}
+	return errors.New("access denied")
+}
+
+func checkPermissionScopeWithReqMethod(scope models.RsrcPermissionScope, reqmethod string) error {
+	if reqmethod == http.MethodGet && scope.Read {
+		return nil
+	}
+	if (reqmethod == http.MethodPatch || reqmethod == http.MethodPut) && scope.Update {
+		return nil
+	}
+	if reqmethod == http.MethodDelete && scope.Delete {
+		return nil
+	}
+	if reqmethod == http.MethodPost && scope.Create {
+		return nil
+	}
+	return errors.New("operation not permitted")
+}

+ 1057 - 0
pro/logic/user_mgmt.go

@@ -0,0 +1,1057 @@
+package logic
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+
+	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/mq"
+	"github.com/gravitl/netmaker/servercfg"
+	"golang.org/x/exp/slog"
+)
+
+var ServiceUserPermissionTemplate = models.UserRolePermissionTemplate{
+	ID:                  models.ServiceUser,
+	Default:             true,
+	FullAccess:          false,
+	DenyDashboardAccess: true,
+}
+
+var PlatformUserUserPermissionTemplate = models.UserRolePermissionTemplate{
+	ID:         models.PlatformUser,
+	Default:    true,
+	FullAccess: false,
+}
+
+var NetworkAdminAllPermissionTemplate = models.UserRolePermissionTemplate{
+	ID:         models.UserRoleID(fmt.Sprintf("global-%s", models.NetworkAdmin)),
+	Default:    true,
+	FullAccess: true,
+	NetworkID:  models.AllNetworks,
+}
+
+var NetworkUserAllPermissionTemplate = models.UserRolePermissionTemplate{
+	ID:         models.UserRoleID(fmt.Sprintf("global-%s", models.NetworkUser)),
+	Default:    true,
+	FullAccess: false,
+	NetworkID:  models.AllNetworks,
+	NetworkLevelAccess: map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope{
+		models.RemoteAccessGwRsrc: {
+			models.AllRemoteAccessGwRsrcID: models.RsrcPermissionScope{
+				Read:      true,
+				VPNaccess: true,
+			},
+		},
+		models.ExtClientsRsrc: {
+			models.AllExtClientsRsrcID: models.RsrcPermissionScope{
+				Read:     true,
+				Create:   true,
+				Update:   true,
+				Delete:   true,
+				SelfOnly: true,
+			},
+		},
+	},
+}
+
+func UserRolesInit() {
+	d, _ := json.Marshal(logic.SuperAdminPermissionTemplate)
+	database.Insert(logic.SuperAdminPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+	d, _ = json.Marshal(logic.AdminPermissionTemplate)
+	database.Insert(logic.AdminPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+	d, _ = json.Marshal(ServiceUserPermissionTemplate)
+	database.Insert(ServiceUserPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+	d, _ = json.Marshal(PlatformUserUserPermissionTemplate)
+	database.Insert(PlatformUserUserPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+	d, _ = json.Marshal(NetworkAdminAllPermissionTemplate)
+	database.Insert(NetworkAdminAllPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+	d, _ = json.Marshal(NetworkUserAllPermissionTemplate)
+	database.Insert(NetworkUserAllPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+
+}
+
+func CreateDefaultNetworkRolesAndGroups(netID models.NetworkID) {
+	var NetworkAdminPermissionTemplate = models.UserRolePermissionTemplate{
+		ID:                 models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkAdmin)),
+		Default:            true,
+		NetworkID:          netID,
+		FullAccess:         true,
+		NetworkLevelAccess: make(map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope),
+	}
+
+	var NetworkUserPermissionTemplate = models.UserRolePermissionTemplate{
+		ID:                  models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkUser)),
+		Default:             true,
+		FullAccess:          false,
+		NetworkID:           netID,
+		DenyDashboardAccess: false,
+		NetworkLevelAccess: map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope{
+			models.RemoteAccessGwRsrc: {
+				models.AllRemoteAccessGwRsrcID: models.RsrcPermissionScope{
+					Read:      true,
+					VPNaccess: true,
+				},
+			},
+			models.ExtClientsRsrc: {
+				models.AllExtClientsRsrcID: models.RsrcPermissionScope{
+					Read:     true,
+					Create:   true,
+					Update:   true,
+					Delete:   true,
+					SelfOnly: true,
+				},
+			},
+		},
+	}
+	d, _ := json.Marshal(NetworkAdminPermissionTemplate)
+	database.Insert(NetworkAdminPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+	d, _ = json.Marshal(NetworkUserPermissionTemplate)
+	database.Insert(NetworkUserPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+
+	// create default network groups
+	var NetworkAdminGroup = models.UserGroup{
+		ID: models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkAdmin)),
+		NetworkRoles: map[models.NetworkID]map[models.UserRoleID]struct{}{
+			netID: {
+				models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkAdmin)): {},
+			},
+		},
+		MetaData: "The network role was automatically created by Netmaker.",
+	}
+	var NetworkUserGroup = models.UserGroup{
+		ID: models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkUser)),
+		NetworkRoles: map[models.NetworkID]map[models.UserRoleID]struct{}{
+			netID: {
+				models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkUser)): {},
+			},
+		},
+		MetaData: "The network role was automatically created by Netmaker.",
+	}
+	d, _ = json.Marshal(NetworkAdminGroup)
+	database.Insert(NetworkAdminGroup.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME)
+	d, _ = json.Marshal(NetworkUserGroup)
+	database.Insert(NetworkUserGroup.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME)
+}
+
+func DeleteNetworkRoles(netID string) {
+	users, err := logic.GetUsersDB()
+	if err != nil {
+		return
+	}
+	defaultUserGrp := fmt.Sprintf("%s-%s-grp", netID, models.NetworkUser)
+	defaultAdminGrp := fmt.Sprintf("%s-%s-grp", netID, models.NetworkAdmin)
+	for _, user := range users {
+		var upsert bool
+		if _, ok := user.NetworkRoles[models.NetworkID(netID)]; ok {
+			delete(user.NetworkRoles, models.NetworkID(netID))
+			upsert = true
+		}
+		if _, ok := user.UserGroups[models.UserGroupID(defaultUserGrp)]; ok {
+			delete(user.UserGroups, models.UserGroupID(defaultUserGrp))
+			upsert = true
+		}
+		if _, ok := user.UserGroups[models.UserGroupID(defaultAdminGrp)]; ok {
+			delete(user.UserGroups, models.UserGroupID(defaultAdminGrp))
+			upsert = true
+		}
+		if upsert {
+			logic.UpsertUser(user)
+		}
+	}
+	database.DeleteRecord(database.USER_GROUPS_TABLE_NAME, defaultUserGrp)
+	database.DeleteRecord(database.USER_GROUPS_TABLE_NAME, defaultAdminGrp)
+	userGs, _ := ListUserGroups()
+	for _, userGI := range userGs {
+		if _, ok := userGI.NetworkRoles[models.NetworkID(netID)]; ok {
+			delete(userGI.NetworkRoles, models.NetworkID(netID))
+			UpdateUserGroup(userGI)
+		}
+	}
+
+	roles, _ := ListNetworkRoles()
+	for _, role := range roles {
+		if role.NetworkID.String() == netID {
+			database.DeleteRecord(database.USER_PERMISSIONS_TABLE_NAME, role.ID.String())
+		}
+	}
+}
+
+// ListNetworkRoles - lists user network roles permission templates
+func ListNetworkRoles() ([]models.UserRolePermissionTemplate, error) {
+	data, err := database.FetchRecords(database.USER_PERMISSIONS_TABLE_NAME)
+	if err != nil && !database.IsEmptyRecord(err) {
+		return []models.UserRolePermissionTemplate{}, err
+	}
+	userRoles := []models.UserRolePermissionTemplate{}
+	for _, dataI := range data {
+		userRole := models.UserRolePermissionTemplate{}
+		err := json.Unmarshal([]byte(dataI), &userRole)
+		if err != nil {
+			continue
+		}
+		if userRole.NetworkID == "" {
+			continue
+		}
+		userRoles = append(userRoles, userRole)
+	}
+	return userRoles, nil
+}
+
+// ListPlatformRoles - lists user platform roles permission templates
+func ListPlatformRoles() ([]models.UserRolePermissionTemplate, error) {
+	data, err := database.FetchRecords(database.USER_PERMISSIONS_TABLE_NAME)
+	if err != nil && !database.IsEmptyRecord(err) {
+		return []models.UserRolePermissionTemplate{}, err
+	}
+	userRoles := []models.UserRolePermissionTemplate{}
+	for _, dataI := range data {
+		userRole := models.UserRolePermissionTemplate{}
+		err := json.Unmarshal([]byte(dataI), &userRole)
+		if err != nil {
+			continue
+		}
+		if userRole.NetworkID != "" {
+			continue
+		}
+		userRoles = append(userRoles, userRole)
+	}
+	return userRoles, nil
+}
+
+func ValidateCreateRoleReq(userRole *models.UserRolePermissionTemplate) error {
+	// check if role exists with this id
+	_, err := logic.GetRole(userRole.ID)
+	if err == nil {
+		return fmt.Errorf("role with id `%s` exists already", userRole.ID.String())
+	}
+	if len(userRole.NetworkLevelAccess) > 0 {
+		for rsrcType := range userRole.NetworkLevelAccess {
+			if _, ok := models.RsrcTypeMap[rsrcType]; !ok {
+				return errors.New("invalid rsrc type " + rsrcType.String())
+			}
+			if rsrcType == models.RemoteAccessGwRsrc {
+				userRsrcPermissions := userRole.NetworkLevelAccess[models.RemoteAccessGwRsrc]
+				var vpnAccess bool
+				for _, scope := range userRsrcPermissions {
+					if scope.VPNaccess {
+						vpnAccess = true
+						break
+					}
+				}
+				if vpnAccess {
+					userRole.NetworkLevelAccess[models.ExtClientsRsrc] = map[models.RsrcID]models.RsrcPermissionScope{
+						models.AllExtClientsRsrcID: {
+							Read:     true,
+							Create:   true,
+							Update:   true,
+							Delete:   true,
+							SelfOnly: true,
+						},
+					}
+
+				}
+
+			}
+		}
+	}
+	if userRole.NetworkID == "" {
+		return errors.New("only network roles are allowed to be created")
+	}
+	return nil
+}
+
+func ValidateUpdateRoleReq(userRole *models.UserRolePermissionTemplate) error {
+	roleInDB, err := logic.GetRole(userRole.ID)
+	if err != nil {
+		return err
+	}
+	if roleInDB.NetworkID != userRole.NetworkID {
+		return errors.New("network id mismatch")
+	}
+	if roleInDB.Default {
+		return errors.New("cannot update default role")
+	}
+	if len(userRole.NetworkLevelAccess) > 0 {
+		for rsrcType := range userRole.NetworkLevelAccess {
+			if _, ok := models.RsrcTypeMap[rsrcType]; !ok {
+				return errors.New("invalid rsrc type " + rsrcType.String())
+			}
+			if rsrcType == models.RemoteAccessGwRsrc {
+				userRsrcPermissions := userRole.NetworkLevelAccess[models.RemoteAccessGwRsrc]
+				var vpnAccess bool
+				for _, scope := range userRsrcPermissions {
+					if scope.VPNaccess {
+						vpnAccess = true
+						break
+					}
+				}
+				if vpnAccess {
+					userRole.NetworkLevelAccess[models.ExtClientsRsrc] = map[models.RsrcID]models.RsrcPermissionScope{
+						models.AllExtClientsRsrcID: {
+							Read:     true,
+							Create:   true,
+							Update:   true,
+							Delete:   true,
+							SelfOnly: true,
+						},
+					}
+
+				}
+
+			}
+		}
+	}
+	return nil
+}
+
+// CreateRole - inserts new role into DB
+func CreateRole(r models.UserRolePermissionTemplate) error {
+	// check if role already exists
+	if r.ID.String() == "" {
+		return errors.New("role id cannot be empty")
+	}
+	_, err := database.FetchRecord(database.USER_PERMISSIONS_TABLE_NAME, r.ID.String())
+	if err == nil {
+		return errors.New("role already exists")
+	}
+	d, err := json.Marshal(r)
+	if err != nil {
+		return err
+	}
+	return database.Insert(r.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+}
+
+// UpdateRole - updates role template
+func UpdateRole(r models.UserRolePermissionTemplate) error {
+	if r.ID.String() == "" {
+		return errors.New("role id cannot be empty")
+	}
+	_, err := database.FetchRecord(database.USER_PERMISSIONS_TABLE_NAME, r.ID.String())
+	if err != nil {
+		return err
+	}
+	d, err := json.Marshal(r)
+	if err != nil {
+		return err
+	}
+	return database.Insert(r.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+}
+
+// DeleteRole - deletes user role
+func DeleteRole(rid models.UserRoleID, force bool) error {
+	if rid.String() == "" {
+		return errors.New("role id cannot be empty")
+	}
+	users, err := logic.GetUsersDB()
+	if err != nil {
+		return err
+	}
+	role, err := logic.GetRole(rid)
+	if err != nil {
+		return err
+	}
+	if role.NetworkID == "" {
+		return errors.New("cannot delete platform role")
+	}
+	// allow deletion of default network roles if network doesn't exist
+	if role.NetworkID == models.AllNetworks {
+		return errors.New("cannot delete default network role")
+	}
+	// check if network exists
+	exists, _ := logic.NetworkExists(role.NetworkID.String())
+	if role.Default {
+		if exists && !force {
+			return errors.New("cannot delete default role")
+		}
+	}
+	for _, user := range users {
+		for userG := range user.UserGroups {
+			ug, err := GetUserGroup(userG)
+			if err == nil {
+				if role.NetworkID != "" {
+					for netID, networkRoles := range ug.NetworkRoles {
+						if _, ok := networkRoles[rid]; ok {
+							delete(networkRoles, rid)
+							ug.NetworkRoles[netID] = networkRoles
+							UpdateUserGroup(ug)
+						}
+
+					}
+				}
+
+			}
+		}
+
+		if user.PlatformRoleID == rid {
+			err = errors.New("active roles cannot be deleted.switch existing users to a new role before deleting")
+			return err
+		}
+		if role.NetworkID != "" {
+			for netID, networkRoles := range user.NetworkRoles {
+				if _, ok := networkRoles[rid]; ok {
+					delete(networkRoles, rid)
+					user.NetworkRoles[netID] = networkRoles
+					logic.UpsertUser(user)
+				}
+
+			}
+		}
+	}
+
+	return database.DeleteRecord(database.USER_PERMISSIONS_TABLE_NAME, rid.String())
+}
+
+func ValidateCreateGroupReq(g models.UserGroup) error {
+
+	// check if network roles are valid
+	for _, roleMap := range g.NetworkRoles {
+		for roleID := range roleMap {
+			role, err := logic.GetRole(roleID)
+			if err != nil {
+				return fmt.Errorf("invalid network role %s", roleID)
+			}
+			if role.NetworkID == "" {
+				return errors.New("platform role cannot be used as network role")
+			}
+		}
+	}
+	return nil
+}
+func ValidateUpdateGroupReq(g models.UserGroup) error {
+
+	for networkID := range g.NetworkRoles {
+		userRolesMap := g.NetworkRoles[networkID]
+		for roleID := range userRolesMap {
+			netRole, err := logic.GetRole(roleID)
+			if err != nil {
+				err = fmt.Errorf("invalid network role")
+				return err
+			}
+			if netRole.NetworkID == "" {
+				return errors.New("platform role cannot be used as network role")
+			}
+		}
+	}
+	return nil
+}
+
+// CreateUserGroup - creates new user group
+func CreateUserGroup(g models.UserGroup) error {
+	// check if role already exists
+	if g.ID == "" {
+		return errors.New("group id cannot be empty")
+	}
+	_, err := database.FetchRecord(database.USER_GROUPS_TABLE_NAME, g.ID.String())
+	if err == nil {
+		return errors.New("group already exists")
+	}
+	d, err := json.Marshal(g)
+	if err != nil {
+		return err
+	}
+	return database.Insert(g.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME)
+}
+
+// GetUserGroup - fetches user group
+func GetUserGroup(gid models.UserGroupID) (models.UserGroup, error) {
+	d, err := database.FetchRecord(database.USER_GROUPS_TABLE_NAME, gid.String())
+	if err != nil {
+		return models.UserGroup{}, err
+	}
+	var ug models.UserGroup
+	err = json.Unmarshal([]byte(d), &ug)
+	if err != nil {
+		return ug, err
+	}
+	return ug, nil
+}
+
+// ListUserGroups - lists user groups
+func ListUserGroups() ([]models.UserGroup, error) {
+	data, err := database.FetchRecords(database.USER_GROUPS_TABLE_NAME)
+	if err != nil && !database.IsEmptyRecord(err) {
+		return []models.UserGroup{}, err
+	}
+	userGroups := []models.UserGroup{}
+	for _, dataI := range data {
+		userGroup := models.UserGroup{}
+		err := json.Unmarshal([]byte(dataI), &userGroup)
+		if err != nil {
+			continue
+		}
+		userGroups = append(userGroups, userGroup)
+	}
+	return userGroups, nil
+}
+
+// UpdateUserGroup - updates new user group
+func UpdateUserGroup(g models.UserGroup) error {
+	// check if group exists
+	if g.ID == "" {
+		return errors.New("group id cannot be empty")
+	}
+	_, err := database.FetchRecord(database.USER_GROUPS_TABLE_NAME, g.ID.String())
+	if err != nil {
+		return err
+	}
+	d, err := json.Marshal(g)
+	if err != nil {
+		return err
+	}
+	return database.Insert(g.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME)
+}
+
+// DeleteUserGroup - deletes user group
+func DeleteUserGroup(gid models.UserGroupID) error {
+	users, err := logic.GetUsersDB()
+	if err != nil {
+		return err
+	}
+	for _, user := range users {
+		delete(user.UserGroups, gid)
+		logic.UpsertUser(user)
+	}
+	return database.DeleteRecord(database.USER_GROUPS_TABLE_NAME, gid.String())
+}
+
+func HasNetworkRsrcScope(permissionTemplate models.UserRolePermissionTemplate, netid string, rsrcType models.RsrcType, rsrcID models.RsrcID, op string) bool {
+	if permissionTemplate.FullAccess {
+		return true
+	}
+
+	rsrcScope, ok := permissionTemplate.NetworkLevelAccess[rsrcType]
+	if !ok {
+		return false
+	}
+	_, ok = rsrcScope[rsrcID]
+	return ok
+}
+func GetUserRAGNodes(user models.User) (gws map[string]models.Node) {
+	gws = make(map[string]models.Node)
+	userGwAccessScope := GetUserNetworkRolesWithRemoteVPNAccess(user)
+	logger.Log(0, fmt.Sprintf("User Gw Access Scope: %+v", userGwAccessScope))
+	_, allNetAccess := userGwAccessScope["*"]
+	nodes, err := logic.GetAllNodes()
+	if err != nil {
+		return
+	}
+	for _, node := range nodes {
+		if node.IsIngressGateway && !node.PendingDelete {
+			if allNetAccess {
+				gws[node.ID.String()] = node
+			} else {
+				gwRsrcMap := userGwAccessScope[models.NetworkID(node.Network)]
+				scope, ok := gwRsrcMap[models.AllRemoteAccessGwRsrcID]
+				if !ok {
+					if scope, ok = gwRsrcMap[models.RsrcID(node.ID.String())]; !ok {
+						continue
+					}
+				}
+				if scope.VPNaccess {
+					gws[node.ID.String()] = node
+				}
+
+			}
+		}
+	}
+	return
+}
+
+// GetUserNetworkRoles - get user network roles
+func GetUserNetworkRolesWithRemoteVPNAccess(user models.User) (gwAccess map[models.NetworkID]map[models.RsrcID]models.RsrcPermissionScope) {
+	gwAccess = make(map[models.NetworkID]map[models.RsrcID]models.RsrcPermissionScope)
+	platformRole, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		return
+	}
+	if platformRole.FullAccess {
+		gwAccess[models.NetworkID("*")] = make(map[models.RsrcID]models.RsrcPermissionScope)
+		return
+	}
+	if _, ok := user.NetworkRoles[models.AllNetworks]; ok {
+		gwAccess[models.NetworkID("*")] = make(map[models.RsrcID]models.RsrcPermissionScope)
+	}
+	if len(user.UserGroups) > 0 {
+		for gID := range user.UserGroups {
+			userG, err := GetUserGroup(gID)
+			if err != nil {
+				continue
+			}
+			for netID, roleMap := range userG.NetworkRoles {
+				for roleID := range roleMap {
+					role, err := logic.GetRole(roleID)
+					if err == nil {
+						if role.FullAccess {
+							gwAccess[netID] = map[models.RsrcID]models.RsrcPermissionScope{
+								models.AllRemoteAccessGwRsrcID: {
+									Create:    true,
+									Read:      true,
+									Update:    true,
+									VPNaccess: true,
+									Delete:    true,
+								},
+								models.AllExtClientsRsrcID: {
+									Create: true,
+									Read:   true,
+									Update: true,
+									Delete: true,
+								},
+							}
+							break
+						}
+						if rsrcsMap, ok := role.NetworkLevelAccess[models.RemoteAccessGwRsrc]; ok {
+							if permissions, ok := rsrcsMap[models.AllRemoteAccessGwRsrcID]; ok && permissions.VPNaccess {
+								if len(gwAccess[netID]) == 0 {
+									gwAccess[netID] = make(map[models.RsrcID]models.RsrcPermissionScope)
+								}
+								gwAccess[netID][models.AllRemoteAccessGwRsrcID] = permissions
+								break
+							} else {
+								for gwID, scope := range rsrcsMap {
+									if scope.VPNaccess {
+										if len(gwAccess[netID]) == 0 {
+											gwAccess[netID] = make(map[models.RsrcID]models.RsrcPermissionScope)
+										}
+										gwAccess[netID][gwID] = scope
+									}
+								}
+							}
+
+						}
+
+					}
+				}
+			}
+		}
+	}
+	for netID, roleMap := range user.NetworkRoles {
+		for roleID := range roleMap {
+			role, err := logic.GetRole(roleID)
+			if err == nil {
+				if role.FullAccess {
+					gwAccess[netID] = map[models.RsrcID]models.RsrcPermissionScope{
+						models.AllRemoteAccessGwRsrcID: {
+							Create:    true,
+							Read:      true,
+							Update:    true,
+							VPNaccess: true,
+							Delete:    true,
+						},
+						models.AllExtClientsRsrcID: {
+							Create: true,
+							Read:   true,
+							Update: true,
+							Delete: true,
+						},
+					}
+					break
+				}
+				if rsrcsMap, ok := role.NetworkLevelAccess[models.RemoteAccessGwRsrc]; ok {
+					if permissions, ok := rsrcsMap[models.AllRemoteAccessGwRsrcID]; ok && permissions.VPNaccess {
+						if len(gwAccess[netID]) == 0 {
+							gwAccess[netID] = make(map[models.RsrcID]models.RsrcPermissionScope)
+						}
+						gwAccess[netID][models.AllRemoteAccessGwRsrcID] = permissions
+						break
+					} else {
+						for gwID, scope := range rsrcsMap {
+							if scope.VPNaccess {
+								if len(gwAccess[netID]) == 0 {
+									gwAccess[netID] = make(map[models.RsrcID]models.RsrcPermissionScope)
+								}
+								gwAccess[netID][gwID] = scope
+							}
+						}
+					}
+
+				}
+
+			}
+		}
+	}
+
+	return
+}
+
+func GetFilteredNodesByUserAccess(user models.User, nodes []models.Node) (filteredNodes []models.Node) {
+
+	nodesMap := make(map[string]struct{})
+	allNetworkRoles := make(map[models.UserRoleID]struct{})
+
+	if len(user.NetworkRoles) > 0 {
+		for _, netRoles := range user.NetworkRoles {
+			for netRoleI := range netRoles {
+				allNetworkRoles[netRoleI] = struct{}{}
+			}
+		}
+	}
+	if _, ok := user.NetworkRoles[models.AllNetworks]; ok {
+		return nodes
+	}
+	if len(user.UserGroups) > 0 {
+		for userGID := range user.UserGroups {
+			userG, err := GetUserGroup(userGID)
+			if err == nil {
+				if len(userG.NetworkRoles) > 0 {
+					if _, ok := userG.NetworkRoles[models.AllNetworks]; ok {
+						return nodes
+					}
+					for _, netRoles := range userG.NetworkRoles {
+						for netRoleI := range netRoles {
+							allNetworkRoles[netRoleI] = struct{}{}
+						}
+					}
+				}
+			}
+		}
+	}
+	for networkRoleID := range allNetworkRoles {
+		userPermTemplate, err := logic.GetRole(networkRoleID)
+		if err != nil {
+			continue
+		}
+		networkNodes := logic.GetNetworkNodesMemory(nodes, userPermTemplate.NetworkID.String())
+		if userPermTemplate.FullAccess {
+			for _, node := range networkNodes {
+				if _, ok := nodesMap[node.ID.String()]; ok {
+					continue
+				}
+				nodesMap[node.ID.String()] = struct{}{}
+				filteredNodes = append(filteredNodes, node)
+			}
+
+			continue
+		}
+		if rsrcPerms, ok := userPermTemplate.NetworkLevelAccess[models.RemoteAccessGwRsrc]; ok {
+			if _, ok := rsrcPerms[models.AllRemoteAccessGwRsrcID]; ok {
+				for _, node := range networkNodes {
+					if _, ok := nodesMap[node.ID.String()]; ok {
+						continue
+					}
+					if node.IsIngressGateway {
+						nodesMap[node.ID.String()] = struct{}{}
+						filteredNodes = append(filteredNodes, node)
+					}
+				}
+			} else {
+				for gwID, scope := range rsrcPerms {
+					if _, ok := nodesMap[gwID.String()]; ok {
+						continue
+					}
+					if scope.Read {
+						gwNode, err := logic.GetNodeByID(gwID.String())
+						if err == nil && gwNode.IsIngressGateway {
+							nodesMap[gwNode.ID.String()] = struct{}{}
+							filteredNodes = append(filteredNodes, gwNode)
+						}
+					}
+				}
+			}
+		}
+
+	}
+	return
+}
+
+func FilterNetworksByRole(allnetworks []models.Network, user models.User) []models.Network {
+	platformRole, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		return []models.Network{}
+	}
+	if !platformRole.FullAccess {
+		allNetworkRoles := make(map[models.NetworkID]struct{})
+		if len(user.NetworkRoles) > 0 {
+			for netID := range user.NetworkRoles {
+				if netID == models.AllNetworks {
+					return allnetworks
+				}
+				allNetworkRoles[netID] = struct{}{}
+
+			}
+		}
+		if len(user.UserGroups) > 0 {
+			for userGID := range user.UserGroups {
+				userG, err := GetUserGroup(userGID)
+				if err == nil {
+					if len(userG.NetworkRoles) > 0 {
+						for netID := range userG.NetworkRoles {
+							if netID == models.AllNetworks {
+								return allnetworks
+							}
+							allNetworkRoles[netID] = struct{}{}
+
+						}
+					}
+				}
+			}
+		}
+		filteredNetworks := []models.Network{}
+		for _, networkI := range allnetworks {
+			if _, ok := allNetworkRoles[models.NetworkID(networkI.NetID)]; ok {
+				filteredNetworks = append(filteredNetworks, networkI)
+			}
+		}
+		allnetworks = filteredNetworks
+	}
+	return allnetworks
+}
+
+func IsGroupsValid(groups map[models.UserGroupID]struct{}) error {
+
+	for groupID := range groups {
+		_, err := GetUserGroup(groupID)
+		if err != nil {
+			return fmt.Errorf("user group `%s` not found", groupID)
+		}
+	}
+	return nil
+}
+
+func IsNetworkRolesValid(networkRoles map[models.NetworkID]map[models.UserRoleID]struct{}) error {
+	for netID, netRoles := range networkRoles {
+
+		if netID != models.AllNetworks {
+			_, err := logic.GetNetwork(netID.String())
+			if err != nil {
+				return fmt.Errorf("failed to fetch network %s ", netID)
+			}
+		}
+		for netRoleID := range netRoles {
+			role, err := logic.GetRole(netRoleID)
+			if err != nil {
+				return fmt.Errorf("failed to fetch role %s ", netRoleID)
+			}
+			if role.NetworkID == "" {
+				return fmt.Errorf("cannot use platform as network role %s", netRoleID)
+			}
+		}
+	}
+	return nil
+}
+
+// PrepareOauthUserFromInvite - init oauth user before create
+func PrepareOauthUserFromInvite(in models.UserInvite) (models.User, error) {
+	var newPass, fetchErr = logic.FetchPassValue("")
+	if fetchErr != nil {
+		return models.User{}, fetchErr
+	}
+	user := models.User{
+		UserName: in.Email,
+		Password: newPass,
+	}
+	user.UserGroups = in.UserGroups
+	user.NetworkRoles = in.NetworkRoles
+	user.PlatformRoleID = models.UserRoleID(in.PlatformRoleID)
+	if user.PlatformRoleID == "" {
+		user.PlatformRoleID = models.ServiceUser
+	}
+	return user, nil
+}
+
+func UpdatesUserGwAccessOnRoleUpdates(currNetworkAccess,
+	changeNetworkAccess map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope, netID string) {
+	networkChangeMap := make(map[models.RsrcID]models.RsrcPermissionScope)
+	for rsrcType, RsrcPermsMap := range currNetworkAccess {
+		if rsrcType != models.RemoteAccessGwRsrc {
+			continue
+		}
+		if _, ok := changeNetworkAccess[rsrcType]; !ok {
+			for rsrcID, scope := range RsrcPermsMap {
+				networkChangeMap[rsrcID] = scope
+			}
+		} else {
+			for rsrcID, scope := range RsrcPermsMap {
+				if _, ok := changeNetworkAccess[rsrcType][rsrcID]; !ok {
+					networkChangeMap[rsrcID] = scope
+				}
+			}
+		}
+	}
+
+	extclients, err := logic.GetAllExtClients()
+	if err != nil {
+		slog.Error("failed to fetch extclients", "error", err)
+		return
+	}
+	userMap, err := logic.GetUserMap()
+	if err != nil {
+		return
+	}
+	for _, extclient := range extclients {
+		if extclient.Network != netID {
+			continue
+		}
+		if _, ok := networkChangeMap[models.AllRemoteAccessGwRsrcID]; ok {
+			if user, ok := userMap[extclient.OwnerID]; ok {
+				if user.PlatformRoleID != models.ServiceUser {
+					continue
+				}
+				err = logic.DeleteExtClientAndCleanup(extclient)
+				if err != nil {
+					slog.Error("failed to delete extclient",
+						"id", extclient.ClientID, "owner", user.UserName, "error", err)
+				} else {
+					if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil {
+						slog.Error("error setting ext peers: " + err.Error())
+					}
+				}
+			}
+			continue
+		}
+		if _, ok := networkChangeMap[models.RsrcID(extclient.IngressGatewayID)]; ok {
+			if user, ok := userMap[extclient.OwnerID]; ok {
+				if user.PlatformRoleID != models.ServiceUser {
+					continue
+				}
+				err = logic.DeleteExtClientAndCleanup(extclient)
+				if err != nil {
+					slog.Error("failed to delete extclient",
+						"id", extclient.ClientID, "owner", user.UserName, "error", err)
+				} else {
+					if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil {
+						slog.Error("error setting ext peers: " + err.Error())
+					}
+				}
+			}
+
+		}
+
+	}
+	if servercfg.IsDNSMode() {
+		logic.SetDNS()
+	}
+}
+
+func UpdatesUserGwAccessOnGrpUpdates(currNetworkRoles, changeNetworkRoles map[models.NetworkID]map[models.UserRoleID]struct{}) {
+	networkChangeMap := make(map[models.NetworkID]map[models.UserRoleID]struct{})
+	for netID, networkUserRoles := range currNetworkRoles {
+		if _, ok := changeNetworkRoles[netID]; !ok {
+			for netRoleID := range networkUserRoles {
+				if _, ok := networkChangeMap[netID]; !ok {
+					networkChangeMap[netID] = make(map[models.UserRoleID]struct{})
+				}
+				networkChangeMap[netID][netRoleID] = struct{}{}
+			}
+		} else {
+			for netRoleID := range networkUserRoles {
+				if _, ok := changeNetworkRoles[netID][netRoleID]; !ok {
+					if _, ok := networkChangeMap[netID]; !ok {
+						networkChangeMap[netID] = make(map[models.UserRoleID]struct{})
+					}
+					networkChangeMap[netID][netRoleID] = struct{}{}
+				}
+			}
+		}
+	}
+	extclients, err := logic.GetAllExtClients()
+	if err != nil {
+		slog.Error("failed to fetch extclients", "error", err)
+		return
+	}
+	userMap, err := logic.GetUserMap()
+	if err != nil {
+		return
+	}
+	for _, extclient := range extclients {
+
+		if _, ok := networkChangeMap[models.NetworkID(extclient.Network)]; ok {
+			if user, ok := userMap[extclient.OwnerID]; ok {
+				if user.PlatformRoleID != models.ServiceUser {
+					continue
+				}
+				err = logic.DeleteExtClientAndCleanup(extclient)
+				if err != nil {
+					slog.Error("failed to delete extclient",
+						"id", extclient.ClientID, "owner", user.UserName, "error", err)
+				} else {
+					if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil {
+						slog.Error("error setting ext peers: " + err.Error())
+					}
+				}
+			}
+
+		}
+
+	}
+	if servercfg.IsDNSMode() {
+		logic.SetDNS()
+	}
+
+}
+
+func UpdateUserGwAccess(currentUser, changeUser models.User) {
+	if changeUser.PlatformRoleID != models.ServiceUser {
+		return
+	}
+
+	networkChangeMap := make(map[models.NetworkID]map[models.UserRoleID]struct{})
+	for netID, networkUserRoles := range currentUser.NetworkRoles {
+		if _, ok := changeUser.NetworkRoles[netID]; !ok {
+			for netRoleID := range networkUserRoles {
+				if _, ok := networkChangeMap[netID]; !ok {
+					networkChangeMap[netID] = make(map[models.UserRoleID]struct{})
+				}
+				networkChangeMap[netID][netRoleID] = struct{}{}
+			}
+		} else {
+			for netRoleID := range networkUserRoles {
+				if _, ok := changeUser.NetworkRoles[netID][netRoleID]; !ok {
+					if _, ok := networkChangeMap[netID]; !ok {
+						networkChangeMap[netID] = make(map[models.UserRoleID]struct{})
+					}
+					networkChangeMap[netID][netRoleID] = struct{}{}
+				}
+			}
+		}
+	}
+	for gID := range currentUser.UserGroups {
+		if _, ok := changeUser.UserGroups[gID]; ok {
+			continue
+		}
+		userG, err := GetUserGroup(gID)
+		if err == nil {
+			for netID, networkUserRoles := range userG.NetworkRoles {
+				for netRoleID := range networkUserRoles {
+					if _, ok := networkChangeMap[netID]; !ok {
+						networkChangeMap[netID] = make(map[models.UserRoleID]struct{})
+					}
+					networkChangeMap[netID][netRoleID] = struct{}{}
+				}
+			}
+		}
+	}
+	if len(networkChangeMap) == 0 {
+		return
+	}
+	// TODO - cleanup gw access when role and groups are updated
+	//removedGwAccess
+	extclients, err := logic.GetAllExtClients()
+	if err != nil {
+		slog.Error("failed to fetch extclients", "error", err)
+		return
+	}
+	for _, extclient := range extclients {
+		if extclient.OwnerID == currentUser.UserName {
+			if _, ok := networkChangeMap[models.NetworkID(extclient.Network)]; ok {
+				err = logic.DeleteExtClientAndCleanup(extclient)
+				if err != nil {
+					slog.Error("failed to delete extclient",
+						"id", extclient.ClientID, "owner", changeUser.UserName, "error", err)
+				} else {
+					if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil {
+						slog.Error("error setting ext peers: " + err.Error())
+					}
+				}
+			}
+
+		}
+	}
+	if servercfg.IsDNSMode() {
+		logic.SetDNS()
+	}
+
+}

+ 11 - 0
scripts/netmaker.default.env

@@ -75,3 +75,14 @@ RAC_AUTO_DISABLE=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
+# config for sending emails
+# mail server host
+SMTP_HOST=smtp.gmail.com
+# mail server port
+SMTP_PORT=587
+# sender email
+EMAIL_SENDER_ADDR=
+# sender email auth
+EMAIL_SENDER_AUTH=
+# mail sender type (smtp or resend)
+EMAIL_SENDER_TYPE=smtp

+ 1 - 0
scripts/nm-quick.sh

@@ -231,6 +231,7 @@ save_config() { (
 	fi
 	if [ -n "$NETMAKER_BASE_DOMAIN" ]; then
 		save_config_item NM_DOMAIN "$NETMAKER_BASE_DOMAIN"
+		save_config_item FRONTEND_URL "https://dashboard.$NETMAKER_BASE_DOMAIN"
 	fi
 	save_config_item UI_IMAGE_TAG "$IMAGE_TAG"
 	# version-specific entries

+ 54 - 1
servercfg/serverconf.go

@@ -242,6 +242,59 @@ func GetPublicBrokerEndpoint() string {
 	}
 }
 
+func GetSmtpHost() string {
+	v := ""
+	if fromEnv := os.Getenv("SMTP_HOST"); fromEnv != "" {
+		v = fromEnv
+	} else if fromCfg := config.Config.Server.SmtpHost; fromCfg != "" {
+		v = fromCfg
+	}
+	return v
+}
+
+func GetSmtpPort() int {
+	v := 587
+	if fromEnv := os.Getenv("SMTP_PORT"); fromEnv != "" {
+		port, err := strconv.Atoi(fromEnv)
+		if err == nil {
+			v = port
+		}
+	} else if fromCfg := config.Config.Server.SmtpPort; fromCfg != 0 {
+		v = fromCfg
+	}
+	return v
+}
+
+func GetSenderEmail() string {
+	v := ""
+	if fromEnv := os.Getenv("EMAIL_SENDER_ADDR"); fromEnv != "" {
+		v = fromEnv
+	} else if fromCfg := config.Config.Server.EmailSenderAddr; fromCfg != "" {
+		v = fromCfg
+	}
+	return v
+}
+
+func GetEmaiSenderAuth() string {
+	v := ""
+	if fromEnv := os.Getenv("EMAIL_SENDER_AUTH"); fromEnv != "" {
+		v = fromEnv
+	} else if fromCfg := config.Config.Server.EmailSenderAddr; fromCfg != "" {
+		v = fromCfg
+	}
+	return v
+}
+
+func EmailSenderType() string {
+	s := ""
+	if fromEnv := os.Getenv("EMAIL_SENDER_TYPE"); fromEnv != "" {
+		s = fromEnv
+	} else if fromCfg := config.Config.Server.EmailSenderType; fromCfg != "" {
+		s = fromCfg
+	}
+	return s
+}
+
 // GetOwnerEmail - gets the owner email (saas)
 func GetOwnerEmail() string {
 	return os.Getenv("SAAS_OWNER_EMAIL")
@@ -472,7 +525,7 @@ func GetPublicIP() (string, error) {
 			break
 		}
 	}
-	if err == nil && endpoint == "" {
+	if endpoint == "" {
 		err = errors.New("public address not found")
 	}
 	return endpoint, err