Bläddra i källkod

Merge pull request #3189 from gravitl/release-v0.26.0

v0.26.0
Abhishek K 10 månader sedan
förälder
incheckning
c245c789f6
91 ändrade filer med 3529 tillägg och 1197 borttagningar
  1. 1 0
      .github/ISSUE_TEMPLATE/bug-report.yml
  2. 0 4
      .github/workflows/docker-builder.yml
  3. 1 1
      .github/workflows/docs.yml
  4. 5 5
      .github/workflows/test.yml
  5. 2 2
      Dockerfile
  6. 1 1
      Dockerfile-quick
  7. 7 5
      README.md
  8. 10 2
      auth/host_session.go
  9. 1 1
      cli/cmd/user/groups.go
  10. 1 1
      cli/cmd/user/roles.go
  11. 1 1
      compose/docker-compose.netclient.yml
  12. 1 1
      compose/docker-compose.yml
  13. 2 0
      config/config.go
  14. 230 0
      controllers/acls.go
  15. 2 0
      controllers/controller.go
  16. 48 0
      controllers/dns.go
  17. 6 3
      controllers/enrollmentkeys.go
  18. 38 4
      controllers/ext_client.go
  19. 9 14
      controllers/hosts.go
  20. 10 1
      controllers/middleware.go
  21. 47 1
      controllers/network.go
  22. 6 0
      controllers/node.go
  23. 4 4
      controllers/regex.go
  24. 18 0
      controllers/server.go
  25. 231 0
      controllers/tags.go
  26. 3 0
      controllers/user.go
  27. 6 0
      database/database.go
  28. 6 2
      docker/Caddyfile
  29. 6 2
      docker/Caddyfile-pro
  30. 1 1
      docker/Dockerfile-go-builder
  31. 1 1
      docs/Authentication.md
  32. 12 12
      go.mod
  33. 23 28
      go.sum
  34. 1 1
      k8s/client/netclient-daemonset.yaml
  35. 1 1
      k8s/client/netclient.yaml
  36. 1 1
      k8s/server/netmaker-ui.yaml
  37. 650 0
      logic/acls.go
  38. 3 2
      logic/acls/common.go
  39. 2 1
      logic/acls/nodeacls/retrieve.go
  40. 2 1
      logic/auth.go
  41. 1 1
      logic/clients.go
  42. 30 5
      logic/enrollmentkey.go
  43. 15 15
      logic/enrollmentkey_test.go
  44. 311 3
      logic/extpeers.go
  45. 16 33
      logic/gateway.go
  46. 19 13
      logic/hosts.go
  47. 3 2
      logic/jwts.go
  48. 24 0
      logic/networks.go
  49. 130 8
      logic/nodes.go
  50. 21 2
      logic/peers.go
  51. 24 0
      logic/proc.go
  52. 290 0
      logic/tags.go
  53. 11 0
      logic/user_mgmt.go
  54. 3 2
      main.go
  55. 81 62
      migrate/migrate.go
  56. 72 0
      models/acl.go
  57. 42 27
      models/api_node.go
  58. 2 2
      models/dnsEntry.go
  59. 3 0
      models/enrollment_key.go
  60. 14 0
      models/extclient.go
  61. 11 9
      models/metrics.go
  62. 19 4
      models/mqtt.go
  63. 5 229
      models/names.go
  64. 5 0
      models/network.go
  65. 16 10
      models/node.go
  66. 17 3
      models/structs.go
  67. 52 0
      models/tags.go
  68. 8 3
      models/user_mgmt.go
  69. 56 0
      mq/publishers.go
  70. 2 2
      pro/auth/error.go
  71. 1 1
      pro/auth/templates.go
  72. 8 0
      pro/controllers/failover.go
  73. 14 0
      pro/controllers/rac.go
  74. 228 3
      pro/controllers/users.go
  75. 39 44
      pro/email/invite.go
  76. 129 521
      pro/email/utils.go
  77. 13 6
      pro/initialize.go
  78. 1 1
      pro/logic/ext_acls.go
  79. 8 5
      pro/logic/metrics.go
  80. 68 0
      pro/logic/migrate.go
  81. 12 0
      pro/logic/security.go
  82. 205 8
      pro/logic/user_mgmt.go
  83. 7 2
      pro/remote_access_client.go
  84. 6 6
      pro/types.go
  85. 9 9
      release.md
  86. 4 1
      scripts/netmaker.default.env
  87. 6 48
      scripts/nm-quick.sh
  88. 37 1
      servercfg/serverconf.go
  89. 29 0
      servercfg/serverconf_test.go
  90. 1 1
      serverctl/serverctl.go
  91. 1 1
      swagger.yaml

+ 1 - 0
.github/ISSUE_TEMPLATE/bug-report.yml

@@ -31,6 +31,7 @@ body:
       label: Version
       description: What version are you running?
       options:
+        - v0.26.0
         - v0.25.0
         - v0.24.3
         - v0.24.2

+ 0 - 4
.github/workflows/docker-builder.yml

@@ -7,10 +7,6 @@ on:
         description: 'Docker tag to use (default: latest)'
         required: false
         default: 'latest'
-  push:
-    branches:
-      - 'develop'
-
 jobs:
   go-builder:
     runs-on: ubuntu-latest

+ 1 - 1
.github/workflows/docs.yml

@@ -36,7 +36,7 @@ jobs:
         run: echo "timestamp=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT
 
       - name: Create Pull Request
-        uses: peter-evans/create-pull-request@v6
+        uses: peter-evans/create-pull-request@v7
         with:
           token: ${{ secrets.GITHUB_TOKEN }}
           commit-message: "Update documentation ${{ steps.timestamp.outputs.timestamp }}"

+ 5 - 5
.github/workflows/test.yml

@@ -15,7 +15,7 @@ jobs:
       - name: Setup Go
         uses: actions/setup-go@v5
         with:
-          go-version: 1.19
+          go-version-file: 'go.mod'
       - name: Build
         run: |
          env CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build main.go
@@ -29,7 +29,7 @@ jobs:
       - name: Setup go
         uses: actions/setup-go@v5
         with:
-          go-version: 1.19
+          go-version-file: 'go.mod'
       - name: Build
         run: |
           cd cli
@@ -46,7 +46,7 @@ jobs:
       - name: Setup Go
         uses: actions/setup-go@v5
         with:
-          go-version: 1.19
+          go-version-file: 'go.mod'
       - name: run tests
         run: |
           go vet ./...
@@ -66,9 +66,9 @@ jobs:
       - name: Setup Go
         uses: actions/setup-go@v5
         with:
-          go-version: 1.19
+          go-version-file: 'go.mod'
       - name: run static checks
         run: |
           sudo apt update
-          go install honnef.co/go/tools/cmd/staticcheck@v0.4.7
+          go install honnef.co/go/tools/cmd/staticcheck@latest
           { ~/go/bin/staticcheck  -tags=ee ./... ; }

+ 2 - 2
Dockerfile

@@ -1,12 +1,12 @@
 #first stage - builder
-FROM gravitl/go-builder AS builder
+FROM gravitl/go-builder:1.23.0 AS builder
 ARG tags 
 WORKDIR /app
 COPY . .
 
 RUN GOOS=linux CGO_ENABLED=1 go build -ldflags="-s -w " -tags ${tags} .
 # RUN go build -tags=ee . -o netmaker main.go
-FROM alpine:3.20.2
+FROM alpine:3.20.3
 
 # add a c lib
 # set the working directory

+ 1 - 1
Dockerfile-quick

@@ -1,5 +1,5 @@
 #first stage - builder
-FROM alpine:3.20.2
+FROM alpine:3.20.3
 ARG version 
 WORKDIR /app
 COPY ./netmaker /root/netmaker

+ 7 - 5
README.md

@@ -16,7 +16,7 @@
 
 <p align="center">
   <a href="https://github.com/gravitl/netmaker/releases">
-    <img src="https://img.shields.io/badge/Version-0.25.0-informational?style=flat-square" />
+    <img src="https://img.shields.io/badge/Version-0.26.0-informational?style=flat-square" />
   </a>
   <a href="https://hub.docker.com/r/gravitl/netmaker/tags">
     <img src="https://img.shields.io/docker/pulls/gravitl/netmaker?label=downloads" />
@@ -51,18 +51,20 @@
 
 If you're looking for a managed service, you can get started with just few clicks, visit [netmaker.io](https://account.netmaker.io) to create your netmaker server.  
 
-# Self-Hosted Quick Start  
+# Self-Hosted Open Source Quick Start  
 
 These are the instructions for deploying a Netmaker server on your own cloud VM as quickly as possible. For more detailed instructions, visit the [Install Docs](https://docs.netmaker.io/docs/server-installation/quick-install#quick-install-script).  
 
 1. Get a cloud VM with Ubuntu 22.04 and a public IP.
 2. Open ports 443, 80, 3479, 8089 and 51821-51830/udp on the VM firewall and in cloud security settings.
 3. (recommended) Prepare DNS - Set a wildcard subdomain in your DNS settings for Netmaker, e.g. *.netmaker.example.com, which points to your VM's public IP.
-4. Run the script: 
+4. Run the script to setup open source version of Netmaker: 
+
+`sudo wget -qO /root/nm-quick.sh https://raw.githubusercontent.com/gravitl/netmaker/master/scripts/nm-quick.sh && sudo chmod +x /root/nm-quick.sh && sudo /root/nm-quick.sh`
+
+**<pre>To Install Self-Hosted PRO Version - https://docs.netmaker.io/docs/server-installation/netmaker-professional-setup</pre>** 
 
-`sudo wget -qO /root/nm-quick.sh https://raw.githubusercontent.com/gravitl/netmaker/master/scripts/nm-quick.sh && sudo chmod +x /root/nm-quick.sh && sudo /root/nm-quick.sh`  
 
-This script by default installs PRO version with 14-day trial, check out these instructions for post trial period https://docs.netmaker.io/docs/server-installation/quick-install#after-the-trial-period-ends. It also gives you the option to use your own domain (recommended) or an auto-generated domain. 
 
 <p float="left" align="middle">
 <img src="https://raw.githubusercontent.com/gravitl/netmaker-docs/master/images/netmaker-github/readme.gif" />

+ 10 - 2
auth/host_session.go

@@ -222,7 +222,7 @@ func SessionHandler(conn *websocket.Conn) {
 		if err = conn.WriteMessage(messageType, reponseData); err != nil {
 			logger.Log(0, "error during message writing:", err.Error())
 		}
-		go CheckNetRegAndHostUpdate(netsToAdd[:], &result.Host, uuid.Nil)
+		go CheckNetRegAndHostUpdate(netsToAdd[:], &result.Host, uuid.Nil, []models.TagID{})
 	case <-timeout: // the read from req.answerCh has timed out
 		logger.Log(0, "timeout signal recv,exiting oauth socket conn")
 		break
@@ -236,7 +236,7 @@ func SessionHandler(conn *websocket.Conn) {
 }
 
 // CheckNetRegAndHostUpdate - run through networks and send a host update
-func CheckNetRegAndHostUpdate(networks []string, h *models.Host, relayNodeId uuid.UUID) {
+func CheckNetRegAndHostUpdate(networks []string, h *models.Host, relayNodeId uuid.UUID, tags []models.TagID) {
 	// publish host update through MQ
 	for i := range networks {
 		network := networks[i]
@@ -246,6 +246,14 @@ func CheckNetRegAndHostUpdate(networks []string, h *models.Host, relayNodeId uui
 				logger.Log(0, "failed to add host to network:", h.ID.String(), h.Name, network, err.Error())
 				continue
 			}
+			if len(tags) > 0 {
+				newNode.Tags = make(map[models.TagID]struct{})
+				for _, tagI := range tags {
+					newNode.Tags[tagI] = struct{}{}
+				}
+				logic.UpsertNode(newNode)
+			}
+
 			if relayNodeId != uuid.Nil && !newNode.IsRelayed {
 				// check if relay node exists and acting as relay
 				relaynode, err := logic.GetNodeByID(relayNodeId.String())

+ 1 - 1
cli/cmd/user/groups.go

@@ -56,7 +56,7 @@ var userGroupCreateCmd = &cobra.Command{
 	Short: "create user group",
 	Long:  `create user group`,
 	Run: func(cmd *cobra.Command, args []string) {
-		fmt.Println("CLI doesn't support creation of groups currently. Visit the dashboard to create one or refer to our api documentation https://docs.v2.netmaker.io/reference")
+		fmt.Println("CLI doesn't support creation of groups currently. Visit the dashboard to create one or refer to our api documentation https://docs.netmaker.io/api")
 	},
 }
 

+ 1 - 1
cli/cmd/user/roles.go

@@ -58,7 +58,7 @@ var userRoleCreateCmd = &cobra.Command{
 	Short: "create user role",
 	Long:  `create user role`,
 	Run: func(cmd *cobra.Command, args []string) {
-		fmt.Println("CLI doesn't support creation of roles currently. Visit the dashboard to create one or refer to our api documentation https://docs.v2.netmaker.io/reference")
+		fmt.Println("CLI doesn't support creation of roles currently. Visit the dashboard to create one or refer to our api documentation https://docs.netmaker.io/api")
 	},
 }
 

+ 1 - 1
compose/docker-compose.netclient.yml

@@ -3,7 +3,7 @@ version: "3.4"
 services:
   netclient:
     container_name: netclient
-    image: 'gravitl/netclient:v0.25.0'
+    image: 'gravitl/netclient:v0.26.0'
     hostname: netmaker-1
     network_mode: host
     restart: on-failure

+ 1 - 1
compose/docker-compose.yml

@@ -41,7 +41,7 @@ services:
     restart: always
 
   caddy:
-    image: caddy:2.6.2
+    image: caddy:2.8.4
     container_name: caddy
     env_file: ./netmaker.env
     restart: unless-stopped

+ 2 - 0
config/config.go

@@ -100,6 +100,8 @@ type ServerConfig struct {
 	SmtpHost                   string        `json:"smtp_host"`
 	SmtpPort                   int           `json:"smtp_port"`
 	MetricInterval             string        `yaml:"metric_interval"`
+	ManageDNS                  bool          `yaml:"manage_dns"`
+	DefaultDomain              string        `yaml:"default_domain"`
 }
 
 // SQLConfig - Generic SQL Config

+ 230 - 0
controllers/acls.go

@@ -0,0 +1,230 @@
+package controller
+
+import (
+	"encoding/json"
+	"errors"
+	"net/http"
+	"net/url"
+	"time"
+
+	"github.com/google/uuid"
+	"github.com/gorilla/mux"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/mq"
+)
+
+func aclHandlers(r *mux.Router) {
+	r.HandleFunc("/api/v1/acls", logic.SecurityCheck(true, http.HandlerFunc(getAcls))).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/acls/policy_types", logic.SecurityCheck(true, http.HandlerFunc(aclPolicyTypes))).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/acls", logic.SecurityCheck(true, http.HandlerFunc(createAcl))).
+		Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/acls", logic.SecurityCheck(true, http.HandlerFunc(updateAcl))).
+		Methods(http.MethodPut)
+	r.HandleFunc("/api/v1/acls", logic.SecurityCheck(true, http.HandlerFunc(deleteAcl))).
+		Methods(http.MethodDelete)
+	r.HandleFunc("/api/v1/acls/debug", logic.SecurityCheck(true, http.HandlerFunc(aclDebug))).
+		Methods(http.MethodGet)
+}
+
+// @Summary     List Acl Policy types
+// @Router      /api/v1/acls/policy_types [get]
+// @Tags        ACL
+// @Accept      json
+// @Success     200 {array} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func aclPolicyTypes(w http.ResponseWriter, r *http.Request) {
+	resp := models.AclPolicyTypes{
+		RuleTypes: []models.AclPolicyType{
+			models.DevicePolicy,
+			models.UserPolicy,
+		},
+		SrcGroupTypes: []models.AclGroupType{
+			models.UserAclID,
+			models.UserGroupAclID,
+			models.DeviceAclID,
+		},
+		DstGroupTypes: []models.AclGroupType{
+			models.DeviceAclID,
+			// models.NetmakerIPAclID,
+			// models.NetmakerSubNetRangeAClID,
+		},
+	}
+	logic.ReturnSuccessResponseWithJson(w, r, resp, "fetched acls types")
+}
+
+func aclDebug(w http.ResponseWriter, r *http.Request) {
+	nodeID, _ := url.QueryUnescape(r.URL.Query().Get("node"))
+	peerID, _ := url.QueryUnescape(r.URL.Query().Get("peer"))
+	node, err := logic.GetNodeByID(nodeID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	peer, err := logic.GetNodeByID(peerID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	allowed := logic.IsNodeAllowedToCommunicate(node, peer)
+	logic.ReturnSuccessResponseWithJson(w, r, allowed, "fetched all acls in the network ")
+}
+
+// @Summary     List Acls in a network
+// @Router      /api/v1/acls [get]
+// @Tags        ACL
+// @Accept      json
+// @Success     200 {array} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func getAcls(w http.ResponseWriter, r *http.Request) {
+	netID := r.URL.Query().Get("network")
+	if netID == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("network id param is missing"), "badrequest"))
+		return
+	}
+	// check if network exists
+	_, err := logic.GetNetwork(netID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	acls, err := logic.ListAcls(models.NetworkID(netID))
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "failed to get all network acl entries: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	logic.SortAclEntrys(acls[:])
+	logic.ReturnSuccessResponseWithJson(w, r, acls, "fetched all acls in the network "+netID)
+}
+
+// @Summary     Create Acl
+// @Router      /api/v1/acls [post]
+// @Tags        ACL
+// @Accept      json
+// @Success     200 {array} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func createAcl(w http.ResponseWriter, r *http.Request) {
+	var req models.Acl
+	err := json.NewDecoder(r.Body).Decode(&req)
+	if err != nil {
+		logger.Log(0, "error decoding request body: ",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	user, err := logic.GetUser(r.Header.Get("user"))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	err = logic.ValidateCreateAclReq(req)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	acl := req
+	acl.ID = uuid.New().String()
+	acl.CreatedBy = user.UserName
+	acl.CreatedAt = time.Now().UTC()
+	acl.Default = false
+	if acl.RuleType == models.DevicePolicy {
+		acl.AllowedDirection = models.TrafficDirectionBi
+	} else {
+		acl.AllowedDirection = models.TrafficDirectionUni
+	}
+	// validate create acl policy
+	if !logic.IsAclPolicyValid(acl) {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("invalid policy"), "badrequest"))
+		return
+	}
+	err = logic.InsertAcl(acl)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	acl, err = logic.GetAcl(acl.ID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	go mq.PublishPeerUpdate(false)
+	logic.ReturnSuccessResponseWithJson(w, r, acl, "created acl successfully")
+}
+
+// @Summary     Update Acl
+// @Router      /api/v1/acls [put]
+// @Tags        ACL
+// @Accept      json
+// @Success     200 {array} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func updateAcl(w http.ResponseWriter, r *http.Request) {
+	var updateAcl models.UpdateAclRequest
+	err := json.NewDecoder(r.Body).Decode(&updateAcl)
+	if err != nil {
+		logger.Log(0, "error decoding request body: ",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	acl, err := logic.GetAcl(updateAcl.ID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	if !logic.IsAclPolicyValid(updateAcl.Acl) {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("invalid policy"), "badrequest"))
+		return
+	}
+	if updateAcl.Acl.NetworkID != acl.NetworkID {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("invalid policy, network id mismatch"), "badrequest"))
+		return
+	}
+	if !acl.Default && updateAcl.NewName != "" {
+		//check if policy exists with same name
+		updateAcl.Acl.Name = updateAcl.NewName
+	}
+	err = logic.UpdateAcl(updateAcl.Acl, acl)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	go mq.PublishPeerUpdate(false)
+	logic.ReturnSuccessResponse(w, r, "updated acl "+acl.Name)
+}
+
+// @Summary     Delete Acl
+// @Router      /api/v1/acls [delete]
+// @Tags        ACL
+// @Accept      json
+// @Success     200 {array} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func deleteAcl(w http.ResponseWriter, r *http.Request) {
+	aclID, _ := url.QueryUnescape(r.URL.Query().Get("acl_id"))
+	if aclID == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("acl id is required"), "badrequest"))
+		return
+	}
+	acl, err := logic.GetAcl(aclID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	if acl.Default {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot delete default policy"), "badrequest"))
+		return
+	}
+	err = logic.DeleteAcl(acl)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r,
+			logic.FormatError(errors.New("cannot delete default policy"), "internal"))
+		return
+	}
+	go mq.PublishPeerUpdate(false)
+	logic.ReturnSuccessResponse(w, r, "deleted acl "+acl.Name)
+}

+ 2 - 0
controllers/controller.go

@@ -34,6 +34,8 @@ var HttpHandlers = []interface{}{
 	loggerHandlers,
 	hostHandlers,
 	enrollmentKeyHandlers,
+	tagHandlers,
+	aclHandlers,
 	legacyHandlers,
 }
 

+ 48 - 0
controllers/dns.go

@@ -11,6 +11,7 @@ import (
 	"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"
 )
 
@@ -24,6 +25,8 @@ func dnsHandlers(r *mux.Router) {
 		Methods(http.MethodGet)
 	r.HandleFunc("/api/dns/adm/{network}", logic.SecurityCheck(true, http.HandlerFunc(getDNS))).
 		Methods(http.MethodGet)
+	r.HandleFunc("/api/dns/adm/{network}/sync", logic.SecurityCheck(true, http.HandlerFunc(syncDNS))).
+		Methods(http.MethodPost)
 	r.HandleFunc("/api/dns/{network}", logic.SecurityCheck(true, http.HandlerFunc(createDNS))).
 		Methods(http.MethodPost)
 	r.HandleFunc("/api/dns/adm/pushdns", logic.SecurityCheck(true, http.HandlerFunc(pushDNS))).
@@ -147,6 +150,7 @@ func createDNS(w http.ResponseWriter, r *http.Request) {
 
 	var entry models.DNSEntry
 	var params = mux.Vars(r)
+	netID := params["network"]
 
 	_ = json.NewDecoder(r.Body).Decode(&entry)
 	entry.Network = params["network"]
@@ -176,6 +180,10 @@ func createDNS(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
+	if servercfg.GetManageDNS() {
+		mq.SendDNSSyncByNetwork(netID)
+	}
+
 	logger.Log(1, "new DNS record added:", entry.Name)
 	logger.Log(2, r.Header.Get("user"),
 		fmt.Sprintf("DNS entry is set: %+v", entry))
@@ -197,6 +205,7 @@ func deleteDNS(w http.ResponseWriter, r *http.Request) {
 
 	// get params
 	var params = mux.Vars(r)
+	netID := params["network"]
 	entrytext := params["domain"] + "." + params["network"]
 	err := logic.DeleteDNS(params["domain"], params["network"])
 
@@ -216,6 +225,10 @@ func deleteDNS(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
+	if servercfg.GetManageDNS() {
+		mq.SendDNSSyncByNetwork(netID)
+	}
+
 	json.NewEncoder(w).Encode(entrytext + " deleted.")
 
 }
@@ -264,3 +277,38 @@ func pushDNS(w http.ResponseWriter, r *http.Request) {
 	logger.Log(1, r.Header.Get("user"), "pushed DNS updates to nameserver")
 	json.NewEncoder(w).Encode("DNS Pushed to CoreDNS")
 }
+
+// @Summary     Sync DNS entries for a given network
+// @Router      /api/dns/adm/{network}/sync [post]
+// @Tags        DNS
+// @Accept      json
+// @Success     200 {string} string "DNS Sync completed successfully"
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func syncDNS(w http.ResponseWriter, r *http.Request) {
+	// Set header
+	w.Header().Set("Content-Type", "application/json")
+	if !servercfg.GetManageDNS() {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("manage DNS is set to false"), "badrequest"),
+		)
+		return
+	}
+	var params = mux.Vars(r)
+	netID := params["network"]
+	k, err := logic.GetDNS(netID)
+	if err == nil && len(k) > 0 {
+		err = mq.PushSyncDNS(k)
+	}
+
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"),
+			fmt.Sprintf("Failed to Sync DNS entries to network %s: %v", netID, err))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	logger.Log(1, r.Header.Get("user"), "DNS Sync complelted successfully")
+	json.NewEncoder(w).Encode("DNS Sync completed successfully")
+}

+ 6 - 3
controllers/enrollmentkeys.go

@@ -72,7 +72,7 @@ func getEnrollmentKeys(w http.ResponseWriter, r *http.Request) {
 func deleteEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 	params := mux.Vars(r)
 	keyID := params["keyID"]
-	err := logic.DeleteEnrollmentKey(keyID)
+	err := logic.DeleteEnrollmentKey(keyID, false)
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "failed to remove enrollment key: ", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
@@ -156,8 +156,10 @@ func createEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 		newTime,
 		enrollmentKeyBody.Networks,
 		enrollmentKeyBody.Tags,
+		enrollmentKeyBody.Groups,
 		enrollmentKeyBody.Unlimited,
 		relayId,
+		false,
 	)
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "failed to create enrollment key:", err.Error())
@@ -206,7 +208,7 @@ func updateEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
-	newEnrollmentKey, err := logic.UpdateEnrollmentKey(keyId, relayId)
+	newEnrollmentKey, err := logic.UpdateEnrollmentKey(keyId, relayId, enrollmentKeyBody.Groups)
 	if err != nil {
 		slog.Error("failed to update enrollment key", "error", err)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
@@ -307,6 +309,7 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 				return
 			}
 		}
+
 		if err = logic.CreateHost(&newHost); err != nil {
 			logger.Log(
 				0,
@@ -355,5 +358,5 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(&response)
 	// notify host of changes, peer and node updates
-	go auth.CheckNetRegAndHostUpdate(enrollmentKey.Networks, &newHost, enrollmentKey.Relay)
+	go auth.CheckNetRegAndHostUpdate(enrollmentKey.Networks, &newHost, enrollmentKey.Relay, enrollmentKey.Groups)
 }

+ 38 - 4
controllers/ext_client.go

@@ -244,6 +244,9 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) {
 	if network.DefaultKeepalive != 0 {
 		keepalive = "PersistentKeepalive = " + strconv.Itoa(int(network.DefaultKeepalive))
 	}
+	if gwnode.IngressPersistentKeepalive != 0 {
+		keepalive = "PersistentKeepalive = " + strconv.Itoa(int(gwnode.IngressPersistentKeepalive))
+	}
 
 	gwendpoint := ""
 	if preferredIp == "" {
@@ -284,11 +287,30 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) {
 	} else if gwnode.IngressDNS != "" {
 		defaultDNS = "DNS = " + gwnode.IngressDNS
 	}
+	// if servercfg.GetManageDNS() {
+	// 	if gwnode.Address6.IP != nil {
+	// 		if defaultDNS == "" {
+	// 			defaultDNS = "DNS = " + gwnode.Address6.IP.String()
+	// 		} else {
+	// 			defaultDNS = defaultDNS + ", " + gwnode.Address6.IP.String()
+	// 		}
+	// 	}
+	// 	if gwnode.Address.IP != nil {
+	// 		if defaultDNS == "" {
+	// 			defaultDNS = "DNS = " + gwnode.Address.IP.String()
+	// 		} else {
+	// 			defaultDNS = defaultDNS + ", " + gwnode.Address.IP.String()
+	// 		}
+	// 	}
+	// }
 
 	defaultMTU := 1420
 	if host.MTU != 0 {
 		defaultMTU = host.MTU
 	}
+	if gwnode.IngressMTU != 0 {
+		defaultMTU = int(gwnode.IngressMTU)
+	}
 
 	postUp := strings.Builder{}
 	if client.PostUp != "" && params["type"] != "qr" {
@@ -446,13 +468,14 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 	extclient.OwnerID = userName
 	extclient.RemoteAccessClientID = customExtClient.RemoteAccessClientID
 	extclient.IngressGatewayID = nodeid
-
+	extclient.Network = node.Network
+	extclient.Tags = make(map[models.TagID]struct{})
+	extclient.Tags[models.TagID(fmt.Sprintf("%s.%s", extclient.Network,
+		models.RemoteAccessTagName))] = struct{}{}
 	// set extclient dns to ingressdns if extclient dns is not explicitly set
 	if (extclient.DNS == "") && (node.IngressDNS != "") {
 		extclient.DNS = node.IngressDNS
 	}
-
-	extclient.Network = node.Network
 	host, err := logic.GetHost(node.HostID.String())
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"),
@@ -531,6 +554,7 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 	var update models.CustomExtClient
 	//var oldExtClient models.ExtClient
 	var sendPeerUpdate bool
+	var replacePeers bool
 	err := json.NewDecoder(r.Body).Decode(&update)
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "error decoding request body: ",
@@ -588,6 +612,11 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 	if update.Enabled != oldExtClient.Enabled {
 		sendPeerUpdate = true
 	}
+	if update.PublicKey != oldExtClient.PublicKey {
+		//remove old peer entry
+		sendPeerUpdate = true
+		replacePeers = true
+	}
 	newclient := logic.UpdateExtClient(&oldExtClient, &update)
 	if err := logic.DeleteExtClient(oldExtClient.Network, oldExtClient.ClientID); err != nil {
 		slog.Error(
@@ -627,6 +656,11 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 		if changedID && servercfg.IsDNSMode() {
 			logic.SetDNS()
 		}
+		if replacePeers {
+			if err := mq.PublishDeletedClientPeerUpdate(&oldExtClient); err != nil {
+				slog.Error("error deleting old ext peers", "error", err.Error())
+			}
+		}
 		if sendPeerUpdate { // need to send a peer update to the ingress node as enablement of one of it's clients has changed
 			ingressNode, err := logic.GetNodeByID(newclient.IngressGatewayID)
 			if err == nil {
@@ -735,7 +769,7 @@ func validateCustomExtClient(customExtClient *models.CustomExtClient, checkID bo
 	//validate clientid
 	if customExtClient.ClientID != "" {
 		if err := isValid(customExtClient.ClientID, checkID); err != nil {
-			return fmt.Errorf("client validatation: %v", err)
+			return fmt.Errorf("client validation: %v", err)
 		}
 	}
 	//extclient.ClientID = customExtClient.ClientID

+ 9 - 14
controllers/hosts.go

@@ -167,6 +167,8 @@ func pull(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+
+	sendPeerUpdate := false
 	for _, nodeID := range host.Nodes {
 		node, err := logic.GetNodeByID(nodeID)
 		if err != nil {
@@ -174,7 +176,13 @@ func pull(w http.ResponseWriter, r *http.Request) {
 			continue
 		}
 		if node.FailedOverBy != uuid.Nil {
-			go logic.ResetFailedOverPeer(&node)
+			logic.ResetFailedOverPeer(&node)
+			sendPeerUpdate = true
+		}
+	}
+	if sendPeerUpdate {
+		if err := mq.PublishPeerUpdate(true); err != nil {
+			logger.Log(0, "fail to publish peer update: ", err.Error())
 		}
 	}
 	allNodes, err := logic.GetAllNodes()
@@ -245,19 +253,6 @@ 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())

+ 10 - 1
controllers/middleware.go

@@ -27,13 +27,20 @@ func userMiddleWare(handler http.Handler) http.Handler {
 		r.Header.Set("TARGET_RSRC", "")
 		r.Header.Set("RSRC_TYPE", "")
 		r.Header.Set("TARGET_RSRC_ID", "")
+		r.Header.Set("RAC", "")
 		r.Header.Set("NET_ID", params["network"])
+		if r.URL.Query().Get("network") != "" {
+			r.Header.Set("NET_ID", r.URL.Query().Get("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, "rac") {
+			r.Header.Set("RAC", "true")
+		}
 		if strings.Contains(route, "users") {
 			r.Header.Set("TARGET_RSRC", models.UserRsrc.String())
 		}
@@ -53,6 +60,9 @@ func userMiddleWare(handler http.Handler) http.Handler {
 		if strings.Contains(route, "acls") {
 			r.Header.Set("TARGET_RSRC", models.AclRsrc.String())
 		}
+		if strings.Contains(route, "tags") {
+			r.Header.Set("TARGET_RSRC", models.TagRsrc.String())
+		}
 		if strings.Contains(route, "extclients") {
 			r.Header.Set("TARGET_RSRC", models.ExtClientsRsrc.String())
 		}
@@ -101,7 +111,6 @@ func userMiddleWare(handler http.Handler) http.Handler {
 			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"))
 		handler.ServeHTTP(w, r)
 	})

+ 47 - 1
controllers/network.go

@@ -24,6 +24,8 @@ import (
 func networkHandlers(r *mux.Router) {
 	r.HandleFunc("/api/networks", logic.SecurityCheck(true, http.HandlerFunc(getNetworks))).
 		Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/networks/stats", logic.SecurityCheck(true, http.HandlerFunc(getNetworksStats))).
+		Methods(http.MethodGet)
 	r.HandleFunc("/api/networks", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceNetworks, http.HandlerFunc(createNetwork)))).
 		Methods(http.MethodPost)
 	r.HandleFunc("/api/networks/{networkname}", logic.SecurityCheck(true, http.HandlerFunc(getNetwork))).
@@ -74,6 +76,48 @@ func getNetworks(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(allnetworks)
 }
 
+// @Summary     Lists all networks with stats
+// @Router      /api/v1/networks/stats [get]
+// @Tags        Networks
+// @Security    oauth
+// @Produce     json
+// @Success     200 {object} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func getNetworksStats(w http.ResponseWriter, r *http.Request) {
+
+	var err error
+	allnetworks, err := logic.GetNetworks()
+	if err != nil && !database.IsEmptyRecord(err) {
+		slog.Error("failed to fetch networks", "error", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	if r.Header.Get("ismaster") != "yes" {
+		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)
+	}
+	allNodes, err := logic.GetAllNodes()
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	netstats := []models.NetworkStatResp{}
+	logic.SortNetworks(allnetworks[:])
+	for _, network := range allnetworks {
+		netstats = append(netstats, models.NetworkStatResp{
+			Network: network,
+			Hosts:   len(logic.GetNetworkNodesMemory(allNodes, network.NetID)),
+		})
+	}
+	logger.Log(2, r.Header.Get("user"), "fetched networks.")
+	logic.ReturnSuccessResponseWithJson(w, r, netstats, "fetched networks with stats")
+}
+
 // @Summary     Get a network
 // @Router      /api/networks/{networkname} [get]
 // @Tags        Networks
@@ -412,6 +456,7 @@ func deleteNetwork(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	go logic.DeleteNetworkRoles(network)
+	go logic.DeleteDefaultNetworkPolicies(models.NetworkID(network))
 	//delete network from allocated ip map
 	go logic.RemoveNetworkFromAllocatedIpMap(network)
 
@@ -487,7 +532,8 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	logic.CreateDefaultNetworkRolesAndGroups(models.NetworkID(network.NetID))
-
+	logic.CreateDefaultAclNetworkPolicies(models.NetworkID(network.NetID))
+	logic.CreateDefaultTags(models.NetworkID(network.NetID))
 	//add new network to allocated ip map
 	go logic.AddNetworkToAllocatedIpMap(network.NetID)
 

+ 6 - 0
controllers/node.go

@@ -326,6 +326,7 @@ func getNetworkNodes(w http.ResponseWriter, r *http.Request) {
 	if len(filteredNodes) > 0 {
 		nodes = filteredNodes
 	}
+	nodes = logic.AddStaticNodestoList(nodes)
 
 	// returns all the nodes in JSON/API format
 	apiNodes := logic.GetAllNodesAPI(nodes[:])
@@ -363,7 +364,9 @@ func getAllNodes(w http.ResponseWriter, r *http.Request) {
 		if !userPlatformRole.FullAccess {
 			nodes = logic.GetFilteredNodesByUserAccess(*user, nodes)
 		}
+
 	}
+	nodes = logic.AddStaticNodestoList(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")
@@ -587,6 +590,7 @@ func createIngressGateway(w http.ResponseWriter, r *http.Request) {
 		if err := mq.NodeUpdate(&node); err != nil {
 			slog.Error("error publishing node update to node", "node", node.ID, "error", err)
 		}
+		mq.PublishPeerUpdate(false)
 	}()
 }
 
@@ -631,6 +635,7 @@ func deleteIngressGateway(w http.ResponseWriter, r *http.Request) {
 				if err := mq.PublishSingleHostPeerUpdate(host, allNodes, nil, removedClients[:], false, nil); err != nil {
 					slog.Error("publishSingleHostUpdate", "host", host.Name, "error", err)
 				}
+				mq.PublishPeerUpdate(false)
 				if err := mq.NodeUpdate(&node); err != nil {
 					slog.Error(
 						"error publishing node update to node",
@@ -746,6 +751,7 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 				logger.Log(0, "error during node ACL update for node", newNode.ID.String())
 			}
 		}
+		mq.PublishPeerUpdate(false)
 		if servercfg.IsDNSMode() {
 			logic.SetDNS()
 		}

+ 4 - 4
controllers/regex.go

@@ -6,10 +6,10 @@ import (
 )
 
 var (
-	errInvalidExtClientPubKey  = errors.New("incorrect ext client public key")
-	errInvalidExtClientID      = errors.New("ext client ID must be alphanumderic and/or dashes and less that 15 chars")
-	errInvalidExtClientExtraIP = errors.New("ext client extra ip must be a valid cidr")
-	errInvalidExtClientDNS     = errors.New("ext client dns must be a valid ip address")
+	errInvalidExtClientPubKey  = errors.New("incorrect client public key")
+	errInvalidExtClientID      = errors.New("node name must be alphanumderic and/or dashes and less that 15 chars")
+	errInvalidExtClientExtraIP = errors.New("client extra ip must be a valid cidr")
+	errInvalidExtClientDNS     = errors.New("client dns must be a valid ip address")
 	errDuplicateExtClientName  = errors.New("duplicate client name")
 )
 

+ 18 - 0
controllers/server.go

@@ -3,6 +3,7 @@ package controller
 import (
 	"encoding/json"
 	"net/http"
+	"os"
 	"strings"
 	"syscall"
 	"time"
@@ -17,6 +18,8 @@ import (
 	"github.com/gravitl/netmaker/servercfg"
 )
 
+var cpuProfileLog *os.File
+
 func serverHandlers(r *mux.Router) {
 	// r.HandleFunc("/api/server/addnetwork/{network}", securityCheckServer(true, http.HandlerFunc(addNetwork))).Methods(http.MethodPost)
 	r.HandleFunc(
@@ -43,6 +46,21 @@ func serverHandlers(r *mux.Router) {
 	r.HandleFunc("/api/server/status", getStatus).Methods(http.MethodGet)
 	r.HandleFunc("/api/server/usage", logic.SecurityCheck(false, http.HandlerFunc(getUsage))).
 		Methods(http.MethodGet)
+	r.HandleFunc("/api/server/cpu_profile", logic.SecurityCheck(false, http.HandlerFunc(cpuProfile))).
+		Methods(http.MethodPost)
+}
+
+func cpuProfile(w http.ResponseWriter, r *http.Request) {
+	start := r.URL.Query().Get("action") == "start"
+	if start {
+		os.Remove("/root/data/cpu.prof")
+		cpuProfileLog = logic.StartCPUProfiling()
+	} else {
+		if cpuProfileLog != nil {
+			logic.StopCPUProfiling(cpuProfileLog)
+			cpuProfileLog = nil
+		}
+	}
 }
 
 func getUsage(w http.ResponseWriter, _ *http.Request) {

+ 231 - 0
controllers/tags.go

@@ -0,0 +1,231 @@
+package controller
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+
+	"github.com/gorilla/mux"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/mq"
+)
+
+func tagHandlers(r *mux.Router) {
+	r.HandleFunc("/api/v1/tags", logic.SecurityCheck(true, http.HandlerFunc(getTags))).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/tags", logic.SecurityCheck(true, http.HandlerFunc(createTag))).
+		Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/tags", logic.SecurityCheck(true, http.HandlerFunc(updateTag))).
+		Methods(http.MethodPut)
+	r.HandleFunc("/api/v1/tags", logic.SecurityCheck(true, http.HandlerFunc(deleteTag))).
+		Methods(http.MethodDelete)
+
+}
+
+// @Summary     List Tags in a network
+// @Router      /api/v1/tags [get]
+// @Tags        TAG
+// @Accept      json
+// @Success     200 {array} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func getTags(w http.ResponseWriter, r *http.Request) {
+	netID, _ := url.QueryUnescape(r.URL.Query().Get("network"))
+	if netID == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("network id param is missing"), "badrequest"))
+		return
+	}
+	// check if network exists
+	_, err := logic.GetNetwork(netID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	tags, err := logic.ListTagsWithNodes(models.NetworkID(netID))
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "failed to get all network tag entries: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	logic.SortTagEntrys(tags[:])
+	logic.ReturnSuccessResponseWithJson(w, r, tags, "fetched all tags in the network "+netID)
+}
+
+// @Summary     Create Tag
+// @Router      /api/v1/tags [post]
+// @Tags        TAG
+// @Accept      json
+// @Success     200 {array} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func createTag(w http.ResponseWriter, r *http.Request) {
+	var req models.CreateTagReq
+	err := json.NewDecoder(r.Body).Decode(&req)
+	if err != nil {
+		logger.Log(0, "error decoding request body: ",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	user, err := logic.GetUser(r.Header.Get("user"))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	// check if tag network exists
+	_, err = logic.GetNetwork(req.Network.String())
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to get network details for "+req.Network.String()), "badrequest"))
+		return
+	}
+	// check if tag exists
+	tag := models.Tag{
+		ID:        models.TagID(fmt.Sprintf("%s.%s", req.Network, req.TagName)),
+		TagName:   req.TagName,
+		Network:   req.Network,
+		CreatedBy: user.UserName,
+		CreatedAt: time.Now(),
+	}
+	_, err = logic.GetTag(tag.ID)
+	if err == nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("tag with id %s exists already", tag.TagName), "badrequest"))
+		return
+	}
+	// validate name
+	err = logic.CheckIDSyntax(tag.TagName)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	err = logic.InsertTag(tag)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	go func() {
+		for _, node := range req.TaggedNodes {
+			if node.IsStatic {
+				extclient, err := logic.GetExtClient(node.StaticNode.ClientID, node.StaticNode.Network)
+				if err == nil && extclient.RemoteAccessClientID == "" {
+					if extclient.Tags == nil {
+						extclient.Tags = make(map[models.TagID]struct{})
+					}
+					extclient.Tags[tag.ID] = struct{}{}
+					logic.SaveExtClient(&extclient)
+				}
+				continue
+			}
+			node, err := logic.GetNodeByID(node.ID)
+			if err != nil {
+				continue
+			}
+			if node.Tags == nil {
+				node.Tags = make(map[models.TagID]struct{})
+			}
+			node.Tags[tag.ID] = struct{}{}
+			logic.UpsertNode(&node)
+		}
+	}()
+	go mq.PublishPeerUpdate(false)
+
+	var res models.TagListRespNodes = models.TagListRespNodes{
+		Tag:         tag,
+		UsedByCnt:   len(req.TaggedNodes),
+		TaggedNodes: req.TaggedNodes,
+	}
+
+	logic.ReturnSuccessResponseWithJson(w, r, res, "created tag successfully")
+}
+
+// @Summary     Update Tag
+// @Router      /api/v1/tags [put]
+// @Tags        TAG
+// @Accept      json
+// @Success     200 {array} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func updateTag(w http.ResponseWriter, r *http.Request) {
+	var updateTag models.UpdateTagReq
+	err := json.NewDecoder(r.Body).Decode(&updateTag)
+	if err != nil {
+		logger.Log(0, "error decoding request body: ",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	tag, err := logic.GetTag(updateTag.ID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	updateTag.NewName = strings.TrimSpace(updateTag.NewName)
+	var newID models.TagID
+	if updateTag.NewName != "" {
+		// validate name
+		err = logic.CheckIDSyntax(updateTag.NewName)
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+			return
+		}
+		newID = models.TagID(fmt.Sprintf("%s.%s", tag.Network, updateTag.NewName))
+		tag.ID = newID
+		tag.TagName = updateTag.NewName
+		err = logic.InsertTag(tag)
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+			return
+		}
+		// delete old Tag entry
+		logic.DeleteTag(updateTag.ID, false)
+	}
+	go func() {
+		logic.UpdateTag(updateTag, newID)
+		if updateTag.NewName != "" {
+			logic.UpdateDeviceTag(updateTag.ID, newID, tag.Network)
+		}
+		mq.PublishPeerUpdate(false)
+	}()
+
+	var res models.TagListRespNodes = models.TagListRespNodes{
+		Tag:         tag,
+		UsedByCnt:   len(updateTag.TaggedNodes),
+		TaggedNodes: updateTag.TaggedNodes,
+	}
+
+	logic.ReturnSuccessResponseWithJson(w, r, res, "updated tags")
+}
+
+// @Summary     Delete Tag
+// @Router      /api/v1/tags [delete]
+// @Tags        TAG
+// @Accept      json
+// @Success     200 {array} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func deleteTag(w http.ResponseWriter, r *http.Request) {
+	tagID, _ := url.QueryUnescape(r.URL.Query().Get("tag_id"))
+	if tagID == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("role is required"), "badrequest"))
+		return
+	}
+	tag, err := logic.GetTag(models.TagID(tagID))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	err = logic.DeleteTag(models.TagID(tagID), true)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+
+	go func() {
+		logic.RemoveDeviceTagFromAclPolicies(tag.ID, tag.Network)
+		logic.RemoveTagFromEnrollmentKeys(tag.ID)
+		mq.PublishPeerUpdate(false)
+	}()
+	logic.ReturnSuccessResponse(w, r, "deleted tag "+tagID)
+}

+ 3 - 0
controllers/user.go

@@ -451,6 +451,7 @@ func createUser(w http.ResponseWriter, r *http.Request) {
 	}
 	logic.DeleteUserInvite(user.UserName)
 	logic.DeletePendingUser(user.UserName)
+	go mq.PublishPeerUpdate(false)
 	slog.Info("user was created", "username", user.UserName)
 	json.NewEncoder(w).Encode(logic.ToReturnUser(user))
 }
@@ -590,6 +591,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
+	go mq.PublishPeerUpdate(false)
 	logger.Log(1, username, "was updated")
 	json.NewEncoder(w).Encode(logic.ToReturnUser(*user))
 }
@@ -692,6 +694,7 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
 				}
 			}
 		}
+		mq.PublishPeerUpdate(false)
 		if servercfg.IsDNSMode() {
 			logic.SetDNS()
 		}

+ 6 - 0
database/database.go

@@ -47,6 +47,8 @@ const (
 	GENERATED_TABLE_NAME = "generated"
 	// NODE_ACLS_TABLE_NAME - stores the node ACL rules
 	NODE_ACLS_TABLE_NAME = "nodeacls"
+	// ACLS_TABLE_NAME - table for acls v2
+	ACLS_TABLE_NAME = "acls"
 	// SSO_STATE_CACHE - holds sso session information for OAuth2 sign-ins
 	SSO_STATE_CACHE = "ssostatecache"
 	// METRICS_TABLE_NAME - stores network metrics
@@ -67,6 +69,8 @@ const (
 	PENDING_USERS_TABLE_NAME = "pending_users"
 	// USER_INVITES - table for user invites
 	USER_INVITES_TABLE_NAME = "user_invites"
+	// TAG_TABLE_NAME - table for tags
+	TAG_TABLE_NAME = "tags"
 	// == ERROR CONSTS ==
 	// NO_RECORD - no singular result found
 	NO_RECORD = "no result found"
@@ -152,6 +156,8 @@ func createTables() {
 	CreateTable(PENDING_USERS_TABLE_NAME)
 	CreateTable(USER_PERMISSIONS_TABLE_NAME)
 	CreateTable(USER_INVITES_TABLE_NAME)
+	CreateTable(TAG_TABLE_NAME)
+	CreateTable(ACLS_TABLE_NAME)
 }
 
 func CreateTable(tableName string) error {

+ 6 - 2
docker/Caddyfile

@@ -25,6 +25,10 @@ https://api.{$NM_DOMAIN} {
 }
 
 # MQ
-wss://broker.{$NM_DOMAIN} {
-	reverse_proxy ws://mq:8883 # For EMQX websockets use `reverse_proxy ws://mq:8083`
+broker.{$NM_DOMAIN} {
+	@ws {
+			header Connection *Upgrade*
+			header Upgrade websocket
+		}
+	reverse_proxy @ws mq:8883   # For EMQX websockets use `reverse_proxy @ws mq:8083`
 }

+ 6 - 2
docker/Caddyfile-pro

@@ -40,6 +40,10 @@ https://api.{$NM_DOMAIN} {
 }
 
 # MQ
-wss://broker.{$NM_DOMAIN} {
-	reverse_proxy ws://mq:8883
+broker.{$NM_DOMAIN} {
+	@ws {
+			header Connection *Upgrade*
+			header Upgrade websocket
+		}
+	reverse_proxy @ws mq:8883
 }

+ 1 - 1
docker/Dockerfile-go-builder

@@ -1,4 +1,4 @@
-FROM golang:1.20.13-alpine3.19
+FROM golang:1.23.0-alpine3.20
 ARG version 
 RUN apk add build-base
 WORKDIR /app

+ 1 - 1
docs/Authentication.md

@@ -7,4 +7,4 @@ Call the api/users/adm/authenticate endpoint (see documentation below for detail
 
 Note: While a MasterKey exists (configurable via env var or config file), it should be considered a backup option, used only when server access is lost. By default, this key is "secret key," but it's crucial to change this and keep it secure in your instance.
 
-For more information on configuration and security best practices, refer to the [Netmaker documentation](https://docs.netmaker.org/index.html).
+For more information on configuration and security best practices, refer to the [Netmaker documentation](https://docs.netmaker.io/).

+ 12 - 12
go.mod

@@ -1,26 +1,26 @@
 module github.com/gravitl/netmaker
 
-go 1.19
+go 1.23
 
 require (
 	github.com/eclipse/paho.mqtt.golang v1.4.3
-	github.com/go-playground/validator/v10 v10.22.0
+	github.com/go-playground/validator/v10 v10.22.1
 	github.com/golang-jwt/jwt/v4 v4.5.0
 	github.com/google/uuid v1.6.0
 	github.com/gorilla/handlers v1.5.2
 	github.com/gorilla/mux v1.8.1
 	github.com/lib/pq v1.10.9
-	github.com/mattn/go-sqlite3 v1.14.22
+	github.com/mattn/go-sqlite3 v1.14.24
 	github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa
-	github.com/seancfoley/ipaddress-go v1.6.0
+	github.com/seancfoley/ipaddress-go v1.7.0
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/stretchr/testify v1.9.0
 	github.com/txn2/txeh v1.5.5
-	golang.org/x/crypto v0.23.0
-	golang.org/x/net v0.23.0 // indirect
-	golang.org/x/oauth2 v0.21.0
-	golang.org/x/sys v0.21.0 // indirect
-	golang.org/x/text v0.16.0 // indirect
+	golang.org/x/crypto v0.28.0
+	golang.org/x/net v0.27.0 // indirect
+	golang.org/x/oauth2 v0.23.0
+	golang.org/x/sys v0.26.0 // indirect
+	golang.org/x/text v0.19.0 // indirect
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb
 	gopkg.in/yaml.v3 v3.0.1
 )
@@ -28,7 +28,7 @@ require (
 require (
 	filippo.io/edwards25519 v1.1.0
 	github.com/c-robinson/iplib v1.0.8
-	github.com/posthog/posthog-go v0.0.0-20211028072449-93c17c49e2b0
+	github.com/posthog/posthog-go v1.2.24
 )
 
 require (
@@ -38,6 +38,7 @@ require (
 )
 
 require (
+	github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e
 	github.com/guumaster/tablewriter v0.0.10
 	github.com/matryer/is v1.4.1
 	github.com/olekukonko/tablewriter v0.0.5
@@ -53,7 +54,6 @@ 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
-	github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
 )
 
@@ -66,5 +66,5 @@ require (
 	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/mattn/go-runewidth v0.0.13 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	golang.org/x/sync v0.7.0 // indirect
+	golang.org/x/sync v0.8.0 // indirect
 )

+ 23 - 28
go.sum

@@ -2,12 +2,10 @@ cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2Qx
 cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/c-robinson/iplib v1.0.8 h1:exDRViDyL9UBLcfmlxxkY5odWX5092nPsQIykHXhIn4=
 github.com/c-robinson/iplib v1.0.8/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo=
 github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
 github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
-github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -21,18 +19,21 @@ github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcP
 github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
 github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
 github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
-github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
+github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
 github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e h1:XmA6L9IPRdUr28a+SK/oMchGgQy159wvzXA5tJ7l+40=
+github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e/go.mod h1:AFIo+02s+12CEg8Gzz9kzhCbmbq6JcKNrhHffCGA9z4=
 github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
 github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
 github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
@@ -55,26 +56,24 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
 github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
-github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
+github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/posthog/posthog-go v0.0.0-20211028072449-93c17c49e2b0 h1:Y2hUrkfuM0on62KZOci/VLijlkdF/yeWU262BQgvcjE=
-github.com/posthog/posthog-go v0.0.0-20211028072449-93c17c49e2b0/go.mod h1:oa2sAs9tGai3VldabTV0eWejt/O4/OOD7azP8GaikqU=
+github.com/posthog/posthog-go v1.2.24 h1:A+iG4saBJemo++VDlcWovbYf8KFFNUfrCoJtsc40RPA=
+github.com/posthog/posthog-go v1.2.24/go.mod h1:uYC2l1Yktc8E+9FAHJ9QZG4vQf/NHJPD800Hsm7DzoM=
 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=
 github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa h1:hxMLFbj+F444JAS5nUQxTDZwUxwCRqg3WkNqhiDzXrM=
 github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg=
-github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/seancfoley/bintree v1.3.1 h1:cqmmQK7Jm4aw8gna0bP+huu5leVOgHGSJBEpUx3EXGI=
 github.com/seancfoley/bintree v1.3.1/go.mod h1:hIUabL8OFYyFVTQ6azeajbopogQc2l5C/hiXMcemWNU=
-github.com/seancfoley/ipaddress-go v1.6.0 h1:9z7yGmOnV4P2ML/dlR/kCJiv5tp8iHOOetJvxJh/R5w=
-github.com/seancfoley/ipaddress-go v1.6.0/go.mod h1:TQRZgv+9jdvzHmKoPGBMxyiaVmoI0rYpfEk8Q/sL/Iw=
-github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/seancfoley/ipaddress-go v1.7.0 h1:vWp3SR3k+HkV3aKiNO2vEe6xbVxS0x/Ixw6hgyP238s=
+github.com/seancfoley/ipaddress-go v1.7.0/go.mod h1:TQRZgv+9jdvzHmKoPGBMxyiaVmoI0rYpfEk8Q/sL/Iw=
 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
 github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
@@ -87,15 +86,12 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/txn2/txeh v1.5.5 h1:UN4e/lCK5HGw/gGAi2GCVrNKg0GTCUWs7gs5riaZlz4=
 github.com/txn2/txeh v1.5.5/go.mod h1:qYzGG9kCzeVEI12geK4IlanHWY8X4uy/I3NcW7mk8g4=
-github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
-github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
-github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
-golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
+golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -105,15 +101,15 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
-golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
-golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
-golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
+golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
+golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
+golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
-golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -122,8 +118,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
-golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
+golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -135,8 +131,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
-golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
+golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
+golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@@ -150,7 +146,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
 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.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 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=

+ 1 - 1
k8s/client/netclient-daemonset.yaml

@@ -16,7 +16,7 @@ spec:
       hostNetwork: true
       containers:
       - name: netclient
-        image: gravitl/netclient:v0.25.0
+        image: gravitl/netclient:v0.26.0
         env:
         - name: TOKEN
           value: "TOKEN_VALUE"

+ 1 - 1
k8s/client/netclient.yaml

@@ -28,7 +28,7 @@ spec:
       #           - "<node label value>"
       containers:
       - name: netclient
-        image: gravitl/netclient:v0.25.0
+        image: gravitl/netclient:v0.26.0
         env:
         - name: TOKEN
           value: "TOKEN_VALUE"

+ 1 - 1
k8s/server/netmaker-ui.yaml

@@ -15,7 +15,7 @@ spec:
     spec:
       containers:
       - name: netmaker-ui
-        image: gravitl/netmaker-ui:v0.25.0
+        image: gravitl/netmaker-ui:v0.26.0
         ports:
         - containerPort: 443
         env:

+ 650 - 0
logic/acls.go

@@ -0,0 +1,650 @@
+package logic
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"sort"
+	"sync"
+	"time"
+
+	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/servercfg"
+)
+
+var (
+	aclCacheMutex = &sync.RWMutex{}
+	aclCacheMap   = make(map[string]models.Acl)
+)
+
+// CreateDefaultAclNetworkPolicies - create default acl network policies
+func CreateDefaultAclNetworkPolicies(netID models.NetworkID) {
+	if netID.String() == "" {
+		return
+	}
+	if !IsAclExists(fmt.Sprintf("%s.%s", netID, "all-nodes")) {
+		defaultDeviceAcl := models.Acl{
+			ID:        fmt.Sprintf("%s.%s", netID, "all-nodes"),
+			Name:      "All Nodes",
+			MetaData:  "This Policy allows all nodes in the network to communicate with each other",
+			Default:   true,
+			NetworkID: netID,
+			RuleType:  models.DevicePolicy,
+			Src: []models.AclPolicyTag{
+				{
+					ID:    models.DeviceAclID,
+					Value: "*",
+				}},
+			Dst: []models.AclPolicyTag{
+				{
+					ID:    models.DeviceAclID,
+					Value: "*",
+				}},
+			AllowedDirection: models.TrafficDirectionBi,
+			Enabled:          true,
+			CreatedBy:        "auto",
+			CreatedAt:        time.Now().UTC(),
+		}
+		InsertAcl(defaultDeviceAcl)
+	}
+	if !IsAclExists(fmt.Sprintf("%s.%s", netID, "all-users")) {
+		defaultUserAcl := models.Acl{
+			ID:        fmt.Sprintf("%s.%s", netID, "all-users"),
+			Default:   true,
+			Name:      "All Users",
+			MetaData:  "This policy gives access to everything in the network for an user",
+			NetworkID: netID,
+			RuleType:  models.UserPolicy,
+			Src: []models.AclPolicyTag{
+				{
+					ID:    models.UserAclID,
+					Value: "*",
+				},
+			},
+			Dst: []models.AclPolicyTag{{
+				ID:    models.DeviceAclID,
+				Value: "*",
+			}},
+			AllowedDirection: models.TrafficDirectionUni,
+			Enabled:          true,
+			CreatedBy:        "auto",
+			CreatedAt:        time.Now().UTC(),
+		}
+		InsertAcl(defaultUserAcl)
+	}
+
+	if !IsAclExists(fmt.Sprintf("%s.%s", netID, "all-remote-access-gws")) {
+		defaultUserAcl := models.Acl{
+			ID:        fmt.Sprintf("%s.%s", netID, "all-remote-access-gws"),
+			Default:   true,
+			Name:      "All Remote Access Gateways",
+			NetworkID: netID,
+			RuleType:  models.DevicePolicy,
+			Src: []models.AclPolicyTag{
+				{
+					ID:    models.DeviceAclID,
+					Value: fmt.Sprintf("%s.%s", netID, models.RemoteAccessTagName),
+				},
+			},
+			Dst: []models.AclPolicyTag{
+				{
+					ID:    models.DeviceAclID,
+					Value: "*",
+				},
+			},
+			AllowedDirection: models.TrafficDirectionBi,
+			Enabled:          true,
+			CreatedBy:        "auto",
+			CreatedAt:        time.Now().UTC(),
+		}
+		InsertAcl(defaultUserAcl)
+	}
+	CreateDefaultUserPolicies(netID)
+}
+
+// DeleteDefaultNetworkPolicies - deletes all default network acl policies
+func DeleteDefaultNetworkPolicies(netId models.NetworkID) {
+	acls, _ := ListAcls(netId)
+	for _, acl := range acls {
+		if acl.NetworkID == netId && acl.Default {
+			DeleteAcl(acl)
+		}
+	}
+}
+
+// ValidateCreateAclReq - validates create req for acl
+func ValidateCreateAclReq(req models.Acl) error {
+	// check if acl network exists
+	_, err := GetNetwork(req.NetworkID.String())
+	if err != nil {
+		return errors.New("failed to get network details for " + req.NetworkID.String())
+	}
+	// err = CheckIDSyntax(req.Name)
+	// if err != nil {
+	// 	return err
+	// }
+	return nil
+}
+
+func listAclFromCache() (acls []models.Acl) {
+	aclCacheMutex.RLock()
+	defer aclCacheMutex.RUnlock()
+	for _, acl := range aclCacheMap {
+		acls = append(acls, acl)
+	}
+	return
+}
+
+func storeAclInCache(a models.Acl) {
+	aclCacheMutex.Lock()
+	defer aclCacheMutex.Unlock()
+	aclCacheMap[a.ID] = a
+}
+
+func removeAclFromCache(a models.Acl) {
+	aclCacheMutex.Lock()
+	defer aclCacheMutex.Unlock()
+	delete(aclCacheMap, a.ID)
+}
+
+func getAclFromCache(aID string) (a models.Acl, ok bool) {
+	aclCacheMutex.RLock()
+	defer aclCacheMutex.RUnlock()
+	a, ok = aclCacheMap[aID]
+	return
+}
+
+// InsertAcl - creates acl policy
+func InsertAcl(a models.Acl) error {
+	d, err := json.Marshal(a)
+	if err != nil {
+		return err
+	}
+	err = database.Insert(a.ID, string(d), database.ACLS_TABLE_NAME)
+	if err == nil && servercfg.CacheEnabled() {
+		storeAclInCache(a)
+	}
+	return err
+}
+
+// GetAcl - gets acl info by id
+func GetAcl(aID string) (models.Acl, error) {
+	a := models.Acl{}
+	if servercfg.CacheEnabled() {
+		var ok bool
+		a, ok = getAclFromCache(aID)
+		if ok {
+			return a, nil
+		}
+	}
+	d, err := database.FetchRecord(database.ACLS_TABLE_NAME, aID)
+	if err != nil {
+		return a, err
+	}
+	err = json.Unmarshal([]byte(d), &a)
+	if err != nil {
+		return a, err
+	}
+	if servercfg.CacheEnabled() {
+		storeAclInCache(a)
+	}
+	return a, nil
+}
+
+// IsAclExists - checks if acl exists
+func IsAclExists(aclID string) bool {
+	_, err := GetAcl(aclID)
+	return err == nil
+}
+
+// IsAclPolicyValid - validates if acl policy is valid
+func IsAclPolicyValid(acl models.Acl) bool {
+	//check if src and dst are valid
+
+	switch acl.RuleType {
+	case models.UserPolicy:
+		// src list should only contain users
+		for _, srcI := range acl.Src {
+
+			if srcI.ID == "" || srcI.Value == "" {
+				return false
+			}
+			if srcI.Value == "*" {
+				continue
+			}
+			if srcI.ID != models.UserAclID && srcI.ID != models.UserGroupAclID {
+				return false
+			}
+			// check if user group is valid
+			if srcI.ID == models.UserAclID {
+				_, err := GetUser(srcI.Value)
+				if err != nil {
+					return false
+				}
+
+			} else if srcI.ID == models.UserGroupAclID {
+				err := IsGroupValid(models.UserGroupID(srcI.Value))
+				if err != nil {
+					return false
+				}
+				// check if group belongs to this network
+				netGrps := GetUserGroupsInNetwork(acl.NetworkID)
+				if _, ok := netGrps[models.UserGroupID(srcI.Value)]; !ok {
+					return false
+				}
+			}
+
+		}
+		for _, dstI := range acl.Dst {
+
+			if dstI.ID == "" || dstI.Value == "" {
+				return false
+			}
+			if dstI.ID != models.DeviceAclID {
+				return false
+			}
+			if dstI.Value == "*" {
+				continue
+			}
+			// check if tag is valid
+			_, err := GetTag(models.TagID(dstI.Value))
+			if err != nil {
+				return false
+			}
+		}
+	case models.DevicePolicy:
+		for _, srcI := range acl.Src {
+			if srcI.ID == "" || srcI.Value == "" {
+				return false
+			}
+			if srcI.ID != models.DeviceAclID {
+				return false
+			}
+			if srcI.Value == "*" {
+				continue
+			}
+			// check if tag is valid
+			_, err := GetTag(models.TagID(srcI.Value))
+			if err != nil {
+				return false
+			}
+		}
+		for _, dstI := range acl.Dst {
+
+			if dstI.ID == "" || dstI.Value == "" {
+				return false
+			}
+			if dstI.ID != models.DeviceAclID {
+				return false
+			}
+			if dstI.Value == "*" {
+				continue
+			}
+			// check if tag is valid
+			_, err := GetTag(models.TagID(dstI.Value))
+			if err != nil {
+				return false
+			}
+		}
+	}
+	return true
+}
+
+// UpdateAcl - updates allowed fields on acls and commits to DB
+func UpdateAcl(newAcl, acl models.Acl) error {
+	if !acl.Default {
+		acl.Name = newAcl.Name
+		acl.Src = newAcl.Src
+		acl.Dst = newAcl.Dst
+	}
+	acl.Enabled = newAcl.Enabled
+	d, err := json.Marshal(acl)
+	if err != nil {
+		return err
+	}
+	err = database.Insert(acl.ID, string(d), database.ACLS_TABLE_NAME)
+	if err == nil && servercfg.CacheEnabled() {
+		storeAclInCache(acl)
+	}
+	return err
+}
+
+// UpsertAcl - upserts acl
+func UpsertAcl(acl models.Acl) error {
+	d, err := json.Marshal(acl)
+	if err != nil {
+		return err
+	}
+	err = database.Insert(acl.ID, string(d), database.ACLS_TABLE_NAME)
+	if err == nil && servercfg.CacheEnabled() {
+		storeAclInCache(acl)
+	}
+	return err
+}
+
+// DeleteAcl - deletes acl policy
+func DeleteAcl(a models.Acl) error {
+	err := database.DeleteRecord(database.ACLS_TABLE_NAME, a.ID)
+	if err == nil && servercfg.CacheEnabled() {
+		removeAclFromCache(a)
+	}
+	return err
+}
+
+// GetDefaultPolicy - fetches default policy in the network by ruleType
+func GetDefaultPolicy(netID models.NetworkID, ruleType models.AclPolicyType) (models.Acl, error) {
+	aclID := "all-users"
+	if ruleType == models.DevicePolicy {
+		aclID = "all-nodes"
+	}
+	acl, err := GetAcl(fmt.Sprintf("%s.%s", netID, aclID))
+	if err != nil {
+		return models.Acl{}, errors.New("default rule not found")
+	}
+	if acl.Enabled {
+		return acl, nil
+	}
+	// check if there are any custom all policies
+	policies, _ := ListAcls(netID)
+	for _, policy := range policies {
+		if !policy.Enabled {
+			continue
+		}
+		if policy.RuleType == ruleType {
+			dstMap := convAclTagToValueMap(policy.Dst)
+			srcMap := convAclTagToValueMap(policy.Src)
+			if _, ok := srcMap["*"]; ok {
+				if _, ok := dstMap["*"]; ok {
+					return policy, nil
+				}
+			}
+		}
+
+	}
+
+	return acl, nil
+}
+
+func listAcls() (acls []models.Acl) {
+	if servercfg.CacheEnabled() && len(aclCacheMap) > 0 {
+		return listAclFromCache()
+	}
+
+	data, err := database.FetchRecords(database.ACLS_TABLE_NAME)
+	if err != nil && !database.IsEmptyRecord(err) {
+		return []models.Acl{}
+	}
+
+	for _, dataI := range data {
+		acl := models.Acl{}
+		err := json.Unmarshal([]byte(dataI), &acl)
+		if err != nil {
+			continue
+		}
+		acls = append(acls, acl)
+		if servercfg.CacheEnabled() {
+			storeAclInCache(acl)
+		}
+	}
+	return
+}
+
+// ListUserPolicies - lists all acl policies enforced on an user
+func ListUserPolicies(u models.User) []models.Acl {
+	allAcls := listAcls()
+	userAcls := []models.Acl{}
+	for _, acl := range allAcls {
+
+		if acl.RuleType == models.UserPolicy {
+			srcMap := convAclTagToValueMap(acl.Src)
+			if _, ok := srcMap[u.UserName]; ok {
+				userAcls = append(userAcls, acl)
+			} else {
+				// check for user groups
+				for gID := range u.UserGroups {
+					if _, ok := srcMap[gID.String()]; ok {
+						userAcls = append(userAcls, acl)
+						break
+					}
+				}
+			}
+
+		}
+	}
+	return userAcls
+}
+
+// listPoliciesOfUser - lists all user acl policies applied to user in an network
+func listPoliciesOfUser(user models.User, netID models.NetworkID) []models.Acl {
+	allAcls := listAcls()
+	userAcls := []models.Acl{}
+	for _, acl := range allAcls {
+		if acl.NetworkID == netID && acl.RuleType == models.UserPolicy {
+			srcMap := convAclTagToValueMap(acl.Src)
+			if _, ok := srcMap[user.UserName]; ok {
+				userAcls = append(userAcls, acl)
+				continue
+			}
+			for netRole := range user.NetworkRoles {
+				if _, ok := srcMap[netRole.String()]; ok {
+					userAcls = append(userAcls, acl)
+					continue
+				}
+			}
+			for userG := range user.UserGroups {
+				if _, ok := srcMap[userG.String()]; ok {
+					userAcls = append(userAcls, acl)
+					continue
+				}
+			}
+
+		}
+	}
+	return userAcls
+}
+
+// listDevicePolicies - lists all device policies in a network
+func listDevicePolicies(netID models.NetworkID) []models.Acl {
+	allAcls := listAcls()
+	deviceAcls := []models.Acl{}
+	for _, acl := range allAcls {
+		if acl.NetworkID == netID && acl.RuleType == models.DevicePolicy {
+			deviceAcls = append(deviceAcls, acl)
+		}
+	}
+	return deviceAcls
+}
+
+// ListAcls - lists all acl policies
+func ListAcls(netID models.NetworkID) ([]models.Acl, error) {
+
+	allAcls := listAcls()
+	netAcls := []models.Acl{}
+	for _, acl := range allAcls {
+		if acl.NetworkID == netID {
+			netAcls = append(netAcls, acl)
+		}
+	}
+	return netAcls, nil
+}
+
+func convAclTagToValueMap(acltags []models.AclPolicyTag) map[string]struct{} {
+	aclValueMap := make(map[string]struct{})
+	for _, aclTagI := range acltags {
+		aclValueMap[aclTagI.Value] = struct{}{}
+	}
+	return aclValueMap
+}
+
+// IsUserAllowedToCommunicate - check if user is allowed to communicate with peer
+func IsUserAllowedToCommunicate(userName string, peer models.Node) bool {
+	if peer.IsStatic {
+		peer = peer.StaticNode.ConvertToStaticNode()
+	}
+	acl, _ := GetDefaultPolicy(models.NetworkID(peer.Network), models.UserPolicy)
+	if acl.Enabled {
+		return true
+	}
+	user, err := GetUser(userName)
+	if err != nil {
+		return false
+	}
+
+	policies := listPoliciesOfUser(*user, models.NetworkID(peer.Network))
+	for _, policy := range policies {
+		if !policy.Enabled {
+			continue
+		}
+		dstMap := convAclTagToValueMap(policy.Dst)
+		if _, ok := dstMap["*"]; ok {
+			return true
+		}
+		for tagID := range peer.Tags {
+			if _, ok := dstMap[tagID.String()]; ok {
+				return true
+			}
+		}
+
+	}
+	return false
+}
+
+// IsNodeAllowedToCommunicate - check node is allowed to communicate with the peer
+func IsNodeAllowedToCommunicate(node, peer models.Node) bool {
+	if node.IsStatic {
+		node = node.StaticNode.ConvertToStaticNode()
+	}
+	if peer.IsStatic {
+		peer = peer.StaticNode.ConvertToStaticNode()
+	}
+	// check default policy if all allowed return true
+	defaultPolicy, err := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
+	if err == nil {
+		if defaultPolicy.Enabled {
+			return true
+		}
+	}
+
+	// list device policies
+	policies := listDevicePolicies(models.NetworkID(peer.Network))
+	for _, policy := range policies {
+		if !policy.Enabled {
+			continue
+		}
+		srcMap := convAclTagToValueMap(policy.Src)
+		dstMap := convAclTagToValueMap(policy.Dst)
+		// fmt.Printf("\n======> SRCMAP: %+v\n", srcMap)
+		// fmt.Printf("\n======> DSTMAP: %+v\n", dstMap)
+		// fmt.Printf("\n======> node Tags: %+v\n", node.Tags)
+		// fmt.Printf("\n======> peer Tags: %+v\n", peer.Tags)
+		for tagID := range node.Tags {
+			if _, ok := dstMap[tagID.String()]; ok {
+				if _, ok := srcMap["*"]; ok {
+					return true
+				}
+				for tagID := range peer.Tags {
+					if _, ok := srcMap[tagID.String()]; ok {
+						return true
+					}
+				}
+			}
+			if _, ok := srcMap[tagID.String()]; ok {
+				if _, ok := dstMap["*"]; ok {
+					return true
+				}
+				for tagID := range peer.Tags {
+					if _, ok := dstMap[tagID.String()]; ok {
+						return true
+					}
+				}
+			}
+		}
+		for tagID := range peer.Tags {
+			if _, ok := dstMap[tagID.String()]; ok {
+				if _, ok := srcMap["*"]; ok {
+					return true
+				}
+				for tagID := range node.Tags {
+
+					if _, ok := srcMap[tagID.String()]; ok {
+						return true
+					}
+				}
+			}
+			if _, ok := srcMap[tagID.String()]; ok {
+				if _, ok := dstMap["*"]; ok {
+					return true
+				}
+				for tagID := range node.Tags {
+					if _, ok := dstMap[tagID.String()]; ok {
+						return true
+					}
+				}
+			}
+		}
+	}
+	return false
+}
+
+// SortTagEntrys - Sorts slice of Tag entries by their id
+func SortAclEntrys(acls []models.Acl) {
+	sort.Slice(acls, func(i, j int) bool {
+		return acls[i].Name < acls[j].Name
+	})
+}
+
+// UpdateDeviceTag - updates device tag on acl policies
+func UpdateDeviceTag(OldID, newID models.TagID, netID models.NetworkID) {
+	acls := listDevicePolicies(netID)
+	update := false
+	for _, acl := range acls {
+		for i, srcTagI := range acl.Src {
+			if srcTagI.ID == models.DeviceAclID {
+				if OldID.String() == srcTagI.Value {
+					acl.Src[i].Value = newID.String()
+					update = true
+				}
+			}
+		}
+		for i, dstTagI := range acl.Dst {
+			if dstTagI.ID == models.DeviceAclID {
+				if OldID.String() == dstTagI.Value {
+					acl.Dst[i].Value = newID.String()
+					update = true
+				}
+			}
+		}
+		if update {
+			UpsertAcl(acl)
+		}
+	}
+}
+
+// RemoveDeviceTagFromAclPolicies - remove device tag from acl policies
+func RemoveDeviceTagFromAclPolicies(tagID models.TagID, netID models.NetworkID) error {
+	acls := listDevicePolicies(netID)
+	update := false
+	for _, acl := range acls {
+		for i, srcTagI := range acl.Src {
+			if srcTagI.ID == models.DeviceAclID {
+				if tagID.String() == srcTagI.Value {
+					acl.Src = append(acl.Src[:i], acl.Src[i+1:]...)
+					update = true
+				}
+			}
+		}
+		for i, dstTagI := range acl.Dst {
+			if dstTagI.ID == models.DeviceAclID {
+				if tagID.String() == dstTagI.Value {
+					acl.Dst = append(acl.Dst[:i], acl.Dst[i+1:]...)
+					update = true
+				}
+			}
+		}
+		if update {
+			UpsertAcl(acl)
+		}
+	}
+	return nil
+}

+ 3 - 2
logic/acls/common.go

@@ -2,6 +2,7 @@ package acls
 
 import (
 	"encoding/json"
+	"maps"
 	"sync"
 
 	"github.com/gravitl/netmaker/database"
@@ -133,7 +134,7 @@ func fetchACLContainer(containerID ContainerID) (ACLContainer, error) {
 	defer AclMutex.RUnlock()
 	if servercfg.CacheEnabled() {
 		if aclContainer, ok := fetchAclContainerFromCache(containerID); ok {
-			return aclContainer, nil
+			return maps.Clone(aclContainer), nil
 		}
 	}
 	aclJson, err := fetchACLContainerJson(ContainerID(containerID))
@@ -147,7 +148,7 @@ func fetchACLContainer(containerID ContainerID) (ACLContainer, error) {
 	if servercfg.CacheEnabled() {
 		storeAclContainerInCache(containerID, currentNetworkACL)
 	}
-	return currentNetworkACL, nil
+	return maps.Clone(currentNetworkACL), nil
 }
 
 // fetchACLContainerJson - fetch the current ACL of given container except in json string

+ 2 - 1
logic/acls/nodeacls/retrieve.go

@@ -3,6 +3,7 @@ package nodeacls
 import (
 	"encoding/json"
 	"fmt"
+	"maps"
 	"sync"
 
 	"github.com/gravitl/netmaker/logic/acls"
@@ -67,5 +68,5 @@ func FetchAllACLs(networkID NetworkID) (acls.ACLContainer, error) {
 	if err != nil {
 		return nil, err
 	}
-	return currentNetworkACL, nil
+	return maps.Clone(currentNetworkACL), nil
 }

+ 2 - 1
logic/auth.go

@@ -186,7 +186,7 @@ func CreateUser(user *models.User) error {
 		logger.Log(0, "failed to insert user", err.Error())
 		return err
 	}
-
+	AddGlobalNetRolesToAdmins(*user)
 	return nil
 }
 
@@ -305,6 +305,7 @@ func UpdateUser(userchange, user *models.User) (*models.User, error) {
 	}
 	user.UserGroups = userchange.UserGroups
 	user.NetworkRoles = userchange.NetworkRoles
+	AddGlobalNetRolesToAdmins(*user)
 	err := ValidateUser(user)
 	if err != nil {
 		return &models.User{}, err

+ 1 - 1
logic/clients.go

@@ -32,7 +32,7 @@ var (
 			slog.Error("failed to get network acls", "error", err)
 			return err
 		}
-		networkAcls[acls.AclID(ec.ClientID)] = acls.ACL{}
+		networkAcls[acls.AclID(ec.ClientID)] = make(acls.ACL)
 		for objId := range networkAcls {
 			networkAcls[objId][acls.AclID(ec.ClientID)] = acls.Allowed
 			networkAcls[acls.AclID(ec.ClientID)][objId] = acls.Allowed

+ 30 - 5
logic/enrollmentkey.go

@@ -37,7 +37,7 @@ var (
 )
 
 // CreateEnrollmentKey - creates a new enrollment key in db
-func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string, unlimited bool, relay uuid.UUID) (*models.EnrollmentKey, error) {
+func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string, groups []models.TagID, unlimited bool, relay uuid.UUID, defaultKey bool) (*models.EnrollmentKey, error) {
 	newKeyID, err := getUniqueEnrollmentID()
 	if err != nil {
 		return nil, err
@@ -51,6 +51,8 @@ func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string
 		Tags:          []string{},
 		Type:          models.Undefined,
 		Relay:         relay,
+		Groups:        groups,
+		Default:       defaultKey,
 	}
 	if uses > 0 {
 		k.UsesRemaining = uses
@@ -89,7 +91,7 @@ func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string
 }
 
 // UpdateEnrollmentKey - updates an existing enrollment key's associated relay
-func UpdateEnrollmentKey(keyId string, relayId uuid.UUID) (*models.EnrollmentKey, error) {
+func UpdateEnrollmentKey(keyId string, relayId uuid.UUID, groups []models.TagID) (*models.EnrollmentKey, error) {
 	key, err := GetEnrollmentKey(keyId)
 	if err != nil {
 		return nil, err
@@ -109,7 +111,7 @@ func UpdateEnrollmentKey(keyId string, relayId uuid.UUID) (*models.EnrollmentKey
 	}
 
 	key.Relay = relayId
-
+	key.Groups = groups
 	if err = upsertEnrollmentKey(&key); err != nil {
 		return nil, err
 	}
@@ -151,11 +153,14 @@ func deleteEnrollmentkeyFromCache(key string) {
 }
 
 // DeleteEnrollmentKey - delete's a given enrollment key by value
-func DeleteEnrollmentKey(value string) error {
-	_, err := GetEnrollmentKey(value)
+func DeleteEnrollmentKey(value string, force bool) error {
+	key, err := GetEnrollmentKey(value)
 	if err != nil {
 		return err
 	}
+	if key.Default && !force {
+		return errors.New("cannot delete default network key")
+	}
 	err = database.DeleteRecord(database.ENROLLMENT_KEYS_TABLE_NAME, value)
 	if err == nil {
 		if servercfg.CacheEnabled() {
@@ -310,3 +315,23 @@ func getEnrollmentKeysMap() (map[string]models.EnrollmentKey, error) {
 	}
 	return currentKeys, nil
 }
+
+func RemoveTagFromEnrollmentKeys(deletedTagID models.TagID) {
+	keys, _ := GetAllEnrollmentKeys()
+	for _, key := range keys {
+		newTags := []models.TagID{}
+		update := false
+		for _, tagID := range key.Groups {
+			if tagID == deletedTagID {
+				update = true
+				continue
+			}
+			newTags = append(newTags, tagID)
+		}
+		if update {
+			key.Groups = newTags
+			upsertEnrollmentKey(&key)
+		}
+
+	}
+}

+ 15 - 15
logic/enrollmentkey_test.go

@@ -14,35 +14,35 @@ func TestCreateEnrollmentKey(t *testing.T) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
 	t.Run("Can_Not_Create_Key", func(t *testing.T) {
-		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, nil, false, uuid.Nil)
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, nil, nil, false, uuid.Nil, false)
 		assert.Nil(t, newKey)
 		assert.NotNil(t, err)
 		assert.ErrorIs(t, err, models.ErrInvalidEnrollmentKey)
 	})
 	t.Run("Can_Create_Key_Uses", func(t *testing.T) {
-		newKey, err := CreateEnrollmentKey(1, time.Time{}, nil, nil, false, uuid.Nil)
+		newKey, err := CreateEnrollmentKey(1, time.Time{}, nil, nil, nil, false, uuid.Nil, false)
 		assert.Nil(t, err)
 		assert.Equal(t, 1, newKey.UsesRemaining)
 		assert.True(t, newKey.IsValid())
 	})
 	t.Run("Can_Create_Key_Time", func(t *testing.T) {
-		newKey, err := CreateEnrollmentKey(0, time.Now().Add(time.Minute), nil, nil, false, uuid.Nil)
+		newKey, err := CreateEnrollmentKey(0, time.Now().Add(time.Minute), nil, nil, nil, false, uuid.Nil, false)
 		assert.Nil(t, err)
 		assert.True(t, newKey.IsValid())
 	})
 	t.Run("Can_Create_Key_Unlimited", func(t *testing.T) {
-		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, nil, true, uuid.Nil)
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, nil, nil, true, uuid.Nil, false)
 		assert.Nil(t, err)
 		assert.True(t, newKey.IsValid())
 	})
 	t.Run("Can_Create_Key_WithNetworks", func(t *testing.T) {
-		newKey, err := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true, uuid.Nil)
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false)
 		assert.Nil(t, err)
 		assert.True(t, newKey.IsValid())
 		assert.True(t, len(newKey.Networks) == 2)
 	})
 	t.Run("Can_Create_Key_WithTags", func(t *testing.T) {
-		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, []string{"tag1", "tag2"}, true, uuid.Nil)
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, []string{"tag1", "tag2"}, nil, true, uuid.Nil, false)
 		assert.Nil(t, err)
 		assert.True(t, newKey.IsValid())
 		assert.True(t, len(newKey.Tags) == 2)
@@ -62,10 +62,10 @@ func TestCreateEnrollmentKey(t *testing.T) {
 func TestDelete_EnrollmentKey(t *testing.T) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
-	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true, uuid.Nil)
+	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false)
 	t.Run("Can_Delete_Key", func(t *testing.T) {
 		assert.True(t, newKey.IsValid())
-		err := DeleteEnrollmentKey(newKey.Value)
+		err := DeleteEnrollmentKey(newKey.Value, false)
 		assert.Nil(t, err)
 		oldKey, err := GetEnrollmentKey(newKey.Value)
 		assert.Equal(t, oldKey, models.EnrollmentKey{})
@@ -73,7 +73,7 @@ func TestDelete_EnrollmentKey(t *testing.T) {
 		assert.Equal(t, err, EnrollmentErrors.NoKeyFound)
 	})
 	t.Run("Can_Not_Delete_Invalid_Key", func(t *testing.T) {
-		err := DeleteEnrollmentKey("notakey")
+		err := DeleteEnrollmentKey("notakey", false)
 		assert.NotNil(t, err)
 		assert.Equal(t, err, EnrollmentErrors.NoKeyFound)
 	})
@@ -83,7 +83,7 @@ func TestDelete_EnrollmentKey(t *testing.T) {
 func TestDecrement_EnrollmentKey(t *testing.T) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
-	newKey, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, false, uuid.Nil)
+	newKey, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, nil, false, uuid.Nil, false)
 	t.Run("Check_initial_uses", func(t *testing.T) {
 		assert.True(t, newKey.IsValid())
 		assert.Equal(t, newKey.UsesRemaining, 1)
@@ -107,9 +107,9 @@ func TestDecrement_EnrollmentKey(t *testing.T) {
 func TestUsability_EnrollmentKey(t *testing.T) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
-	key1, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, false, uuid.Nil)
-	key2, _ := CreateEnrollmentKey(0, time.Now().Add(time.Minute<<4), nil, nil, false, uuid.Nil)
-	key3, _ := CreateEnrollmentKey(0, time.Time{}, nil, nil, true, uuid.Nil)
+	key1, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, nil, false, uuid.Nil, false)
+	key2, _ := CreateEnrollmentKey(0, time.Now().Add(time.Minute<<4), nil, nil, nil, false, uuid.Nil, false)
+	key3, _ := CreateEnrollmentKey(0, time.Time{}, nil, nil, nil, true, uuid.Nil, false)
 	t.Run("Check if valid use key can be used", func(t *testing.T) {
 		assert.Equal(t, key1.UsesRemaining, 1)
 		ok := TryToUseEnrollmentKey(key1)
@@ -145,7 +145,7 @@ func removeAllEnrollments() {
 func TestTokenize_EnrollmentKeys(t *testing.T) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
-	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true, uuid.Nil)
+	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false)
 	const defaultValue = "MwE5MwE5MwE5MwE5MwE5MwE5MwE5MwE5"
 	const b64value = "eyJzZXJ2ZXIiOiJhcGkubXlzZXJ2ZXIuY29tIiwidmFsdWUiOiJNd0U1TXdFNU13RTVNd0U1TXdFNU13RTVNd0U1TXdFNSJ9"
 	const serverAddr = "api.myserver.com"
@@ -178,7 +178,7 @@ func TestTokenize_EnrollmentKeys(t *testing.T) {
 func TestDeTokenize_EnrollmentKeys(t *testing.T) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
-	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true, uuid.Nil)
+	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false)
 	const b64Value = "eyJzZXJ2ZXIiOiJhcGkubXlzZXJ2ZXIuY29tIiwidmFsdWUiOiJNd0U1TXdFNU13RTVNd0U1TXdFNU13RTVNd0U1TXdFNSJ9"
 	const serverAddr = "api.myserver.com"
 

+ 311 - 3
logic/extpeers.go

@@ -69,7 +69,7 @@ func GetEgressRangesOnNetwork(client *models.ExtClient) ([]string, error) {
 			}
 		}
 	}
-	extclients := GetGwExtclients(client.IngressGatewayID, client.Network)
+	extclients, _ := GetNetworkExtClients(client.Network)
 	for _, extclient := range extclients {
 		if extclient.ClientID == client.ClientID {
 			continue
@@ -136,6 +136,12 @@ func DeleteExtClientAndCleanup(extClient models.ExtClient) error {
 	return nil
 }
 
+//TODO - enforce extclient-to-extclient on ingress gw
+/* 1. fetch all non-user static nodes
+a. check against each user node, if allowed add rule
+
+*/
+
 // GetNetworkExtClients - gets the ext clients of given network
 func GetNetworkExtClients(network string) ([]models.ExtClient, error) {
 	var extclients []models.ExtClient
@@ -329,6 +335,7 @@ func UpdateExtClient(old *models.ExtClient, update *models.CustomExtClient) mode
 	// replace any \r\n with \n in postup and postdown from HTTP request
 	new.PostUp = strings.Replace(update.PostUp, "\r\n", "\n", -1)
 	new.PostDown = strings.Replace(update.PostDown, "\r\n", "\n", -1)
+	new.Tags = update.Tags
 	return new
 }
 
@@ -395,6 +402,206 @@ func ToggleExtClientConnectivity(client *models.ExtClient, enable bool) (models.
 	return newClient, nil
 }
 
+func GetStaticNodeIps(node models.Node) (ips []net.IP) {
+	defaultUserPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy)
+	defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
+
+	extclients := GetStaticNodesByNetwork(models.NetworkID(node.Network), false)
+	for _, extclient := range extclients {
+		if extclient.IsUserNode && defaultUserPolicy.Enabled {
+			continue
+		}
+		if !extclient.IsUserNode && defaultDevicePolicy.Enabled {
+			continue
+		}
+		if extclient.StaticNode.Address != "" {
+			ips = append(ips, extclient.StaticNode.AddressIPNet4().IP)
+		}
+		if extclient.StaticNode.Address6 != "" {
+			ips = append(ips, extclient.StaticNode.AddressIPNet6().IP)
+		}
+	}
+	return
+}
+
+func GetFwRulesOnIngressGateway(node models.Node) (rules []models.FwRule) {
+	// fetch user access to static clients via policies
+	defaultUserPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy)
+	defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
+	nodes, _ := GetNetworkNodes(node.Network)
+	nodes = append(nodes, GetStaticNodesByNetwork(models.NetworkID(node.Network), true)...)
+	//fmt.Printf("=====> NODES: %+v \n\n", nodes)
+	userNodes := GetStaticUserNodesByNetwork(models.NetworkID(node.Network))
+	//fmt.Printf("=====> USER NODES %+v \n\n", userNodes)
+	for _, userNodeI := range userNodes {
+		for _, peer := range nodes {
+			if peer.IsUserNode {
+				continue
+			}
+			if IsUserAllowedToCommunicate(userNodeI.StaticNode.OwnerID, peer) {
+				if peer.IsStatic {
+					if userNodeI.StaticNode.Address != "" {
+						if !defaultUserPolicy.Enabled {
+							rules = append(rules, models.FwRule{
+								SrcIP: userNodeI.StaticNode.AddressIPNet4(),
+								DstIP: peer.StaticNode.AddressIPNet4(),
+								Allow: true,
+							})
+						}
+						rules = append(rules, models.FwRule{
+							SrcIP: peer.StaticNode.AddressIPNet4(),
+							DstIP: userNodeI.StaticNode.AddressIPNet4(),
+							Allow: true,
+						})
+					}
+					if userNodeI.StaticNode.Address6 != "" {
+						if !defaultUserPolicy.Enabled {
+							rules = append(rules, models.FwRule{
+								SrcIP: userNodeI.StaticNode.AddressIPNet6(),
+								DstIP: peer.StaticNode.AddressIPNet6(),
+								Allow: true,
+							})
+						}
+
+						rules = append(rules, models.FwRule{
+							SrcIP: peer.StaticNode.AddressIPNet6(),
+							DstIP: userNodeI.StaticNode.AddressIPNet6(),
+							Allow: true,
+						})
+					}
+					if len(peer.StaticNode.ExtraAllowedIPs) > 0 {
+						for _, additionalAllowedIPNet := range peer.StaticNode.ExtraAllowedIPs {
+							_, ipNet, err := net.ParseCIDR(additionalAllowedIPNet)
+							if err != nil {
+								continue
+							}
+							if ipNet.IP.To4() != nil {
+								rules = append(rules, models.FwRule{
+									SrcIP: userNodeI.StaticNode.AddressIPNet4(),
+									DstIP: *ipNet,
+									Allow: true,
+								})
+							} else {
+								rules = append(rules, models.FwRule{
+									SrcIP: userNodeI.StaticNode.AddressIPNet6(),
+									DstIP: *ipNet,
+									Allow: true,
+								})
+							}
+
+						}
+
+					}
+				} else {
+
+					if userNodeI.StaticNode.Address != "" {
+						if !defaultUserPolicy.Enabled {
+							rules = append(rules, models.FwRule{
+								SrcIP: userNodeI.StaticNode.AddressIPNet4(),
+								DstIP: net.IPNet{
+									IP:   peer.Address.IP,
+									Mask: net.CIDRMask(32, 32),
+								},
+								Allow: true,
+							})
+						}
+					}
+
+					if userNodeI.StaticNode.Address6 != "" {
+						rules = append(rules, models.FwRule{
+							SrcIP: userNodeI.StaticNode.AddressIPNet6(),
+							DstIP: net.IPNet{
+								IP:   peer.Address6.IP,
+								Mask: net.CIDRMask(128, 128),
+							},
+							Allow: true,
+						})
+					}
+				}
+
+			}
+		}
+	}
+
+	if defaultDevicePolicy.Enabled {
+		return
+	}
+	for _, nodeI := range nodes {
+		if !nodeI.IsStatic || nodeI.IsUserNode {
+			continue
+		}
+		for _, peer := range nodes {
+			if peer.StaticNode.ClientID == nodeI.StaticNode.ClientID || peer.IsUserNode {
+				continue
+			}
+			if IsNodeAllowedToCommunicate(nodeI, peer) {
+				if peer.IsStatic {
+					if nodeI.StaticNode.Address != "" {
+						rules = append(rules, models.FwRule{
+							SrcIP: nodeI.StaticNode.AddressIPNet4(),
+							DstIP: peer.StaticNode.AddressIPNet4(),
+							Allow: true,
+						})
+					}
+					if nodeI.StaticNode.Address6 != "" {
+						rules = append(rules, models.FwRule{
+							SrcIP: nodeI.StaticNode.AddressIPNet6(),
+							DstIP: peer.StaticNode.AddressIPNet6(),
+							Allow: true,
+						})
+					}
+					if len(peer.StaticNode.ExtraAllowedIPs) > 0 {
+						for _, additionalAllowedIPNet := range peer.StaticNode.ExtraAllowedIPs {
+							_, ipNet, err := net.ParseCIDR(additionalAllowedIPNet)
+							if err != nil {
+								continue
+							}
+							if ipNet.IP.To4() != nil {
+								rules = append(rules, models.FwRule{
+									SrcIP: nodeI.StaticNode.AddressIPNet4(),
+									DstIP: *ipNet,
+									Allow: true,
+								})
+							} else {
+								rules = append(rules, models.FwRule{
+									SrcIP: nodeI.StaticNode.AddressIPNet6(),
+									DstIP: *ipNet,
+									Allow: true,
+								})
+							}
+
+						}
+
+					}
+				} else {
+					if nodeI.StaticNode.Address != "" {
+						rules = append(rules, models.FwRule{
+							SrcIP: nodeI.StaticNode.AddressIPNet4(),
+							DstIP: net.IPNet{
+								IP:   peer.Address.IP,
+								Mask: net.CIDRMask(32, 32),
+							},
+							Allow: true,
+						})
+					}
+					if nodeI.StaticNode.Address6 != "" {
+						rules = append(rules, models.FwRule{
+							SrcIP: nodeI.StaticNode.AddressIPNet6(),
+							DstIP: net.IPNet{
+								IP:   peer.Address6.IP,
+								Mask: net.CIDRMask(128, 128),
+							},
+							Allow: true,
+						})
+					}
+				}
+
+			}
+		}
+	}
+	return
+}
+
 func GetExtPeers(node, peer *models.Node) ([]wgtypes.PeerConfig, []models.IDandAddr, []models.EgressNetworkRoutes, error) {
 	var peers []wgtypes.PeerConfig
 	var idsAndAddr []models.IDandAddr
@@ -412,6 +619,16 @@ func GetExtPeers(node, peer *models.Node) ([]wgtypes.PeerConfig, []models.IDandA
 		if !IsClientNodeAllowed(&extPeer, peer.ID.String()) {
 			continue
 		}
+		if extPeer.RemoteAccessClientID == "" {
+			if !IsNodeAllowedToCommunicate(extPeer.ConvertToStaticNode(), *peer) {
+				continue
+			}
+		} else {
+			if !IsUserAllowedToCommunicate(extPeer.OwnerID, *peer) {
+				continue
+			}
+		}
+
 		pubkey, err := wgtypes.ParseKey(extPeer.PublicKey)
 		if err != nil {
 			logger.Log(1, "error parsing ext pub key:", err.Error())
@@ -483,8 +700,8 @@ func getExtPeerEgressRoute(node models.Node, extPeer models.ExtClient) (egressRo
 	return
 }
 
-func getExtpeersExtraRoutes(node models.Node, network string) (egressRoutes []models.EgressNetworkRoutes) {
-	extPeers, err := GetNetworkExtClients(network)
+func getExtpeerEgressRanges(node models.Node) (ranges, ranges6 []net.IPNet) {
+	extPeers, err := GetNetworkExtClients(node.Network)
 	if err != nil {
 		return
 	}
@@ -492,6 +709,36 @@ func getExtpeersExtraRoutes(node models.Node, network string) (egressRoutes []mo
 		if len(extPeer.ExtraAllowedIPs) == 0 {
 			continue
 		}
+		if !IsNodeAllowedToCommunicate(extPeer.ConvertToStaticNode(), node) {
+			continue
+		}
+		for _, allowedRange := range extPeer.ExtraAllowedIPs {
+			_, ipnet, err := net.ParseCIDR(allowedRange)
+			if err == nil {
+				if ipnet.IP.To4() != nil {
+					ranges = append(ranges, *ipnet)
+				} else {
+					ranges6 = append(ranges6, *ipnet)
+				}
+
+			}
+		}
+	}
+	return
+}
+
+func getExtpeersExtraRoutes(node models.Node) (egressRoutes []models.EgressNetworkRoutes) {
+	extPeers, err := GetNetworkExtClients(node.Network)
+	if err != nil {
+		return
+	}
+	for _, extPeer := range extPeers {
+		if len(extPeer.ExtraAllowedIPs) == 0 {
+			continue
+		}
+		if !IsNodeAllowedToCommunicate(extPeer.ConvertToStaticNode(), node) {
+			continue
+		}
 		egressRoutes = append(egressRoutes, getExtPeerEgressRoute(node, extPeer)...)
 	}
 	return
@@ -528,3 +775,64 @@ func GetExtclientAllowedIPs(client models.ExtClient) (allowedIPs []string) {
 	}
 	return
 }
+
+func GetStaticUserNodesByNetwork(network models.NetworkID) (staticNode []models.Node) {
+	extClients, err := GetAllExtClients()
+	if err != nil {
+		return
+	}
+	for _, extI := range extClients {
+		if extI.Network == network.String() {
+			if extI.RemoteAccessClientID != "" {
+				n := models.Node{
+					IsStatic:   true,
+					StaticNode: extI,
+					IsUserNode: extI.RemoteAccessClientID != "",
+				}
+				staticNode = append(staticNode, n)
+			}
+		}
+	}
+
+	return
+}
+
+func GetStaticNodesByNetwork(network models.NetworkID, onlyWg bool) (staticNode []models.Node) {
+	extClients, err := GetAllExtClients()
+	if err != nil {
+		return
+	}
+	for _, extI := range extClients {
+		if extI.Network == network.String() {
+			if onlyWg && extI.RemoteAccessClientID != "" {
+				continue
+			}
+			n := models.Node{
+				IsStatic:   true,
+				StaticNode: extI,
+				IsUserNode: extI.RemoteAccessClientID != "",
+			}
+			staticNode = append(staticNode, n)
+		}
+	}
+
+	return
+}
+
+func GetStaticNodesByGw(gwNode models.Node) (staticNode []models.Node) {
+	extClients, err := GetAllExtClients()
+	if err != nil {
+		return
+	}
+	for _, extI := range extClients {
+		if extI.IngressGatewayID == gwNode.ID.String() {
+			n := models.Node{
+				IsStatic:   true,
+				StaticNode: extI,
+				IsUserNode: extI.RemoteAccessClientID != "",
+			}
+			staticNode = append(staticNode, n)
+		}
+	}
+	return
+}

+ 16 - 33
logic/gateway.go

@@ -2,6 +2,7 @@ package logic
 
 import (
 	"errors"
+	"fmt"
 	"time"
 
 	"github.com/gravitl/netmaker/database"
@@ -74,7 +75,7 @@ func CreateEgressGateway(gateway models.EgressGatewayRequest) (models.Node, erro
 		return models.Node{}, errors.New(host.OS + " is unsupported for egress gateways")
 	}
 	if host.FirewallInUse == models.FIREWALL_NONE {
-		return models.Node{}, errors.New("firewall is not supported for egress gateways. please install iptables or nftables on the device in order to use this feature")
+		return models.Node{}, errors.New("please install iptables or nftables on the device")
 	}
 	for i := len(gateway.Ranges) - 1; i >= 0; i-- {
 		// check if internet gateway IPv4
@@ -149,9 +150,6 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq
 	if host.OS != "linux" {
 		return models.Node{}, errors.New("ingress can only be created on linux based node")
 	}
-	if host.FirewallInUse == models.FIREWALL_NONE {
-		return models.Node{}, errors.New("firewall is not supported for ingress gateways")
-	}
 
 	network, err := GetParentNetwork(netid)
 	if err != nil {
@@ -164,6 +162,14 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq
 	node.IngressGatewayRange = network.AddressRange
 	node.IngressGatewayRange6 = network.AddressRange6
 	node.IngressDNS = ingress.ExtclientDNS
+	node.IngressPersistentKeepalive = 20
+	if ingress.PersistentKeepalive != 0 {
+		node.IngressPersistentKeepalive = ingress.PersistentKeepalive
+	}
+	node.IngressMTU = 1420
+	if ingress.MTU != 0 {
+		node.IngressMTU = ingress.MTU
+	}
 	if servercfg.IsPro {
 		if _, exists := FailOverExists(node.Network); exists {
 			ResetFailedOverPeer(&node)
@@ -174,34 +180,14 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq
 	if node.Metadata == "" {
 		node.Metadata = "This host can be used for remote access"
 	}
+	if node.Tags == nil {
+		node.Tags = make(map[models.TagID]struct{})
+	}
+	node.Tags[models.TagID(fmt.Sprintf("%s.%s", netid, models.RemoteAccessTagName))] = struct{}{}
 	err = UpsertNode(&node)
 	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
 }
@@ -249,17 +235,14 @@ func DeleteIngressGateway(nodeid string) (models.Node, []models.ExtClient, error
 	if !servercfg.IsPro {
 		node.IsInternetGateway = false
 	}
+	delete(node.Tags, models.TagID(fmt.Sprintf("%s.%s", node.Network, models.RemoteAccessTagName)))
 	node.IngressGatewayRange = ""
 	node.Metadata = ""
 	err = UpsertNode(&node)
 	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
 }

+ 19 - 13
logic/hosts.go

@@ -10,6 +10,7 @@ import (
 
 	"github.com/google/uuid"
 	"golang.org/x/crypto/bcrypt"
+	"golang.org/x/exp/slog"
 
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
@@ -254,14 +255,31 @@ func UpdateHostFromClient(newHost, currHost *models.Host) (sendPeerUpdate bool)
 		currHost.WgPublicListenPort = newHost.WgPublicListenPort
 		sendPeerUpdate = true
 	}
+	isEndpointChanged := false
 	if currHost.EndpointIP.String() != newHost.EndpointIP.String() {
 		currHost.EndpointIP = newHost.EndpointIP
 		sendPeerUpdate = true
+		isEndpointChanged = true
 	}
 	if currHost.EndpointIPv6.String() != newHost.EndpointIPv6.String() {
 		currHost.EndpointIPv6 = newHost.EndpointIPv6
 		sendPeerUpdate = true
+		isEndpointChanged = true
 	}
+
+	if isEndpointChanged {
+		for _, nodeID := range currHost.Nodes {
+			node, err := GetNodeByID(nodeID)
+			if err != nil {
+				slog.Error("failed to get node:", "id", node.ID, "error", err)
+				continue
+			}
+			if node.FailedOverBy != uuid.Nil {
+				ResetFailedOverPeer(&node)
+			}
+		}
+	}
+
 	currHost.DaemonInstalled = newHost.DaemonInstalled
 	currHost.Debug = newHost.Debug
 	currHost.Verbosity = newHost.Verbosity
@@ -269,19 +287,7 @@ 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

+ 3 - 2
logic/jwts.go

@@ -56,8 +56,9 @@ func CreateJWT(uuid string, macAddress string, network string) (response string,
 func CreateUserJWT(username string, role models.UserRoleID) (response string, err error) {
 	expirationTime := time.Now().Add(servercfg.GetServerConfig().JwtValidityDuration)
 	claims := &models.UserClaims{
-		UserName: username,
-		Role:     role,
+		UserName:       username,
+		Role:           role,
+		RacAutoDisable: servercfg.GetRacAutoDisable() && (role != models.SuperAdminRole && role != models.AdminRole),
 		RegisteredClaims: jwt.RegisteredClaims{
 			Issuer:    "Netmaker",
 			Subject:   fmt.Sprintf("user|%s", username),

+ 24 - 0
logic/networks.go

@@ -8,9 +8,11 @@ import (
 	"sort"
 	"strings"
 	"sync"
+	"time"
 
 	"github.com/c-robinson/iplib"
 	validator "github.com/go-playground/validator/v10"
+	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic/acls/nodeacls"
@@ -175,6 +177,17 @@ func DeleteNetwork(network string) error {
 	if err != nil {
 		logger.Log(1, "failed to remove the node acls during network delete for network,", network)
 	}
+	// Delete default network enrollment key
+	keys, _ := GetAllEnrollmentKeys()
+	for _, key := range keys {
+		if key.Tags[0] == network {
+			if key.Default {
+				DeleteEnrollmentKey(key.Value, true)
+				break
+			}
+
+		}
+	}
 	nodeCount, err := GetNetworkNonServerNodeCount(network)
 	if nodeCount == 0 || database.IsEmptyRecord(err) {
 		// delete server nodes first then db records
@@ -233,6 +246,17 @@ func CreateNetwork(network models.Network) (models.Network, error) {
 		storeNetworkInCache(network.NetID, network)
 	}
 
+	_, _ = CreateEnrollmentKey(
+		0,
+		time.Time{},
+		[]string{network.NetID},
+		[]string{network.NetID},
+		[]models.TagID{},
+		true,
+		uuid.Nil,
+		true,
+	)
+
 	return network, nil
 }
 

+ 130 - 8
logic/nodes.go

@@ -196,10 +196,6 @@ 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
@@ -378,6 +374,20 @@ func GetAllNodes() ([]models.Node, error) {
 	return nodes, nil
 }
 
+func AddStaticNodestoList(nodes []models.Node) []models.Node {
+	netMap := make(map[string]struct{})
+	for _, node := range nodes {
+		if _, ok := netMap[node.Network]; ok {
+			continue
+		}
+		if node.IsIngressGateway {
+			nodes = append(nodes, GetStaticNodesByNetwork(models.NetworkID(node.Network), false)...)
+			netMap[node.Network] = struct{}{}
+		}
+	}
+	return nodes
+}
+
 // GetNetworkByNode - gets the network model from a node
 func GetNetworkByNode(node *models.Node) (models.Network, error) {
 
@@ -393,7 +403,7 @@ func GetNetworkByNode(node *models.Node) (models.Network, error) {
 }
 
 // SetNodeDefaults - sets the defaults of a node to avoid empty fields
-func SetNodeDefaults(node *models.Node) {
+func SetNodeDefaults(node *models.Node, resetConnected bool) {
 
 	parentNetwork, _ := GetNetworkByNode(node)
 	_, cidr, err := net.ParseCIDR(parentNetwork.AddressRange)
@@ -414,8 +424,14 @@ func SetNodeDefaults(node *models.Node) {
 
 	node.SetLastModified()
 	node.SetLastCheckIn()
-	node.SetDefaultConnected()
+
+	if resetConnected {
+		node.SetDefaultConnected()
+	}
 	node.SetExpirationDateTime()
+	if node.Tags == nil {
+		node.Tags = make(map[models.TagID]struct{})
+	}
 }
 
 // GetRecordKey - get record key
@@ -461,7 +477,7 @@ func GetDeletedNodeByID(uuid string) (models.Node, error) {
 		return models.Node{}, err
 	}
 
-	SetNodeDefaults(&node)
+	SetNodeDefaults(&node, true)
 
 	return node, nil
 }
@@ -531,7 +547,7 @@ func createNode(node *models.Node) error {
 		}
 	}
 
-	SetNodeDefaults(node)
+	SetNodeDefaults(node, true)
 
 	defaultACLVal := acls.Allowed
 	parentNetwork, err := GetNetwork(node.Network)
@@ -694,3 +710,109 @@ func GetAllFailOvers() ([]models.Node, error) {
 	}
 	return igs, nil
 }
+
+func GetTagMapWithNodes() (tagNodesMap map[models.TagID][]models.Node) {
+	tagNodesMap = make(map[models.TagID][]models.Node)
+	nodes, _ := GetAllNodes()
+	for _, nodeI := range nodes {
+		if nodeI.Tags == nil {
+			continue
+		}
+		for nodeTagID := range nodeI.Tags {
+			tagNodesMap[nodeTagID] = append(tagNodesMap[nodeTagID], nodeI)
+		}
+	}
+	return
+}
+
+func GetTagMapWithNodesByNetwork(netID models.NetworkID) (tagNodesMap map[models.TagID][]models.Node) {
+	tagNodesMap = make(map[models.TagID][]models.Node)
+	nodes, _ := GetNetworkNodes(netID.String())
+	for _, nodeI := range nodes {
+		if nodeI.Tags == nil {
+			continue
+		}
+		for nodeTagID := range nodeI.Tags {
+			tagNodesMap[nodeTagID] = append(tagNodesMap[nodeTagID], nodeI)
+		}
+	}
+	return AddTagMapWithStaticNodes(netID, tagNodesMap)
+}
+
+func AddTagMapWithStaticNodes(netID models.NetworkID,
+	tagNodesMap map[models.TagID][]models.Node) map[models.TagID][]models.Node {
+	extclients, err := GetNetworkExtClients(netID.String())
+	if err != nil {
+		return tagNodesMap
+	}
+	for _, extclient := range extclients {
+		if extclient.Tags == nil || extclient.RemoteAccessClientID != "" {
+			continue
+		}
+		for tagID := range extclient.Tags {
+			tagNodesMap[tagID] = append(tagNodesMap[tagID], models.Node{
+				IsStatic:   true,
+				StaticNode: extclient,
+			})
+		}
+
+	}
+	return tagNodesMap
+}
+
+func GetNodesWithTag(tagID models.TagID) map[string]models.Node {
+	nMap := make(map[string]models.Node)
+	tag, err := GetTag(tagID)
+	if err != nil {
+		return nMap
+	}
+	nodes, _ := GetNetworkNodes(tag.Network.String())
+	for _, nodeI := range nodes {
+		if nodeI.Tags == nil {
+			continue
+		}
+		if _, ok := nodeI.Tags[tagID]; ok {
+			nMap[nodeI.ID.String()] = nodeI
+		}
+	}
+	return AddStaticNodesWithTag(tag, nMap)
+}
+
+func AddStaticNodesWithTag(tag models.Tag, nMap map[string]models.Node) map[string]models.Node {
+	extclients, err := GetNetworkExtClients(tag.Network.String())
+	if err != nil {
+		return nMap
+	}
+	for _, extclient := range extclients {
+		if extclient.RemoteAccessClientID != "" {
+			continue
+		}
+		if _, ok := extclient.Tags[tag.ID]; ok {
+			nMap[extclient.ClientID] = models.Node{
+				IsStatic:   true,
+				StaticNode: extclient,
+			}
+		}
+
+	}
+	return nMap
+}
+
+func GetStaticNodeWithTag(tagID models.TagID) map[string]models.Node {
+	nMap := make(map[string]models.Node)
+	tag, err := GetTag(tagID)
+	if err != nil {
+		return nMap
+	}
+	extclients, err := GetNetworkExtClients(tag.Network.String())
+	if err != nil {
+		return nMap
+	}
+	for _, extclient := range extclients {
+		nMap[extclient.ClientID] = models.Node{
+			IsStatic:   true,
+			StaticNode: extclient,
+		}
+	}
+	return nMap
+}

+ 21 - 2
logic/peers.go

@@ -74,7 +74,8 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 		ServerVersion: servercfg.GetVersion(),
 		ServerAddrs:   []models.ServerAddr{},
 		FwUpdate: models.FwUpdate{
-			EgressInfo: make(map[string]models.EgressInfo),
+			EgressInfo:  make(map[string]models.EgressInfo),
+			IngressInfo: make(map[string]models.IngressInfo),
 		},
 		PeerIDs:           make(models.PeerMap, 0),
 		Peers:             []wgtypes.PeerConfig{},
@@ -182,7 +183,7 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 				})
 			}
 			if peer.IsIngressGateway {
-				hostPeerUpdate.EgressRoutes = append(hostPeerUpdate.EgressRoutes, getExtpeersExtraRoutes(node, peer.Network)...)
+				hostPeerUpdate.EgressRoutes = append(hostPeerUpdate.EgressRoutes, getExtpeersExtraRoutes(node)...)
 			}
 			_, isFailOverPeer := node.FailOverPeers[peer.ID.String()]
 			if servercfg.IsPro {
@@ -241,6 +242,7 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 				!peer.PendingDelete &&
 				peer.Connected &&
 				nodeacls.AreNodesAllowed(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), nodeacls.NodeID(peer.ID.String())) &&
+				IsNodeAllowedToCommunicate(node, peer) &&
 				(deletedNode == nil || (deletedNode != nil && peer.ID.String() != deletedNode.ID.String())) {
 				peerConfig.AllowedIPs = allowedips // only append allowed IPs if valid connection
 			}
@@ -287,8 +289,23 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 		var extPeerIDAndAddrs []models.IDandAddr
 		var egressRoutes []models.EgressNetworkRoutes
 		if node.IsIngressGateway {
+			hostPeerUpdate.FwUpdate.IsIngressGw = true
 			extPeers, extPeerIDAndAddrs, egressRoutes, err = GetExtPeers(&node, &node)
 			if err == nil {
+				defaultUserPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy)
+				defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
+				if !defaultDevicePolicy.Enabled || !defaultUserPolicy.Enabled {
+					ingFwUpdate := models.IngressInfo{
+						IngressID:     node.ID.String(),
+						Network:       node.NetworkRange,
+						Network6:      node.NetworkRange6,
+						AllowAll:      defaultDevicePolicy.Enabled && defaultUserPolicy.Default,
+						StaticNodeIps: GetStaticNodeIps(node),
+						Rules:         GetFwRulesOnIngressGateway(node),
+					}
+					ingFwUpdate.EgressRanges, ingFwUpdate.EgressRanges6 = getExtpeerEgressRanges(node)
+					hostPeerUpdate.FwUpdate.IngressInfo[node.ID.String()] = ingFwUpdate
+				}
 				hostPeerUpdate.EgressRoutes = append(hostPeerUpdate.EgressRoutes, egressRoutes...)
 				hostPeerUpdate.Peers = append(hostPeerUpdate.Peers, extPeers...)
 				for _, extPeerIdAndAddr := range extPeerIDAndAddrs {
@@ -391,6 +408,7 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 		}
 	}
 
+	hostPeerUpdate.ManageDNS = servercfg.GetManageDNS()
 	return hostPeerUpdate, nil
 }
 
@@ -425,6 +443,7 @@ func GetAllowedIPs(node, peer *models.Node, metrics *models.Metrics) []net.IPNet
 			logger.Log(2, "could not retrieve ext peers for ", peer.ID.String(), err.Error())
 		}
 		for _, extPeer := range extPeers {
+
 			allowedips = append(allowedips, extPeer.AllowedIPs...)
 		}
 	}

+ 24 - 0
logic/proc.go

@@ -0,0 +1,24 @@
+package logic
+
+import (
+	"os"
+	"runtime/pprof"
+
+	"github.com/gravitl/netmaker/logger"
+)
+
+func StartCPUProfiling() *os.File {
+	f, err := os.OpenFile("/root/data/cpu.prof", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0755)
+	if err != nil {
+		logger.Log(0, "could not create CPU profile: ", err.Error())
+	}
+	if err := pprof.StartCPUProfile(f); err != nil {
+		logger.Log(0, "could not start CPU profile: ", err.Error())
+	}
+	return f
+}
+
+func StopCPUProfiling(f *os.File) {
+	pprof.StopCPUProfile()
+	f.Close()
+}

+ 290 - 0
logic/tags.go

@@ -0,0 +1,290 @@
+package logic
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"regexp"
+	"sort"
+	"sync"
+	"time"
+
+	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/models"
+	"golang.org/x/exp/slog"
+)
+
+var tagMutex = &sync.RWMutex{}
+
+// GetTag - fetches tag info
+func GetTag(tagID models.TagID) (models.Tag, error) {
+	data, err := database.FetchRecord(database.TAG_TABLE_NAME, tagID.String())
+	if err != nil {
+		return models.Tag{}, err
+	}
+	tag := models.Tag{}
+	err = json.Unmarshal([]byte(data), &tag)
+	if err != nil {
+		return tag, err
+	}
+	return tag, nil
+}
+
+// InsertTag - creates new tag
+func InsertTag(tag models.Tag) error {
+	tagMutex.Lock()
+	defer tagMutex.Unlock()
+	_, err := database.FetchRecord(database.TAG_TABLE_NAME, tag.ID.String())
+	if err == nil {
+		return fmt.Errorf("tag `%s` exists already", tag.ID)
+	}
+	d, err := json.Marshal(tag)
+	if err != nil {
+		return err
+	}
+	return database.Insert(tag.ID.String(), string(d), database.TAG_TABLE_NAME)
+}
+
+// DeleteTag - delete tag, will also untag hosts
+func DeleteTag(tagID models.TagID, removeFromPolicy bool) error {
+	tagMutex.Lock()
+	defer tagMutex.Unlock()
+	// cleanUp tags on hosts
+	tag, err := GetTag(tagID)
+	if err != nil {
+		return err
+	}
+	nodes, err := GetNetworkNodes(tag.Network.String())
+	if err != nil {
+		return err
+	}
+	for _, nodeI := range nodes {
+		nodeI := nodeI
+		if _, ok := nodeI.Tags[tagID]; ok {
+			delete(nodeI.Tags, tagID)
+			UpsertNode(&nodeI)
+		}
+	}
+	if removeFromPolicy {
+		// remove tag used on acl policy
+		go RemoveDeviceTagFromAclPolicies(tagID, tag.Network)
+	}
+	extclients, _ := GetNetworkExtClients(tag.Network.String())
+	for _, extclient := range extclients {
+		if _, ok := extclient.Tags[tagID]; ok {
+			delete(extclient.Tags, tagID)
+			SaveExtClient(&extclient)
+		}
+	}
+	return database.DeleteRecord(database.TAG_TABLE_NAME, tagID.String())
+}
+
+// ListTagsWithHosts - lists all tags with tagged hosts
+func ListTagsWithNodes(netID models.NetworkID) ([]models.TagListResp, error) {
+	tags, err := ListNetworkTags(netID)
+	if err != nil {
+		return []models.TagListResp{}, err
+	}
+	tagsNodeMap := GetTagMapWithNodesByNetwork(netID)
+	resp := []models.TagListResp{}
+	for _, tagI := range tags {
+		tagRespI := models.TagListResp{
+			Tag:         tagI,
+			UsedByCnt:   len(tagsNodeMap[tagI.ID]),
+			TaggedNodes: GetAllNodesAPI(tagsNodeMap[tagI.ID]),
+		}
+		resp = append(resp, tagRespI)
+	}
+	return resp, nil
+}
+
+// ListTags - lists all tags from DB
+func ListTags() ([]models.Tag, error) {
+	tagMutex.RLock()
+	defer tagMutex.RUnlock()
+	data, err := database.FetchRecords(database.TAG_TABLE_NAME)
+	if err != nil && !database.IsEmptyRecord(err) {
+		return []models.Tag{}, err
+	}
+	tags := []models.Tag{}
+	for _, dataI := range data {
+		tag := models.Tag{}
+		err := json.Unmarshal([]byte(dataI), &tag)
+		if err != nil {
+			continue
+		}
+		tags = append(tags, tag)
+	}
+	return tags, nil
+}
+
+// ListTags - lists all tags from DB
+func ListNetworkTags(netID models.NetworkID) ([]models.Tag, error) {
+	tagMutex.RLock()
+	defer tagMutex.RUnlock()
+	data, err := database.FetchRecords(database.TAG_TABLE_NAME)
+	if err != nil && !database.IsEmptyRecord(err) {
+		return []models.Tag{}, err
+	}
+	tags := []models.Tag{}
+	for _, dataI := range data {
+		tag := models.Tag{}
+		err := json.Unmarshal([]byte(dataI), &tag)
+		if err != nil {
+			continue
+		}
+		if tag.Network == netID {
+			tags = append(tags, tag)
+		}
+
+	}
+	return tags, nil
+}
+
+// UpdateTag - updates and syncs hosts with tag update
+func UpdateTag(req models.UpdateTagReq, newID models.TagID) {
+	tagMutex.Lock()
+	defer tagMutex.Unlock()
+	var err error
+	tagNodesMap := GetNodesWithTag(req.ID)
+	for _, apiNode := range req.TaggedNodes {
+		node := models.Node{}
+		var nodeID string
+		if apiNode.IsStatic {
+			if apiNode.StaticNode.RemoteAccessClientID != "" {
+				continue
+			}
+			extclient, err := GetExtClient(apiNode.StaticNode.ClientID, apiNode.StaticNode.Network)
+			if err != nil {
+				continue
+			}
+			node.IsStatic = true
+			nodeID = extclient.ClientID
+			node.StaticNode = extclient
+		} else {
+			node, err = GetNodeByID(apiNode.ID)
+			if err != nil {
+				continue
+			}
+			nodeID = node.ID.String()
+		}
+
+		if _, ok := tagNodesMap[nodeID]; !ok {
+			if node.StaticNode.Tags == nil {
+				node.StaticNode.Tags = make(map[models.TagID]struct{})
+			}
+			if node.Tags == nil {
+				node.Tags = make(map[models.TagID]struct{})
+			}
+			if newID != "" {
+				if node.IsStatic {
+					node.StaticNode.Tags[newID] = struct{}{}
+					SaveExtClient(&node.StaticNode)
+				} else {
+					node.Tags[newID] = struct{}{}
+					UpsertNode(&node)
+				}
+
+			} else {
+				if node.IsStatic {
+					node.StaticNode.Tags[req.ID] = struct{}{}
+					SaveExtClient(&node.StaticNode)
+				} else {
+					node.Tags[req.ID] = struct{}{}
+					UpsertNode(&node)
+				}
+			}
+		} else {
+			if newID != "" {
+				delete(node.Tags, req.ID)
+				delete(node.StaticNode.Tags, req.ID)
+				if node.IsStatic {
+					node.StaticNode.Tags[newID] = struct{}{}
+					SaveExtClient(&node.StaticNode)
+				} else {
+					node.Tags[newID] = struct{}{}
+					UpsertNode(&node)
+				}
+			}
+			delete(tagNodesMap, nodeID)
+		}
+
+	}
+	for _, deletedTaggedNode := range tagNodesMap {
+		delete(deletedTaggedNode.Tags, req.ID)
+		delete(deletedTaggedNode.StaticNode.Tags, req.ID)
+		if deletedTaggedNode.IsStatic {
+			SaveExtClient(&deletedTaggedNode.StaticNode)
+		} else {
+			UpsertNode(&deletedTaggedNode)
+		}
+	}
+	go func(req models.UpdateTagReq) {
+		if newID != "" {
+			tagNodesMap = GetNodesWithTag(req.ID)
+			for _, nodeI := range tagNodesMap {
+				nodeI := nodeI
+				if nodeI.StaticNode.Tags == nil {
+					nodeI.StaticNode.Tags = make(map[models.TagID]struct{})
+				}
+				if nodeI.Tags == nil {
+					nodeI.Tags = make(map[models.TagID]struct{})
+				}
+				delete(nodeI.Tags, req.ID)
+				delete(nodeI.StaticNode.Tags, req.ID)
+				nodeI.Tags[newID] = struct{}{}
+				nodeI.StaticNode.Tags[newID] = struct{}{}
+				if nodeI.IsStatic {
+					SaveExtClient(&nodeI.StaticNode)
+				} else {
+					UpsertNode(&nodeI)
+				}
+			}
+		}
+	}(req)
+
+}
+
+// SortTagEntrys - Sorts slice of Tag entries by their id
+func SortTagEntrys(tags []models.TagListResp) {
+	sort.Slice(tags, func(i, j int) bool {
+		return tags[i].ID < tags[j].ID
+	})
+}
+
+func CheckIDSyntax(id string) error {
+	if id == "" {
+		return errors.New("name is required")
+	}
+	if len(id) < 3 {
+		return errors.New("name should have min 3 characters")
+	}
+	reg, err := regexp.Compile("^[a-zA-Z-]+$")
+	if err != nil {
+		return err
+	}
+	if !reg.MatchString(id) {
+		return errors.New("invalid name. allowed characters are [a-zA-Z-]")
+	}
+	return nil
+}
+
+func CreateDefaultTags(netID models.NetworkID) {
+	// create tag for remote access gws in the network
+	tag := models.Tag{
+		ID:        models.TagID(fmt.Sprintf("%s.%s", netID.String(), models.RemoteAccessTagName)),
+		TagName:   models.RemoteAccessTagName,
+		Network:   netID,
+		CreatedBy: "auto",
+		CreatedAt: time.Now(),
+	}
+	_, err := GetTag(tag.ID)
+	if err == nil {
+		return
+	}
+	err = InsertTag(tag)
+	if err != nil {
+		slog.Error("failed to create remote access gw tag", "error", err.Error())
+		return
+	}
+}

+ 11 - 0
logic/user_mgmt.go

@@ -39,17 +39,28 @@ var FilterNetworksByRole = func(allnetworks []models.Network, user models.User)
 var IsGroupsValid = func(groups map[models.UserGroupID]struct{}) error {
 	return nil
 }
+var IsGroupValid = func(groupID models.UserGroupID) error {
+	return nil
+}
 var IsNetworkRolesValid = func(networkRoles map[models.NetworkID]map[models.UserRoleID]struct{}) error {
 	return nil
 }
 
+var MigrateUserRoleAndGroups = func(u models.User) {
+
+}
+
 var UpdateUserGwAccess = func(currentUser, changeUser models.User) {}
 
 var UpdateRole = func(r models.UserRolePermissionTemplate) error { return nil }
 
 var InitialiseRoles = userRolesInit
+var IntialiseGroups = func() {}
 var DeleteNetworkRoles = func(netID string) {}
 var CreateDefaultNetworkRolesAndGroups = func(netID models.NetworkID) {}
+var CreateDefaultUserPolicies = func(netID models.NetworkID) {}
+var GetUserGroupsInNetwork = func(netID models.NetworkID) (networkGrps map[models.UserGroupID]models.UserGroup) { return }
+var AddGlobalNetRolesToAdmins = func(u models.User) {}
 
 // GetRole - fetches role template by id
 func GetRole(roleID models.UserRoleID) (models.UserRolePermissionTemplate, error) {

+ 3 - 2
main.go

@@ -27,10 +27,10 @@ import (
 	"golang.org/x/exp/slog"
 )
 
-var version = "v0.25.0"
+var version = "v0.26.0"
 
 //	@title			NetMaker
-//	@version		0.24.3
+//	@version		0.26.0
 //	@description	NetMaker API Docs
 //	@tag.name	    APIUsage
 //	@tag.description.markdown
@@ -103,6 +103,7 @@ func initialize() { // Client Mode Prereq Check
 
 	logic.SetJWTSecret()
 	logic.InitialiseRoles()
+	logic.IntialiseGroups()
 	err = serverctl.SetDefaults()
 	if err != nil {
 		logger.FatalLog("error setting defaults: ", err.Error())

+ 81 - 62
migrate/migrate.go

@@ -8,6 +8,7 @@ import (
 
 	"golang.org/x/exp/slog"
 
+	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
@@ -21,6 +22,7 @@ import (
 func Run() {
 	updateEnrollmentKeys()
 	assignSuperAdmin()
+	createDefaultTagsAndPolicies()
 	removeOldUserGrps()
 	syncUsers()
 	updateHosts()
@@ -122,6 +124,35 @@ func updateEnrollmentKeys() {
 		}
 
 	}
+
+	existingKeys, err := logic.GetAllEnrollmentKeys()
+	if err != nil {
+		return
+	}
+	// check if any tags are duplicate
+	existingTags := make(map[string]struct{})
+	for _, existingKey := range existingKeys {
+		for _, t := range existingKey.Tags {
+			existingTags[t] = struct{}{}
+		}
+	}
+	networks, _ := logic.GetNetworks()
+	for _, network := range networks {
+		if _, ok := existingTags[network.NetID]; ok {
+			continue
+		}
+		_, _ = logic.CreateEnrollmentKey(
+			0,
+			time.Time{},
+			[]string{network.NetID},
+			[]string{network.NetID},
+			[]models.TagID{},
+			true,
+			uuid.Nil,
+			true,
+		)
+
+	}
 }
 
 func removeOldUserGrps() {
@@ -166,6 +197,26 @@ func updateNodes() {
 		return
 	}
 	for _, node := range nodes {
+		node := node
+		if node.Tags == nil {
+			node.Tags = make(map[models.TagID]struct{})
+			logic.UpsertNode(&node)
+		}
+		if node.IsIngressGateway {
+			tagID := models.TagID(fmt.Sprintf("%s.%s", node.Network,
+				models.RemoteAccessTagName))
+			if node.Tags == nil {
+				node.Tags = make(map[models.TagID]struct{})
+			}
+			if _, ok := node.Tags[tagID]; !ok {
+				node.Tags[tagID] = struct{}{}
+				logic.UpsertNode(&node)
+			}
+			host, err := logic.GetHost(node.HostID.String())
+			if err == nil {
+				go logic.DeleteRole(models.GetRAGRoleID(node.Network, host.ID.String()), true)
+			}
+		}
 		if node.IsEgressGateway {
 			egressRanges, update := removeInterGw(node.EgressGatewayRanges)
 			if update {
@@ -175,6 +226,18 @@ func updateNodes() {
 			}
 		}
 	}
+	extclients, _ := logic.GetAllExtClients()
+	for _, extclient := range extclients {
+		tagID := models.TagID(fmt.Sprintf("%s.%s", extclient.Network,
+			models.RemoteAccessTagName))
+		if extclient.Tags == nil {
+			extclient.Tags = make(map[models.TagID]struct{})
+		}
+		if _, ok := extclient.Tags[tagID]; !ok {
+			extclient.Tags[tagID] = struct{}{}
+			logic.SaveExtClient(&extclient)
+		}
+	}
 }
 
 func removeInterGw(egressRanges []string) ([]string, bool) {
@@ -330,42 +393,8 @@ 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 {
-				logic.CreateDefaultNetworkRolesAndGroups(models.NetworkID(netI.NetID))
-				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,
-										},
-									},
-								},
-							})
-						}
-
-					}
-				}
-			}
+		for _, netI := range networks {
+			logic.CreateDefaultNetworkRolesAndGroups(models.NetworkID(netI.NetID))
 		}
 	}
 
@@ -382,6 +411,8 @@ func syncUsers() {
 				logic.UpsertUser(user)
 			}
 			if user.PlatformRoleID.String() != "" {
+				logic.MigrateUserRoleAndGroups(user)
+				logic.AddGlobalNetRolesToAdmins(user)
 				continue
 			}
 			user.AuthType = models.BasicAuth
@@ -403,32 +434,20 @@ func syncUsers() {
 				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)
-			}
+			logic.AddGlobalNetRolesToAdmins(user)
+			logic.MigrateUserRoleAndGroups(user)
 		}
 	}
+
+}
+
+func createDefaultTagsAndPolicies() {
+	networks, err := logic.GetNetworks()
+	if err != nil {
+		return
+	}
+	for _, network := range networks {
+		logic.CreateDefaultTags(models.NetworkID(network.NetID))
+		logic.CreateDefaultAclNetworkPolicies(models.NetworkID(network.NetID))
+	}
 }

+ 72 - 0
models/acl.go

@@ -0,0 +1,72 @@
+package models
+
+import (
+	"time"
+)
+
+// AllowedTrafficDirection - allowed direction of traffic
+type AllowedTrafficDirection int
+
+const (
+	// TrafficDirectionUni implies traffic is only allowed in one direction (src --> dst)
+	TrafficDirectionUni AllowedTrafficDirection = iota
+	// TrafficDirectionBi implies traffic is allowed both direction (src <--> dst )
+	TrafficDirectionBi
+)
+
+type AclPolicyType string
+
+const (
+	UserPolicy   AclPolicyType = "user-policy"
+	DevicePolicy AclPolicyType = "device-policy"
+)
+
+type AclPolicyTag struct {
+	ID    AclGroupType `json:"id"`
+	Value string       `json:"value"`
+}
+
+type AclGroupType string
+
+const (
+	UserAclID                AclGroupType = "user"
+	UserGroupAclID           AclGroupType = "user-group"
+	DeviceAclID              AclGroupType = "tag"
+	NetmakerIPAclID          AclGroupType = "ip"
+	NetmakerSubNetRangeAClID AclGroupType = "ipset"
+)
+
+func (g AclGroupType) String() string {
+	return string(g)
+}
+
+type UpdateAclRequest struct {
+	Acl
+	NewName string `json:"new_name"`
+}
+
+type AclPolicy struct {
+	TypeID        AclPolicyType
+	PrefixTagUser AclGroupType
+}
+
+type Acl struct {
+	ID               string                  `json:"id"`
+	Default          bool                    `json:"default"`
+	MetaData         string                  `json:"meta_data"`
+	Name             string                  `json:"name"`
+	NetworkID        NetworkID               `json:"network_id"`
+	RuleType         AclPolicyType           `json:"policy_type"`
+	Src              []AclPolicyTag          `json:"src_type"`
+	Dst              []AclPolicyTag          `json:"dst_type"`
+	AllowedDirection AllowedTrafficDirection `json:"allowed_traffic_direction"`
+	Enabled          bool                    `json:"enabled"`
+	CreatedBy        string                  `json:"created_by"`
+	CreatedAt        time.Time               `json:"created_at"`
+}
+
+type AclPolicyTypes struct {
+	RuleTypes     []AclPolicyType `json:"policy_types"`
+	SrcGroupTypes []AclGroupType  `json:"src_grp_types"`
+	DstGroupTypes []AclGroupType  `json:"dst_grp_types"`
+}

+ 42 - 27
models/api_node.go

@@ -10,33 +10,35 @@ import (
 
 // ApiNode is a stripped down Node DTO that exposes only required fields to external systems
 type ApiNode struct {
-	ID                      string   `json:"id,omitempty" validate:"required,min=5,id_unique"`
-	HostID                  string   `json:"hostid,omitempty" validate:"required,min=5,id_unique"`
-	Address                 string   `json:"address" validate:"omitempty,cidrv4"`
-	Address6                string   `json:"address6" validate:"omitempty,cidrv6"`
-	LocalAddress            string   `json:"localaddress" validate:"omitempty,cidr"`
-	AllowedIPs              []string `json:"allowedips"`
-	LastModified            int64    `json:"lastmodified"`
-	ExpirationDateTime      int64    `json:"expdatetime"`
-	LastCheckIn             int64    `json:"lastcheckin"`
-	LastPeerUpdate          int64    `json:"lastpeerupdate"`
-	Network                 string   `json:"network"`
-	NetworkRange            string   `json:"networkrange"`
-	NetworkRange6           string   `json:"networkrange6"`
-	IsRelayed               bool     `json:"isrelayed"`
-	IsRelay                 bool     `json:"isrelay"`
-	RelayedBy               string   `json:"relayedby" bson:"relayedby" yaml:"relayedby"`
-	RelayedNodes            []string `json:"relaynodes" yaml:"relayedNodes"`
-	IsEgressGateway         bool     `json:"isegressgateway"`
-	IsIngressGateway        bool     `json:"isingressgateway"`
-	EgressGatewayRanges     []string `json:"egressgatewayranges"`
-	EgressGatewayNatEnabled bool     `json:"egressgatewaynatenabled"`
-	DNSOn                   bool     `json:"dnson"`
-	IngressDns              string   `json:"ingressdns"`
-	Server                  string   `json:"server"`
-	Connected               bool     `json:"connected"`
-	PendingDelete           bool     `json:"pendingdelete"`
-	Metadata                string   `json:"metadata"`
+	ID                         string   `json:"id,omitempty" validate:"required,min=5,id_unique"`
+	HostID                     string   `json:"hostid,omitempty" validate:"required,min=5,id_unique"`
+	Address                    string   `json:"address" validate:"omitempty,cidrv4"`
+	Address6                   string   `json:"address6" validate:"omitempty,cidrv6"`
+	LocalAddress               string   `json:"localaddress" validate:"omitempty,cidr"`
+	AllowedIPs                 []string `json:"allowedips"`
+	LastModified               int64    `json:"lastmodified"`
+	ExpirationDateTime         int64    `json:"expdatetime"`
+	LastCheckIn                int64    `json:"lastcheckin"`
+	LastPeerUpdate             int64    `json:"lastpeerupdate"`
+	Network                    string   `json:"network"`
+	NetworkRange               string   `json:"networkrange"`
+	NetworkRange6              string   `json:"networkrange6"`
+	IsRelayed                  bool     `json:"isrelayed"`
+	IsRelay                    bool     `json:"isrelay"`
+	RelayedBy                  string   `json:"relayedby" bson:"relayedby" yaml:"relayedby"`
+	RelayedNodes               []string `json:"relaynodes" yaml:"relayedNodes"`
+	IsEgressGateway            bool     `json:"isegressgateway"`
+	IsIngressGateway           bool     `json:"isingressgateway"`
+	EgressGatewayRanges        []string `json:"egressgatewayranges"`
+	EgressGatewayNatEnabled    bool     `json:"egressgatewaynatenabled"`
+	DNSOn                      bool     `json:"dnson"`
+	IngressDns                 string   `json:"ingressdns"`
+	IngressPersistentKeepalive int32    `json:"ingresspersistentkeepalive"`
+	IngressMTU                 int32    `json:"ingressmtu"`
+	Server                     string   `json:"server"`
+	Connected                  bool     `json:"connected"`
+	PendingDelete              bool     `json:"pendingdelete"`
+	Metadata                   string   `json:"metadata"`
 	// == PRO ==
 	DefaultACL        string              `json:"defaultacl,omitempty" validate:"checkyesornoorunset"`
 	IsFailOver        bool                `json:"is_fail_over"`
@@ -46,6 +48,10 @@ type ApiNode struct {
 	InetNodeReq       InetNodeReq         `json:"inet_node_req" yaml:"inet_node_req"`
 	InternetGwID      string              `json:"internetgw_node_id" yaml:"internetgw_node_id"`
 	AdditionalRagIps  []string            `json:"additional_rag_ips" yaml:"additional_rag_ips"`
+	Tags              map[TagID]struct{}  `json:"tags" yaml:"tags"`
+	IsStatic          bool                `json:"is_static"`
+	IsUserNode        bool                `json:"is_user_node"`
+	StaticNode        ExtClient           `json:"static_node"`
 }
 
 // ApiNode.ConvertToServerNode - converts an api node to a server node
@@ -72,6 +78,8 @@ func (a *ApiNode) ConvertToServerNode(currentNode *Node) *Node {
 	convertedNode.IngressGatewayRange6 = currentNode.IngressGatewayRange6
 	convertedNode.DNSOn = a.DNSOn
 	convertedNode.IngressDNS = a.IngressDns
+	convertedNode.IngressPersistentKeepalive = a.IngressPersistentKeepalive
+	convertedNode.IngressMTU = a.IngressMTU
 	convertedNode.IsInternetGateway = a.IsInternetGateway
 	convertedNode.EgressGatewayRequest = currentNode.EgressGatewayRequest
 	convertedNode.EgressGatewayNatEnabled = currentNode.EgressGatewayNatEnabled
@@ -119,6 +127,7 @@ func (a *ApiNode) ConvertToServerNode(currentNode *Node) *Node {
 		}
 		convertedNode.AdditionalRagIps = append(convertedNode.AdditionalRagIps, ragIp)
 	}
+	convertedNode.Tags = a.Tags
 	return &convertedNode
 }
 
@@ -162,6 +171,8 @@ func (nm *Node) ConvertToAPINode() *ApiNode {
 	apiNode.EgressGatewayNatEnabled = nm.EgressGatewayNatEnabled
 	apiNode.DNSOn = nm.DNSOn
 	apiNode.IngressDns = nm.IngressDNS
+	apiNode.IngressPersistentKeepalive = nm.IngressPersistentKeepalive
+	apiNode.IngressMTU = nm.IngressMTU
 	apiNode.Server = nm.Server
 	apiNode.Connected = nm.Connected
 	apiNode.PendingDelete = nm.PendingDelete
@@ -174,9 +185,13 @@ func (nm *Node) ConvertToAPINode() *ApiNode {
 	apiNode.FailedOverBy = nm.FailedOverBy
 	apiNode.Metadata = nm.Metadata
 	apiNode.AdditionalRagIps = []string{}
+	apiNode.Tags = nm.Tags
 	for _, ip := range nm.AdditionalRagIps {
 		apiNode.AdditionalRagIps = append(apiNode.AdditionalRagIps, ip.String())
 	}
+	apiNode.IsStatic = nm.IsStatic
+	apiNode.IsUserNode = nm.IsUserNode
+	apiNode.StaticNode = nm.StaticNode
 	return &apiNode
 }
 

+ 2 - 2
models/dnsEntry.go

@@ -42,8 +42,8 @@ type DNSUpdate struct {
 
 // DNSEntry - a DNS entry represented as struct
 type DNSEntry struct {
-	Address  string `json:"address" validate:"ip"`
-	Address6 string `json:"address6"`
+	Address  string `json:"address" validate:"omitempty,ip"`
+	Address6 string `json:"address6" validate:"omitempty,ip"`
 	Name     string `json:"name" validate:"required,name_unique,min=1,max=192,whitespace"`
 	Network  string `json:"network" validate:"network_exists"`
 }

+ 3 - 0
models/enrollment_key.go

@@ -52,6 +52,8 @@ type EnrollmentKey struct {
 	Token         string    `json:"token,omitempty"` // B64 value of EnrollmentToken
 	Type          KeyType   `json:"type"`
 	Relay         uuid.UUID `json:"relay"`
+	Groups        []TagID   `json:"groups"`
+	Default       bool      `json:"default"`
 }
 
 // APIEnrollmentKey - used to create enrollment keys via API
@@ -63,6 +65,7 @@ type APIEnrollmentKey struct {
 	Tags          []string `json:"tags" validate:"required,dive,min=3,max=32"`
 	Type          KeyType  `json:"type"`
 	Relay         string   `json:"relay"`
+	Groups        []TagID  `json:"groups"`
 }
 
 // RegisterResponse - the response to a successful enrollment register

+ 14 - 0
models/extclient.go

@@ -20,6 +20,7 @@ type ExtClient struct {
 	RemoteAccessClientID   string              `json:"remote_access_client_id"` // unique ID (MAC address) of RAC machine
 	PostUp                 string              `json:"postup" bson:"postup"`
 	PostDown               string              `json:"postdown" bson:"postdown"`
+	Tags                   map[TagID]struct{}  `json:"tags"`
 }
 
 // CustomExtClient - struct for CustomExtClient params
@@ -33,4 +34,17 @@ type CustomExtClient struct {
 	RemoteAccessClientID string              `json:"remote_access_client_id"` // unique ID (MAC address) of RAC machine
 	PostUp               string              `json:"postup" bson:"postup" validate:"max=1024"`
 	PostDown             string              `json:"postdown" bson:"postdown" validate:"max=1024"`
+	Tags                 map[TagID]struct{}  `json:"tags"`
+}
+
+func (ext *ExtClient) ConvertToStaticNode() Node {
+
+	return Node{
+		CommonNode: CommonNode{
+			Network: ext.Network,
+		},
+		Tags:       ext.Tags,
+		IsStatic:   true,
+		StaticNode: *ext,
+	}
 }

+ 11 - 9
models/metrics.go

@@ -14,15 +14,17 @@ type Metrics struct {
 
 // Metric - holds a metric for data between nodes
 type Metric struct {
-	NodeName      string        `json:"node_name" bson:"node_name" yaml:"node_name"`
-	Uptime        int64         `json:"uptime" bson:"uptime" yaml:"uptime"`
-	TotalTime     int64         `json:"totaltime" bson:"totaltime" yaml:"totaltime"`
-	Latency       int64         `json:"latency" bson:"latency" yaml:"latency"`
-	TotalReceived int64         `json:"totalreceived" bson:"totalreceived" yaml:"totalreceived"`
-	TotalSent     int64         `json:"totalsent" bson:"totalsent" yaml:"totalsent"`
-	ActualUptime  time.Duration `json:"actualuptime" bson:"actualuptime" yaml:"actualuptime"`
-	PercentUp     float64       `json:"percentup" bson:"percentup" yaml:"percentup"`
-	Connected     bool          `json:"connected" bson:"connected" yaml:"connected"`
+	NodeName          string        `json:"node_name" bson:"node_name" yaml:"node_name"`
+	Uptime            int64         `json:"uptime" bson:"uptime" yaml:"uptime"`
+	TotalTime         int64         `json:"totaltime" bson:"totaltime" yaml:"totaltime"`
+	Latency           int64         `json:"latency" bson:"latency" yaml:"latency"`
+	TotalReceived     int64         `json:"totalreceived" bson:"totalreceived" yaml:"totalreceived"`
+	LastTotalReceived int64         `json:"lasttotalreceived" bson:"lasttotalreceived" yaml:"lasttotalreceived"`
+	TotalSent         int64         `json:"totalsent" bson:"totalsent" yaml:"totalsent"`
+	LastTotalSent     int64         `json:"lasttotalsent" bson:"lasttotalsent" yaml:"lasttotalsent"`
+	ActualUptime      time.Duration `json:"actualuptime" bson:"actualuptime" yaml:"actualuptime"`
+	PercentUp         float64       `json:"percentup" bson:"percentup" yaml:"percentup"`
+	Connected         bool          `json:"connected" bson:"connected" yaml:"connected"`
 }
 
 // IDandAddr - struct to hold ID and primary Address

+ 19 - 4
models/mqtt.go

@@ -24,12 +24,25 @@ type HostPeerUpdate struct {
 	FwUpdate          FwUpdate              `json:"fw_update"`
 	ReplacePeers      bool                  `json:"replace_peers"`
 	EndpointDetection bool                  `json:"endpoint_detection"`
+	ManageDNS         bool                  `yaml:"manage_dns"`
+}
+
+type FwRule struct {
+	SrcIP net.IPNet
+	DstIP net.IPNet
+	Allow bool
 }
 
 // IngressInfo - struct for ingress info
 type IngressInfo struct {
-	ExtPeers     map[string]ExtClientInfo `json:"ext_peers" yaml:"ext_peers"`
-	EgressRanges []string                 `json:"egress_ranges" yaml:"egress_ranges"`
+	IngressID     string      `json:"ingress_id"`
+	Network       net.IPNet   `json:"network"`
+	Network6      net.IPNet   `json:"network6"`
+	StaticNodeIps []net.IP    `json:"static_node_ips"`
+	Rules         []FwRule    `json:"rules"`
+	AllowAll      bool        `json:"allow_all"`
+	EgressRanges  []net.IPNet `json:"egress_ranges"`
+	EgressRanges6 []net.IPNet `json:"egress_ranges6"`
 }
 
 // EgressInfo - struct for egress info
@@ -77,8 +90,10 @@ type KeyUpdate struct {
 
 // FwUpdate - struct for firewall updates
 type FwUpdate struct {
-	IsEgressGw bool                  `json:"is_egress_gw"`
-	EgressInfo map[string]EgressInfo `json:"egress_info"`
+	IsEgressGw  bool                   `json:"is_egress_gw"`
+	IsIngressGw bool                   `json:"is_ingress_gw"`
+	EgressInfo  map[string]EgressInfo  `json:"egress_info"`
+	IngressInfo map[string]IngressInfo `json:"ingress_info"`
 }
 
 // FailOverMeReq - struct for failover req

+ 5 - 229
models/names.go

@@ -1,242 +1,18 @@
 package models
 
 import (
-	"math/rand"
 	"time"
-)
-
-// NAMES - list of names 4-7 chars in length
-var NAMES = []string{
-	"logic",
-	"warrant",
-	"iconic",
-	"threat",
-	"strike",
-	"boy",
-	"vital",
-	"unity",
-	"audio",
-	"schemer",
-	"depth",
-	"gravitl",
-	"mystic",
-	"donkey",
-	"atomic",
-	"turtle",
-	"monkey",
-	"rabbit",
-	"static",
-	"mosaic",
-	"elite",
-	"stonks",
-	"doggy",
-	"python",
-	"mohawk",
-	"arctic",
-	"rival",
-	"vibes",
-	"delay",
-	"bridge",
-	"weeble",
-	"combat",
-	"animal",
-	"wobble",
-	"rubble",
-	"bucket",
-	"proof",
-	"worker",
-	"beetle",
-	"racket",
-	"equal",
-	"panda",
-	"antics",
-	"strong",
-	"forum",
-	"koala",
-	"anchor",
-	"ornery",
-	"indigo",
-	"schism",
-	"dragon",
-	"knight",
-	"bishop",
-	"laser",
-	"rhino",
-	"clutch",
-	"shark",
-	"leader",
-	"young",
-	"robot",
-	"squish",
-	"chimp",
-	"rocket",
-	"space",
-	"queen",
-	"royalty",
-	"flush",
-	"earth",
-	"planet",
-	"heart",
-	"droplet",
-	"dillon",
-	"saturn",
-	"pluto",
-	"school",
-	"alien",
-	"matte",
-	"dingo",
-	"meercat",
-	"cookie",
-	"snack",
-	"goose",
-	"pepper",
-	"melissa",
-	"alex",
-	"elon",
-	"yeet",
-	"meh",
-	"walrus",
-	"avatar",
-	"chicken",
-	"proton",
-	"mohawk",
-	"tattoo",
-	"zebra",
-	"star",
-	"butter",
-	"tango",
-	"homie",
-	"rambo",
-	"cosmo",
-	"bubbles",
-	"hulk",
-	"pluto",
-	"scooby",
-	"thanos",
-	"yoda",
-	"draco",
-	"goofy",
-	"ditto",
-	"puff",
-	"duck",
-	"mouse",
-	"akita",
-	"water",
-	"hound",
-	"baby",
-	"spider",
-	"squid",
-	"roach",
-	"crab",
-	"cougar",
-	"cyborg",
-	"android",
-	"being",
-	"ninja",
-	"unicorn",
-	"zombie",
-	"warrior",
-	"zamboni",
-	"life",
-	"marine",
-	"node",
-	"mother",
-	"father",
-	"tesla",
-}
 
-// SMALL_NAMES - list of small (4 char or less) names
-var SMALL_NAMES = []string{
-	"ace",
-	"odd",
-	"hot",
-	"ill",
-	"root",
-	"sudo",
-	"moon",
-	"beef",
-	"bro",
-	"dank",
-	"red",
-	"gold",
-	"big",
-	"old",
-	"og",
-	"best",
-	"blue",
-	"lil",
-	"mom",
-	"bot",
-	"evil",
-	"good",
-	"holy",
-	"rad",
-	"bad",
-	"sad",
-	"mad",
-	"chad",
-	"pre",
-	"post",
-	"foot",
-	"soft",
-	"hard",
-	"lite",
-	"dark",
-	"true",
-	"toy",
-	"soy",
-	"rude",
-	"nice",
-	"fun",
-	"fat",
-	"pro",
-	"sly",
-	"tan",
-	"pet",
-	"fine",
-	"main",
-	"last",
-	"wide",
-	"free",
-	"open",
-	"poor",
-	"rich",
-	"next",
-	"real",
-	"long",
-	"huge",
-	"wild",
-	"sick",
-	"weak",
-	"firm",
-	"pink",
-	"okay",
-	"dull",
-	"loud",
-	"lazy",
-	"dumb",
-	"tidy",
-	"idle",
-	"bony",
-	"cute",
-	"oily",
-	"lame",
-	"mega",
-	"limp",
-	"wavy",
-	"edgy",
-	"nosy",
-	"zany",
-	"base",
-	"cold",
-}
+	"github.com/goombaio/namegenerator"
+)
 
 var logoString = retrieveLogo()
 
 // GenerateNodeName - generates a random node name
 func GenerateNodeName() string {
-	rand.Seed(time.Now().UnixNano())
-	return SMALL_NAMES[rand.Intn(len(SMALL_NAMES))] + "-" + NAMES[seededRand.Intn(len(NAMES))]
+	seed := time.Now().UTC().UnixNano()
+	nameGenerator := namegenerator.NewNameGenerator(seed)
+	return nameGenerator.Generate()
 }
 
 // RetrieveLogo - retrieves the ascii art logo for Netmaker

+ 5 - 0
models/network.go

@@ -97,3 +97,8 @@ func (network *Network) GetNetworkNetworkCIDR6() *net.IPNet {
 	_, netCidr, _ := net.ParseCIDR(network.AddressRange6)
 	return netCidr
 }
+
+type NetworkStatResp struct {
+	Network
+	Hosts int `json:"hosts"`
+}

+ 16 - 10
models/node.go

@@ -77,16 +77,18 @@ type CommonNode struct {
 // Node - a model of a network node
 type Node struct {
 	CommonNode
-	PendingDelete           bool                 `json:"pendingdelete"           bson:"pendingdelete"           yaml:"pendingdelete"`
-	LastModified            time.Time            `json:"lastmodified"            bson:"lastmodified"            yaml:"lastmodified"`
-	LastCheckIn             time.Time            `json:"lastcheckin"             bson:"lastcheckin"             yaml:"lastcheckin"`
-	LastPeerUpdate          time.Time            `json:"lastpeerupdate"          bson:"lastpeerupdate"          yaml:"lastpeerupdate"`
-	ExpirationDateTime      time.Time            `json:"expdatetime"             bson:"expdatetime"             yaml:"expdatetime"`
-	EgressGatewayNatEnabled bool                 `json:"egressgatewaynatenabled" bson:"egressgatewaynatenabled" yaml:"egressgatewaynatenabled"`
-	EgressGatewayRequest    EgressGatewayRequest `json:"egressgatewayrequest"    bson:"egressgatewayrequest"    yaml:"egressgatewayrequest"`
-	IngressGatewayRange     string               `json:"ingressgatewayrange"     bson:"ingressgatewayrange"     yaml:"ingressgatewayrange"`
-	IngressGatewayRange6    string               `json:"ingressgatewayrange6"    bson:"ingressgatewayrange6"    yaml:"ingressgatewayrange6"`
-	Metadata                string               `json:"metadata"`
+	PendingDelete              bool                 `json:"pendingdelete"           bson:"pendingdelete"           yaml:"pendingdelete"`
+	LastModified               time.Time            `json:"lastmodified"            bson:"lastmodified"            yaml:"lastmodified"`
+	LastCheckIn                time.Time            `json:"lastcheckin"             bson:"lastcheckin"             yaml:"lastcheckin"`
+	LastPeerUpdate             time.Time            `json:"lastpeerupdate"          bson:"lastpeerupdate"          yaml:"lastpeerupdate"`
+	ExpirationDateTime         time.Time            `json:"expdatetime"             bson:"expdatetime"             yaml:"expdatetime"`
+	EgressGatewayNatEnabled    bool                 `json:"egressgatewaynatenabled" bson:"egressgatewaynatenabled" yaml:"egressgatewaynatenabled"`
+	EgressGatewayRequest       EgressGatewayRequest `json:"egressgatewayrequest"    bson:"egressgatewayrequest"    yaml:"egressgatewayrequest"`
+	IngressGatewayRange        string               `json:"ingressgatewayrange"     bson:"ingressgatewayrange"     yaml:"ingressgatewayrange"`
+	IngressGatewayRange6       string               `json:"ingressgatewayrange6"    bson:"ingressgatewayrange6"    yaml:"ingressgatewayrange6"`
+	IngressPersistentKeepalive int32                `json:"ingresspersistentkeepalive"     bson:"ingresspersistentkeepalive"     yaml:"ingresspersistentkeepalive"`
+	IngressMTU                 int32                `json:"ingressmtu"     bson:"ingressmtu"     yaml:"ingressmtu"`
+	Metadata                   string               `json:"metadata"`
 	// == PRO ==
 	DefaultACL        string              `json:"defaultacl,omitempty"    bson:"defaultacl,omitempty"    yaml:"defaultacl,omitempty"    validate:"checkyesornoorunset"`
 	OwnerID           string              `json:"ownerid,omitempty"       bson:"ownerid,omitempty"       yaml:"ownerid,omitempty"`
@@ -97,6 +99,10 @@ type Node struct {
 	InetNodeReq       InetNodeReq         `json:"inet_node_req"                                          yaml:"inet_node_req"`
 	InternetGwID      string              `json:"internetgw_node_id"                                     yaml:"internetgw_node_id"`
 	AdditionalRagIps  []net.IP            `json:"additional_rag_ips"                                     yaml:"additional_rag_ips"                                     swaggertype:"array,number"`
+	Tags              map[TagID]struct{}  `json:"tags" yaml:"tags"`
+	IsStatic          bool                `json:"is_static"`
+	IsUserNode        bool                `json:"is_user_node"`
+	StaticNode        ExtClient           `json:"static_node"`
 }
 
 // LegacyNode - legacy struct for node model

+ 17 - 3
models/structs.go

@@ -45,6 +45,16 @@ type UserRemoteGws struct {
 	NetworkAddresses  []string  `json:"network_addresses"`
 }
 
+// UserRAGs - struct for user access gws
+type UserRAGs struct {
+	GwID              string `json:"remote_access_gw_id"`
+	GWName            string `json:"gw_name"`
+	Network           string `json:"network"`
+	Connected         bool   `json:"connected"`
+	IsInternetGateway bool   `json:"is_internet_gateway"`
+	Metadata          string `json:"metadata"`
+}
+
 // UserRemoteGwsReq - struct to hold user remote acccess gws req
 type UserRemoteGwsReq struct {
 	RemoteAccessClientID string `json:"remote_access_clientid"`
@@ -163,9 +173,11 @@ type HostRelayRequest struct {
 
 // IngressRequest - ingress request struct
 type IngressRequest struct {
-	ExtclientDNS      string `json:"extclientdns"`
-	IsInternetGateway bool   `json:"is_internet_gw"`
-	Metadata          string `json:"metadata"`
+	ExtclientDNS        string `json:"extclientdns"`
+	IsInternetGateway   bool   `json:"is_internet_gw"`
+	Metadata            string `json:"metadata"`
+	PersistentKeepalive int32  `json:"persistentkeepalive"`
+	MTU                 int32  `json:"mtu"`
 }
 
 // InetNodeReq - exit node request struct
@@ -254,6 +266,8 @@ type ServerConfig struct {
 	IsPro          bool   `yaml:"isee" json:"Is_EE"`
 	TrafficKey     []byte `yaml:"traffickey"`
 	MetricInterval string `yaml:"metric_interval"`
+	ManageDNS      bool   `yaml:"manage_dns"`
+	DefaultDomain  string `yaml:"default_domain"`
 }
 
 // User.NameInCharset - returns if name is in charset below or not

+ 52 - 0
models/tags.go

@@ -0,0 +1,52 @@
+package models
+
+import (
+	"fmt"
+	"time"
+)
+
+type TagID string
+
+const (
+	RemoteAccessTagName = "remote-access-gws"
+)
+
+func (id TagID) String() string {
+	return string(id)
+}
+
+func (t Tag) GetIDFromName() string {
+	return fmt.Sprintf("%s.%s", t.Network, t.TagName)
+}
+
+type Tag struct {
+	ID        TagID     `json:"id"`
+	TagName   string    `json:"tag_name"`
+	Network   NetworkID `json:"network"`
+	CreatedBy string    `json:"created_by"`
+	CreatedAt time.Time `json:"created_at"`
+}
+
+type CreateTagReq struct {
+	TagName     string    `json:"tag_name"`
+	Network     NetworkID `json:"network"`
+	TaggedNodes []ApiNode `json:"tagged_nodes"`
+}
+
+type TagListResp struct {
+	Tag
+	UsedByCnt   int       `json:"used_by_count"`
+	TaggedNodes []ApiNode `json:"tagged_nodes"`
+}
+
+type TagListRespNodes struct {
+	Tag
+	UsedByCnt   int       `json:"used_by_count"`
+	TaggedNodes []ApiNode `json:"tagged_nodes"`
+}
+
+type UpdateTagReq struct {
+	Tag
+	NewName     string    `json:"new_name"`
+	TaggedNodes []ApiNode `json:"tagged_nodes"`
+}

+ 8 - 3
models/user_mgmt.go

@@ -62,6 +62,7 @@ const (
 	EnrollmentKeysRsrc RsrcType = "enrollment_key"
 	UserRsrc           RsrcType = "users"
 	AclRsrc            RsrcType = "acl"
+	TagRsrc            RsrcType = "tag"
 	DnsRsrc            RsrcType = "dns"
 	FailOverRsrc       RsrcType = "fail_over"
 	MetricRsrc         RsrcType = "metrics"
@@ -116,8 +117,9 @@ type RsrcPermissionScope struct {
 
 type UserRolePermissionTemplate struct {
 	ID                  UserRoleID                                  `json:"id"`
-	UiName              string                                      `json:"ui_name"`
+	Name                string                                      `json:"name"`
 	Default             bool                                        `json:"default"`
+	MetaData            string                                      `json:"meta_data"`
 	DenyDashboardAccess bool                                        `json:"deny_dashboard_access"`
 	FullAccess          bool                                        `json:"full_access"`
 	NetworkID           NetworkID                                   `json:"network_id"`
@@ -132,6 +134,8 @@ type CreateGroupReq struct {
 
 type UserGroup struct {
 	ID           UserGroupID                           `json:"id"`
+	Default      bool                                  `json:"default"`
+	Name         string                                `json:"name"`
 	NetworkRoles map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
 	MetaData     string                                `json:"meta_data"`
 }
@@ -177,8 +181,9 @@ type UserAuthParams struct {
 
 // UserClaims - user claims struct
 type UserClaims struct {
-	Role     UserRoleID
-	UserName string
+	Role           UserRoleID
+	UserName       string
+	RacAutoDisable bool
 	jwt.RegisteredClaims
 }
 

+ 56 - 0
mq/publishers.go

@@ -23,6 +23,10 @@ func PublishPeerUpdate(replacePeers bool) error {
 		return nil
 	}
 
+	if servercfg.GetManageDNS() {
+		sendDNSSync()
+	}
+
 	hosts, err := logic.GetAllHosts()
 	if err != nil {
 		logger.Log(1, "err getting all hosts", err.Error())
@@ -249,3 +253,55 @@ func sendPeers() {
 		}
 	}
 }
+
+func SendDNSSyncByNetwork(network string) error {
+
+	k, err := logic.GetDNS(network)
+	if err == nil && len(k) > 0 {
+		err = PushSyncDNS(k)
+		if err != nil {
+			slog.Warn("error publishing dns entry data for network ", network, err.Error())
+		}
+	}
+
+	return err
+}
+
+func sendDNSSync() error {
+
+	networks, err := logic.GetNetworks()
+	if err == nil && len(networks) > 0 {
+		for _, v := range networks {
+			k, err := logic.GetDNS(v.NetID)
+			if err == nil && len(k) > 0 {
+				err = PushSyncDNS(k)
+				if err != nil {
+					slog.Warn("error publishing dns entry data for network ", v.NetID, err.Error())
+				}
+			}
+		}
+		return nil
+	}
+	return err
+}
+
+func PushSyncDNS(dnsEntries []models.DNSEntry) error {
+	logger.Log(2, "----> Pushing Sync DNS")
+	data, err := json.Marshal(dnsEntries)
+	if err != nil {
+		return errors.New("failed to marshal DNS entries: " + err.Error())
+	}
+	if mqclient == nil || !mqclient.IsConnectionOpen() {
+		return errors.New("cannot publish ... mqclient not connected")
+	}
+	if token := mqclient.Publish(fmt.Sprintf("host/dns/sync/%s", dnsEntries[0].Network), 0, true, data); !token.WaitTimeout(MQ_TIMEOUT*time.Second) || token.Error() != nil {
+		var err error
+		if token.Error() == nil {
+			err = errors.New("connection timeout")
+		} else {
+			err = token.Error()
+		}
+		return err
+	}
+	return nil
+}

+ 2 - 2
pro/auth/error.go

@@ -93,12 +93,12 @@ var htmlBaseTemplate = `<!DOCTYPE html>
 </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>`)
+<p>Please visit the docs <a href="https://docs.netmaker.io/docs/server-installation/integrating-oauth" target="_blank" rel="noopener">here</a> to learn how to.</p>`)
 
 var oauthStateInvalid = fmt.Sprintf(htmlBaseTemplate, `<h2>Invalid OAuth Session. Please re-try again.</h2>`)
 
 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>`)
+<p>Non-Admins can access the netmaker networks using <a href="https://docs.netmaker.io/docs/remote-access-client-rac#downloadinstallation" target="_blank" rel="noopener">our Remote Access Client.</a></p>`)
 
 var userFirstTimeSignUp = fmt.Sprintf(htmlBaseTemplate, `<h2>Thank you for signing up. Please contact your administrator for access.</h2>`)
 

+ 1 - 1
pro/auth/templates.go

@@ -118,7 +118,7 @@ var ssoErrCallbackTemplate = template.Must(
 		<h4>Error reason: {.Verb}</h4>
 		<em>Your Netmaker server may not have SSO configured properly.</em>
 		<em>
-			Please visit the <a href="https://docs.netmaker.org/oauth.html" target="_blank" rel="noopener">docs</a> for more information.
+			Please visit the <a href="https://docs.netmaker.io/docs/server-installation/integrating-oauth" target="_blank" rel="noopener">docs</a> for more information.
 		</em>
 		<p>
 			If you feel this is a mistake, please contact your network administrator.

+ 8 - 0
pro/controllers/failover.go

@@ -203,6 +203,14 @@ func failOverME(w http.ResponseWriter, r *http.Request) {
 		)
 		return
 	}
+	if peerNode.IsFailOver {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("peer is acting as failover"), "badrequest"),
+		)
+		return
+	}
 	if node.IsFailOver {
 		logic.ReturnErrorResponse(
 			w,

+ 14 - 0
pro/controllers/rac.go

@@ -0,0 +1,14 @@
+package controllers
+
+import (
+	"net/http"
+
+	"github.com/gorilla/mux"
+	"github.com/gravitl/netmaker/logic"
+)
+
+func RacHandlers(r *mux.Router) {
+	r.HandleFunc("/api/v1/rac/networks", logic.SecurityCheck(false, http.HandlerFunc(getUserRemoteAccessNetworks))).Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/rac/network/{network}/access_points", logic.SecurityCheck(false, http.HandlerFunc(getUserRemoteAccessNetworkGateways))).Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/rac/access_point/{access_point_id}/config", logic.SecurityCheck(false, http.HandlerFunc(getRemoteAccessGatewayConf))).Methods(http.MethodGet)
+}

+ 228 - 3
pro/controllers/users.go

@@ -250,8 +250,9 @@ func inviteUsers(w http.ResponseWriter, r *http.Request) {
 			// Set E-Mail body. You can set plain text or html with text/html
 
 			e := email.UserInvitedMail{
-				BodyBuilder: &email.EmailBodyBuilderWithH1HeadlineAndImage{},
-				InviteURL:   invite.InviteURL,
+				BodyBuilder:    &email.EmailBodyBuilderWithH1HeadlineAndImage{},
+				InviteURL:      invite.InviteURL,
+				PlatformRoleID: invite.PlatformRoleID,
 			}
 			n := email.Notification{
 				RecipientMail: invite.Email,
@@ -263,7 +264,6 @@ func inviteUsers(w http.ResponseWriter, r *http.Request) {
 		}(invite)
 	}
 	logic.ReturnSuccessResponse(w, r, "triggered user invites")
-
 }
 
 // swagger:route GET /api/v1/users/invites user listUserInvites
@@ -451,6 +451,10 @@ func updateUserGroup(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
+	if currUserG.Default {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot update default user group"), "badrequest"))
+		return
+	}
 	err = proLogic.ValidateUpdateGroupReq(userGroup)
 	if err != nil {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
@@ -461,6 +465,7 @@ func updateUserGroup(w http.ResponseWriter, r *http.Request) {
 		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")
@@ -496,6 +501,10 @@ func deleteUserGroup(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to fetch group details"), "badrequest"))
 		return
 	}
+	if userG.Default {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot delete default user group"), "badrequest"))
+		return
+	}
 	err = proLogic.DeleteUserGroup(models.UserGroupID(gid))
 	if err != nil {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
@@ -816,6 +825,221 @@ func removeUserFromRemoteAccessGW(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(logic.ToReturnUser(*user))
 }
 
+// @Summary     Get Users Remote Access Gw Networks.
+// @Router      /api/users/{username}/remote_access_gw [get]
+// @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 getUserRemoteAccessNetworks(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+	username := r.Header.Get("user")
+	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
+	}
+	userGws := make(map[string][]models.UserRemoteGws)
+	networks := []models.Network{}
+	networkMap := make(map[string]struct{})
+	userGwNodes := proLogic.GetUserRAGNodes(*user)
+	for _, node := range userGwNodes {
+		network, err := logic.GetNetwork(node.Network)
+		if err != nil {
+			slog.Error("failed to get node network", "error", err)
+			continue
+		}
+		if _, ok := networkMap[network.NetID]; ok {
+			continue
+		}
+		networkMap[network.NetID] = struct{}{}
+		networks = append(networks, network)
+	}
+
+	slog.Debug("returned user gws", "user", username, "gws", userGws)
+	logic.ReturnSuccessResponseWithJson(w, r, networks, "fetched user accessible networks")
+}
+
+// @Summary     Get Users Remote Access Gw Networks.
+// @Router      /api/users/{username}/remote_access_gw [get]
+// @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 getUserRemoteAccessNetworkGateways(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+	var params = mux.Vars(r)
+	username := r.Header.Get("user")
+	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
+	}
+	network := params["network"]
+	if network == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("required params network"), "badrequest"))
+		return
+	}
+	userGws := []models.UserRAGs{}
+
+	userGwNodes := proLogic.GetUserRAGNodes(*user)
+	for _, node := range userGwNodes {
+		if node.Network != network {
+			continue
+		}
+
+		host, err := logic.GetHost(node.HostID.String())
+		if err != nil {
+			continue
+		}
+
+		userGws = append(userGws, models.UserRAGs{
+			GwID:              node.ID.String(),
+			GWName:            host.Name,
+			Network:           node.Network,
+			IsInternetGateway: node.IsInternetGateway,
+			Metadata:          node.Metadata,
+		})
+
+	}
+
+	slog.Debug("returned user gws", "user", username, "gws", userGws)
+	logic.ReturnSuccessResponseWithJson(w, r, userGws, "fetched user accessible gateways in network "+network)
+}
+
+// @Summary     Get Users Remote Access Gw Networks.
+// @Router      /api/users/{username}/remote_access_gw [get]
+// @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 getRemoteAccessGatewayConf(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+	var params = mux.Vars(r)
+	username := r.Header.Get("user")
+	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
+	}
+	remoteGwID := params["access_point_id"]
+	if remoteGwID == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("required params access_point_id"), "badrequest"))
+		return
+	}
+	var req models.UserRemoteGwsReq
+	err = json.NewDecoder(r.Body).Decode(&req)
+	if err != nil {
+		slog.Error("error decoding request body: ", "error", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	userGwNodes := proLogic.GetUserRAGNodes(*user)
+	if _, ok := userGwNodes[remoteGwID]; !ok {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("access denied"), "forbidden"))
+		return
+	}
+	node, err := logic.GetNodeByID(remoteGwID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to fetch gw node %s, error: %v", remoteGwID, err), "badrequest"))
+		return
+	}
+	host, err := logic.GetHost(node.HostID.String())
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to fetch gw host %s, error: %v", remoteGwID, err), "badrequest"))
+		return
+	}
+	network, err := logic.GetNetwork(node.Network)
+	if err != nil {
+		slog.Error("failed to get node network", "error", err)
+	}
+	var userConf models.ExtClient
+	allextClients, err := logic.GetAllExtClients()
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	for _, extClient := range allextClients {
+		if extClient.Network != network.NetID || extClient.IngressGatewayID != node.ID.String() {
+			continue
+		}
+		if extClient.RemoteAccessClientID == req.RemoteAccessClientID && extClient.OwnerID == username {
+			userConf = extClient
+			userConf.AllowedIPs = logic.GetExtclientAllowedIPs(extClient)
+		}
+	}
+	if userConf.ClientID == "" {
+		// create a new conf
+		userConf.OwnerID = user.UserName
+		userConf.RemoteAccessClientID = req.RemoteAccessClientID
+		userConf.IngressGatewayID = node.ID.String()
+
+		// set extclient dns to ingressdns if extclient dns is not explicitly set
+		if (userConf.DNS == "") && (node.IngressDNS != "") {
+			userConf.DNS = node.IngressDNS
+		}
+
+		userConf.Network = node.Network
+		host, err := logic.GetHost(node.HostID.String())
+		if err != nil {
+			logger.Log(0, r.Header.Get("user"),
+				fmt.Sprintf("failed to get ingress gateway host for node [%s] info: %v", node.ID, err))
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+			return
+		}
+		listenPort := logic.GetPeerListenPort(host)
+		if host.EndpointIP.To4() == nil {
+			userConf.IngressGatewayEndpoint = fmt.Sprintf("[%s]:%d", host.EndpointIPv6.String(), listenPort)
+		} else {
+			userConf.IngressGatewayEndpoint = fmt.Sprintf("%s:%d", host.EndpointIP.String(), listenPort)
+		}
+		userConf.Enabled = true
+		parentNetwork, err := logic.GetNetwork(node.Network)
+		if err == nil { // check if parent network default ACL is enabled (yes) or not (no)
+			userConf.Enabled = parentNetwork.DefaultACL == "yes"
+		}
+		userConf.Tags = make(map[models.TagID]struct{})
+		userConf.Tags[models.TagID(fmt.Sprintf("%s.%s", userConf.Network,
+			models.RemoteAccessTagName))] = struct{}{}
+		if err = logic.CreateExtClient(&userConf); err != nil {
+			slog.Error(
+				"failed to create extclient",
+				"user",
+				r.Header.Get("user"),
+				"network",
+				node.Network,
+				"error",
+				err,
+			)
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+			return
+		}
+	}
+	userGw := models.UserRemoteGws{
+		GwID:              node.ID.String(),
+		GWName:            host.Name,
+		Network:           node.Network,
+		GwClient:          userConf,
+		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},
+	}
+
+	slog.Debug("returned user gw config", "user", user.UserName, "gws", userGw)
+	logic.ReturnSuccessResponseWithJson(w, r, userGw, "fetched user config to gw "+remoteGwID)
+}
+
 // @Summary     Get Users Remote Access Gw.
 // @Router      /api/users/{username}/remote_access_gw [get]
 // @Tags        Users
@@ -876,6 +1100,7 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 			network, err := logic.GetNetwork(node.Network)
 			if err != nil {
 				slog.Error("failed to get node network", "error", err)
+				continue
 			}
 
 			gws := userGws[node.Network]

+ 39 - 44
pro/email/invite.go

@@ -2,65 +2,60 @@ package email
 
 import (
 	"fmt"
+	"github.com/gravitl/netmaker/models"
+	proLogic "github.com/gravitl/netmaker/pro/logic"
 	"github.com/gravitl/netmaker/servercfg"
 )
 
 // UserInvitedMail - mail for users that are invited to a tenant
 type UserInvitedMail struct {
-	BodyBuilder EmailBodyBuilder
-	InviteURL   string
+	BodyBuilder    EmailBodyBuilder
+	InviteURL      string
+	PlatformRoleID string
 }
 
 // GetSubject - gets the subject of the email
 func (UserInvitedMail) GetSubject(info Notification) string {
-	return "You're invited to join Netmaker"
+	return "Connect to Your Secure Network Using Netmaker"
 }
 
 // GetBody - gets the body of the email
 func (invite UserInvitedMail) GetBody(info Notification) string {
+	downloadLink := "https://www.netmaker.io/download"
+	supportEmail := "[email protected]"
+
+	dashboardURL := fmt.Sprintf("https://dashboard.%s", servercfg.GetNmBaseDomain())
 	if servercfg.DeployedByOperator() {
-		return invite.BodyBuilder.
-			WithParagraph("Hi there,").
-			WithParagraph("<br>").
-			WithParagraph("Great news! Your colleague has invited you to join their Netmaker SaaS Tenant.").
-			WithParagraph("Click the button to accept your invitation:").
-			WithParagraph("<br>").
-			WithParagraph(fmt.Sprintf("<a class=\"x-button\" href=\"%s\">Accept Invitation</a>", invite.InviteURL)).
-			WithParagraph("<br>").
-			WithParagraph("Why you'll love Netmaker:").
-			WithParagraph("<ul>").
-			WithParagraph("<li>Blazing-fast connections with our WireGuard®-powered mesh VPN</li>").
-			WithParagraph("<li>Seamless multi-cloud and hybrid-cloud networking</li>").
-			WithParagraph("<li>Automated Kubernetes networking across any infrastructure</li>").
-			WithParagraph("<li>Enterprise-grade security with simple management</li>").
-			WithParagraph("</ul>").
-			WithParagraph("Got questions? Our team is here to help you every step of the way.").
-			WithParagraph("<br>").
-			WithParagraph("Welcome aboard,").
-			WithParagraph("<h2>The Netmaker Team</h2>").
-			WithParagraph("P.S. Curious to learn more before accepting? Check out our quick start tutorial at <a href=\"https://netmaker.io/tutorials\">netmaker.io/tutorials</a>").
-			Build()
+		dashboardURL = fmt.Sprintf("%s/dashboard?tenant_id=%s", proLogic.GetAccountsUIHost(), servercfg.GetNetmakerTenantID())
+	}
+
+	content := invite.BodyBuilder.
+		WithParagraph("Hi,").
+		WithParagraph("You've been invited to access a secure network via Netmaker's Remote Access Client (RAC). Follow these simple steps to get connected:").
+		WithHtml("<ol>").
+		WithHtml(fmt.Sprintf("<li>Click <a href=\"%s\">here</a> to accept your invitation and setup your account.</li>", invite.InviteURL)).
+		WithHtml("<br>").
+		WithHtml(fmt.Sprintf("<li><a href=\"%s\">Download the Remote Access Client (RAC)</a>.</li>", downloadLink))
+
+	if invite.PlatformRoleID == models.AdminRole.String() || invite.PlatformRoleID == models.PlatformUser.String() {
+		content = content.
+			WithHtml("<br>").
+			WithHtml(fmt.Sprintf("<li>Access the <a href=\"%s\">Netmaker Dashboard</a> - use it to manage your network settings and view network status.</li>", dashboardURL))
+	}
+
+	connectionID := servercfg.GetNetmakerTenantID()
+	if !servercfg.DeployedByOperator() {
+		connectionID = fmt.Sprintf("api.%s", servercfg.GetNmBaseDomain())
 	}
 
-	return invite.BodyBuilder.
-		WithParagraph("Hi there,").
-		WithParagraph("<br>").
-		WithParagraph("Great news! Your colleague has invited you to join their Netmaker network.").
-		WithParagraph("Click the button to accept your invitation:").
-		WithParagraph("<br>").
-		WithParagraph(fmt.Sprintf("<a class=\"x-button\" href=\"%s\">Accept Invitation</a>", invite.InviteURL)).
-		WithParagraph("<br>").
-		WithParagraph("Why you'll love Netmaker:").
-		WithParagraph("<ul>").
-		WithParagraph("<li>Blazing-fast connections with our WireGuard®-powered mesh VPN</li>").
-		WithParagraph("<li>Seamless multi-cloud and hybrid-cloud networking</li>").
-		WithParagraph("<li>Automated Kubernetes networking across any infrastructure</li>").
-		WithParagraph("<li>Enterprise-grade security with simple management</li>").
-		WithParagraph("</ul>").
-		WithParagraph("Got questions? Our team is here to help you every step of the way.").
-		WithParagraph("<br>").
-		WithParagraph("Welcome aboard,").
-		WithParagraph("<h2>The Netmaker Team</h2>").
-		WithParagraph("P.S. Curious to learn more before accepting? Check out our quick start tutorial at <a href=\"https://netmaker.io/tutorials\">netmaker.io/tutorials</a>").
+	return content.
+		WithHtml("</ol>").
+		WithParagraph("Important Information:").
+		WithHtml("<ul>").
+		WithHtml(fmt.Sprintf("<li>When connecting through RAC, please enter your server connection ID: %s.</li>", connectionID)).
+		WithHtml("</ul>").
+		WithParagraph(fmt.Sprintf("If you have any questions or need assistance, please contact our support team at <a href=\"mailto:%s\">%s</a>.", supportEmail, supportEmail)).
+		WithParagraph("Best Regards,").
+		WithParagraph("The Netmaker Team").
 		Build()
 }

+ 129 - 521
pro/email/utils.go

@@ -4,27 +4,20 @@ 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
+	WithHtml(text string) EmailBodyBuilder
 	WithSignature() EmailBodyBuilder
 	Build() string
 }
 
 type EmailBodyBuilderWithH1HeadlineAndImage struct {
 	headline     string
-	paragraphs   []string
+	bodyContent  []string
 	hasSignature bool
 }
 
@@ -34,7 +27,12 @@ func (b *EmailBodyBuilderWithH1HeadlineAndImage) WithHeadline(text string) Email
 }
 
 func (b *EmailBodyBuilderWithH1HeadlineAndImage) WithParagraph(text string) EmailBodyBuilder {
-	b.paragraphs = append(b.paragraphs, text)
+	b.bodyContent = append(b.bodyContent, styledParagraph(text))
+	return b
+}
+
+func (b *EmailBodyBuilderWithH1HeadlineAndImage) WithHtml(text string) EmailBodyBuilder {
+	b.bodyContent = append(b.bodyContent, text)
 	return b
 }
 
@@ -44,524 +42,134 @@ func (b *EmailBodyBuilderWithH1HeadlineAndImage) WithSignature() EmailBodyBuilde
 }
 
 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, "")
+	bodyContent := strings.Join(b.bodyContent, "")
 
-	signature := ""
-	if b.hasSignature {
-		signature = styledSignature()
-	}
+	// TODO: Edit design to add signature.
+	//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}} .x-button{background:#5E5DF0;border-radius:999px;box-shadow:#5E5DF0 0 10px 20px -10px;box-sizing:border-box;color:#FFFFFF !important;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 20px 20px 0px;text-decoration:none;}
-		    </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>`
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+    <title>Simple Transactional Email</title>
+    <style media="all" type="text/css">
+@media all {
+  .btn-primary table td:hover {
+    background-color: #ec0867 !important;
+  }
+
+  .btn-primary a:hover {
+    background-color: #ec0867 !important;
+    border-color: #ec0867 !important;
+  }
 }
+@media only screen and (max-width: 640px) {
+  .main p,
+.main td,
+.main span {
+    font-size: 16px !important;
+  }
 
-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>`
+  .wrapper {
+    padding: 8px !important;
+  }
+
+  .content {
+    padding: 0 !important;
+  }
+
+  .container {
+    padding: 0 !important;
+    padding-top: 8px !important;
+    width: 100% !important;
+  }
+
+  .main {
+    border-left-width: 0 !important;
+    border-radius: 0 !important;
+    border-right-width: 0 !important;
+  }
+
+  .btn table {
+    max-width: 100% !important;
+    width: 100% !important;
+  }
+
+  .btn a {
+    font-size: 16px !important;
+    max-width: 100% !important;
+    width: 100% !important;
+  }
 }
+@media all {
+  .ExternalClass {
+    width: 100%;
+  }
 
-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>`
+  .ExternalClass,
+.ExternalClass p,
+.ExternalClass span,
+.ExternalClass font,
+.ExternalClass td,
+.ExternalClass div {
+    line-height: 100%;
+  }
+
+  .apple-link a {
+    color: inherit !important;
+    font-family: inherit !important;
+    font-size: inherit !important;
+    font-weight: inherit !important;
+    line-height: inherit !important;
+    text-decoration: none !important;
+  }
+
+  #MessageViewBody a {
+    color: inherit;
+    text-decoration: none;
+    font-size: inherit;
+    font-family: inherit;
+    font-weight: inherit;
+    line-height: inherit;
+  }
 }
+</style>
+  </head>
+  <body style="font-family: Helvetica, sans-serif; -webkit-font-smoothing: antialiased; font-size: 16px; line-height: 1.3; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; background-color: #f4f5f6; margin: 0; padding: 0;">
+    <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f4f5f6; width: 100%;" width="100%" bgcolor="#f4f5f6">
+      <tr>
+        <td style="font-family: Helvetica, sans-serif; font-size: 16px; vertical-align: top;" valign="top">&nbsp;</td>
+        <td class="container" style="font-family: Helvetica, sans-serif; font-size: 16px; vertical-align: top; max-width: 600px; padding: 24px 0px 24px 0px; width: 600px; margin: 0 auto;" width="600" valign="top">
+          <div class="content" style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 600px; padding: 0;">
+
+            <!-- START CENTERED WHITE CONTAINER -->
+            <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background: #ffffff; border: 1px solid #eaebed; border-radius: 16px; width: 100%;" width="100%">
+
+              <!-- START MAIN CONTENT AREA -->
+              <tr>
+                <td class="wrapper" style="font-family: Helvetica, sans-serif; font-size: 16px; vertical-align: top; box-sizing: border-box; padding: 24px;" valign="top">
+                  <img src="` + netmakerLogoTeal + `" alt="Netmaker Logo" width="200" height="100" border="0" style="border:0; outline:none; text-decoration:none; display:block; margin-left: auto;">
+                  ` + bodyContent + `
+                </td>
+              </tr>
 
-func GetMailSignature() string {
-	return styledSignature()
+              <!-- END MAIN CONTENT AREA -->
+              </table>
+
+<!-- END CENTERED WHITE CONTAINER --></div>
+        </td>
+        <td style="font-family: Helvetica, sans-serif; font-size: 16px; vertical-align: top;" valign="top">&nbsp;</td>
+      </tr>
+    </table>
+  </body>
+</html>`
+}
+
+func styledParagraph(text string) string {
+	return `<p style="font-family: Helvetica, sans-serif; font-size: 16px; font-weight: normal; margin: 0; margin-bottom: 16px;">` + text + `</p>`
 }

+ 13 - 6
pro/initialize.go

@@ -33,16 +33,17 @@ func InitPro() {
 		proControllers.UserHandlers,
 		proControllers.FailOverHandlers,
 		proControllers.InetHandlers,
+		proControllers.RacHandlers,
 	)
 	controller.ListRoles = proControllers.ListRoles
 	logic.EnterpriseCheckFuncs = append(logic.EnterpriseCheckFuncs, func() {
 		// == License Handling ==
-		enableLicenseHook := false
-		licenseKeyValue := servercfg.GetLicenseKey()
-		netmakerTenantID := servercfg.GetNetmakerTenantID()
-		if licenseKeyValue != "" && netmakerTenantID != "" {
-			enableLicenseHook = true
-		}
+		enableLicenseHook := true
+		// licenseKeyValue := servercfg.GetLicenseKey()
+		// netmakerTenantID := servercfg.GetNetmakerTenantID()
+		// if licenseKeyValue != "" && netmakerTenantID != "" {
+		// 	enableLicenseHook = true
+		// }
 		if !enableLicenseHook {
 			err := initTrial()
 			if err != nil {
@@ -130,9 +131,15 @@ func InitPro() {
 	logic.CreateDefaultNetworkRolesAndGroups = proLogic.CreateDefaultNetworkRolesAndGroups
 	logic.FilterNetworksByRole = proLogic.FilterNetworksByRole
 	logic.IsGroupsValid = proLogic.IsGroupsValid
+	logic.IsGroupValid = proLogic.IsGroupValid
 	logic.IsNetworkRolesValid = proLogic.IsNetworkRolesValid
 	logic.InitialiseRoles = proLogic.UserRolesInit
 	logic.UpdateUserGwAccess = proLogic.UpdateUserGwAccess
+	logic.CreateDefaultUserPolicies = proLogic.CreateDefaultUserPolicies
+	logic.MigrateUserRoleAndGroups = proLogic.MigrateUserRoleAndGroups
+	logic.IntialiseGroups = proLogic.UserGroupsInit
+	logic.AddGlobalNetRolesToAdmins = proLogic.AddGlobalNetRolesToAdmins
+	logic.GetUserGroupsInNetwork = proLogic.GetUserGroupsInNetwork
 }
 
 func retrieveProLogo() string {

+ 1 - 1
pro/logic/ext_acls.go

@@ -62,7 +62,7 @@ func SetClientDefaultACLs(ec *models.ExtClient) error {
 		slog.Error("failed to get network acls", "error", err)
 		return err
 	}
-	networkAcls[acls.AclID(ec.ClientID)] = acls.ACL{}
+	networkAcls[acls.AclID(ec.ClientID)] = make(acls.ACL)
 	for i := range networkNodes {
 		currNode := networkNodes[i]
 		if network.DefaultACL == "no" || currNode.DefaultACL == "no" {

+ 8 - 5
pro/logic/metrics.go

@@ -2,7 +2,6 @@ package logic
 
 import (
 	"encoding/json"
-	"math"
 	"sync"
 	"time"
 
@@ -209,15 +208,17 @@ func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) {
 		currMetric.TotalTime += oldMetric.TotalTime
 		currMetric.Uptime += oldMetric.Uptime // get the total uptime for this connection
 
-		if currMetric.TotalReceived < oldMetric.TotalReceived {
+		totalRecv := currMetric.TotalReceived
+		totalSent := currMetric.TotalSent
+		if currMetric.TotalReceived < oldMetric.TotalReceived && currMetric.TotalReceived < oldMetric.LastTotalReceived {
 			currMetric.TotalReceived += oldMetric.TotalReceived
 		} else {
-			currMetric.TotalReceived += int64(math.Abs(float64(currMetric.TotalReceived) - float64(oldMetric.TotalReceived)))
+			currMetric.TotalReceived = currMetric.TotalReceived - oldMetric.LastTotalReceived + oldMetric.TotalReceived
 		}
-		if currMetric.TotalSent < oldMetric.TotalSent {
+		if currMetric.TotalSent < oldMetric.TotalSent && currMetric.TotalSent < oldMetric.LastTotalSent {
 			currMetric.TotalSent += oldMetric.TotalSent
 		} else {
-			currMetric.TotalSent += int64(math.Abs(float64(currMetric.TotalSent) - float64(oldMetric.TotalSent)))
+			currMetric.TotalSent = currMetric.TotalSent - oldMetric.LastTotalSent + oldMetric.TotalSent
 		}
 
 		if currMetric.Uptime == 0 || currMetric.TotalTime == 0 {
@@ -228,6 +229,8 @@ func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) {
 		totalUpMinutes := currMetric.Uptime * ncutils.CheckInInterval
 		currMetric.ActualUptime = time.Duration(totalUpMinutes) * time.Minute
 		delete(oldMetrics.Connectivity, k) // remove from old data
+		currMetric.LastTotalReceived = totalRecv
+		currMetric.LastTotalSent = totalSent
 		newMetrics.Connectivity[k] = currMetric
 
 	}

+ 68 - 0
pro/logic/migrate.go

@@ -0,0 +1,68 @@
+package logic
+
+import (
+	"fmt"
+
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/models"
+)
+
+func MigrateUserRoleAndGroups(user models.User) {
+	var err error
+	if user.PlatformRoleID == models.AdminRole || user.PlatformRoleID == models.SuperAdminRole {
+		return
+	}
+	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
+			}
+			var g models.UserGroup
+			if user.PlatformRoleID == models.ServiceUser {
+				g, err = GetUserGroup(models.UserGroupID(fmt.Sprintf("%s-%s-grp", gwNode.Network, models.NetworkUser)))
+			} else {
+				g, err = GetUserGroup(models.UserGroupID(fmt.Sprintf("%s-%s-grp",
+					gwNode.Network, models.NetworkAdmin)))
+			}
+			if err != nil {
+				continue
+			}
+			user.UserGroups[g.ID] = struct{}{}
+		}
+	}
+	if len(user.NetworkRoles) > 0 {
+		for netID, netRoles := range user.NetworkRoles {
+			var g models.UserGroup
+			adminAccess := false
+			for netRoleID := range netRoles {
+				permTemplate, err := logic.GetRole(netRoleID)
+				if err == nil {
+					if permTemplate.FullAccess {
+						adminAccess = true
+					}
+				}
+			}
+
+			if user.PlatformRoleID == models.ServiceUser {
+				g, err = GetUserGroup(models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkUser)))
+			} else {
+				role := models.NetworkUser
+				if adminAccess {
+					role = models.NetworkAdmin
+				}
+				g, err = GetUserGroup(models.UserGroupID(fmt.Sprintf("%s-%s-grp",
+					netID, role)))
+			}
+			if err != nil {
+				continue
+			}
+			user.UserGroups[g.ID] = struct{}{}
+			user.NetworkRoles = make(map[models.NetworkID]map[models.UserRoleID]struct{})
+		}
+
+	}
+	logic.UpsertUser(user)
+}

+ 12 - 0
pro/logic/security.go

@@ -50,6 +50,9 @@ func NetworkPermissionsCheck(username string, r *http.Request) error {
 	if targetRsrc == "" {
 		return errors.New("target rsrc is missing")
 	}
+	if r.Header.Get("RAC") == "true" && r.Method == http.MethodGet {
+		return nil
+	}
 	if netID == "" {
 		return errors.New("network id is missing")
 	}
@@ -79,8 +82,17 @@ func NetworkPermissionsCheck(username string, r *http.Request) error {
 		}
 	}
 	for groupID := range user.UserGroups {
+
 		userG, err := GetUserGroup(groupID)
 		if err == nil {
+			if netRoles, ok := userG.NetworkRoles[models.AllNetworks]; ok {
+				for netRoleID := range netRoles {
+					err = checkNetworkAccessPermissions(netRoleID, username, r.Method, targetRsrc, targetRsrcID, netID)
+					if err == nil {
+						return nil
+					}
+				}
+			}
 			netRoles := userG.NetworkRoles[models.NetworkID(netID)]
 			for netRoleID := range netRoles {
 				err = checkNetworkAccessPermissions(netRoleID, username, r.Method, targetRsrc, targetRsrcID, netID)

+ 205 - 8
pro/logic/user_mgmt.go

@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"time"
 
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
@@ -29,6 +30,8 @@ var PlatformUserUserPermissionTemplate = models.UserRolePermissionTemplate{
 
 var NetworkAdminAllPermissionTemplate = models.UserRolePermissionTemplate{
 	ID:         models.UserRoleID(fmt.Sprintf("global-%s", models.NetworkAdmin)),
+	Name:       "Network Admins",
+	MetaData:   "can manage configuration of all networks",
 	Default:    true,
 	FullAccess: true,
 	NetworkID:  models.AllNetworks,
@@ -36,6 +39,8 @@ var NetworkAdminAllPermissionTemplate = models.UserRolePermissionTemplate{
 
 var NetworkUserAllPermissionTemplate = models.UserRolePermissionTemplate{
 	ID:         models.UserRoleID(fmt.Sprintf("global-%s", models.NetworkUser)),
+	Name:       "Network Users",
+	MetaData:   "cannot access the admin console, but can connect to nodes in your networks via Remote Access Client.",
 	Default:    true,
 	FullAccess: false,
 	NetworkID:  models.AllNetworks,
@@ -74,12 +79,44 @@ func UserRolesInit() {
 
 }
 
+func UserGroupsInit() {
+	// create default network groups
+	var NetworkGlobalAdminGroup = models.UserGroup{
+		ID:       models.UserGroupID(fmt.Sprintf("global-%s-grp", models.NetworkAdmin)),
+		Default:  true,
+		Name:     "All Networks Admin Group",
+		MetaData: "can manage configuration of all networks",
+		NetworkRoles: map[models.NetworkID]map[models.UserRoleID]struct{}{
+			models.AllNetworks: {
+				models.UserRoleID(fmt.Sprintf("global-%s", models.NetworkAdmin)): {},
+			},
+		},
+	}
+	var NetworkGlobalUserGroup = models.UserGroup{
+		ID:      models.UserGroupID(fmt.Sprintf("global-%s-grp", models.NetworkUser)),
+		Name:    "All Networks User Group",
+		Default: true,
+		NetworkRoles: map[models.NetworkID]map[models.UserRoleID]struct{}{
+			models.NetworkID(models.AllNetworks): {
+				models.UserRoleID(fmt.Sprintf("global-%s", models.NetworkUser)): {},
+			},
+		},
+		MetaData: "cannot access the admin console, but can connect to nodes in your networks via Remote Access Client.",
+	}
+	d, _ := json.Marshal(NetworkGlobalAdminGroup)
+	database.Insert(NetworkGlobalAdminGroup.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME)
+	d, _ = json.Marshal(NetworkGlobalUserGroup)
+	database.Insert(NetworkGlobalUserGroup.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME)
+}
+
 func CreateDefaultNetworkRolesAndGroups(netID models.NetworkID) {
 	if netID.String() == "" {
 		return
 	}
 	var NetworkAdminPermissionTemplate = models.UserRolePermissionTemplate{
 		ID:                 models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkAdmin)),
+		Name:               fmt.Sprintf("%s Admin", netID),
+		MetaData:           fmt.Sprintf("can manage your network `%s` configuration.", netID),
 		Default:            true,
 		NetworkID:          netID,
 		FullAccess:         true,
@@ -88,6 +125,8 @@ func CreateDefaultNetworkRolesAndGroups(netID models.NetworkID) {
 
 	var NetworkUserPermissionTemplate = models.UserRolePermissionTemplate{
 		ID:                  models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkUser)),
+		Name:                fmt.Sprintf("%s User", netID),
+		MetaData:            fmt.Sprintf("cannot access the admin console, but can connect to nodes in your network `%s` via Remote Access Client.", netID),
 		Default:             true,
 		FullAccess:          false,
 		NetworkID:           netID,
@@ -117,27 +156,30 @@ func CreateDefaultNetworkRolesAndGroups(netID models.NetworkID) {
 
 	// create default network groups
 	var NetworkAdminGroup = models.UserGroup{
-		ID: models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkAdmin)),
+		ID:   models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkAdmin)),
+		Name: fmt.Sprintf("%s Admin Group", netID),
 		NetworkRoles: map[models.NetworkID]map[models.UserRoleID]struct{}{
 			netID: {
 				models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkAdmin)): {},
 			},
 		},
-		MetaData: "The network group was automatically created by Netmaker.",
+		MetaData: fmt.Sprintf("can manage your network `%s` configuration including adding and removing devices.", netID),
 	}
 	var NetworkUserGroup = models.UserGroup{
-		ID: models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkUser)),
+		ID:   models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkUser)),
+		Name: fmt.Sprintf("%s User Group", netID),
 		NetworkRoles: map[models.NetworkID]map[models.UserRoleID]struct{}{
 			netID: {
 				models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkUser)): {},
 			},
 		},
-		MetaData: "The network group was automatically created by Netmaker.",
+		MetaData: fmt.Sprintf("cannot access the admin console, but can connect to nodes in your network `%s` via Remote Access Client.", netID),
 	}
 	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) {
@@ -403,7 +445,6 @@ func ValidateCreateGroupReq(g models.UserGroup) error {
 	return nil
 }
 func ValidateUpdateGroupReq(g models.UserGroup) error {
-
 	for networkID := range g.NetworkRoles {
 		userRolesMap := g.NetworkRoles[networkID]
 		for roleID := range userRolesMap {
@@ -511,6 +552,49 @@ func HasNetworkRsrcScope(permissionTemplate models.UserRolePermissionTemplate, n
 	_, ok = rsrcScope[rsrcID]
 	return ok
 }
+
+func GetUserRAGNodesV1(user models.User) (gws map[string]models.Node) {
+	gws = make(map[string]models.Node)
+	nodes, err := logic.GetAllNodes()
+	if err != nil {
+		return
+	}
+	if user.PlatformRoleID == models.AdminRole || user.PlatformRoleID == models.SuperAdminRole {
+		for _, node := range nodes {
+			if node.IsIngressGateway {
+				gws[node.ID.String()] = node
+			}
+
+		}
+	}
+	tagNodesMap := logic.GetTagMapWithNodes()
+	accessPolices := logic.ListUserPolicies(user)
+	for _, policyI := range accessPolices {
+		if !policyI.Enabled {
+			continue
+		}
+		for _, dstI := range policyI.Dst {
+			if dstI.Value == "*" {
+				networkNodes := logic.GetNetworkNodesMemory(nodes, policyI.NetworkID.String())
+				for _, node := range networkNodes {
+					if node.IsIngressGateway {
+						gws[node.ID.String()] = node
+					}
+				}
+			}
+			if nodes, ok := tagNodesMap[models.TagID(dstI.Value)]; ok {
+				for _, node := range nodes {
+					if node.IsIngressGateway {
+						gws[node.ID.String()] = node
+					}
+
+				}
+			}
+		}
+	}
+	return
+}
+
 func GetUserRAGNodes(user models.User) (gws map[string]models.Node) {
 	gws = make(map[string]models.Node)
 	userGwAccessScope := GetUserNetworkRolesWithRemoteVPNAccess(user)
@@ -555,6 +639,7 @@ func GetUserNetworkRolesWithRemoteVPNAccess(user models.User) (gwAccess map[mode
 	}
 	if _, ok := user.NetworkRoles[models.AllNetworks]; ok {
 		gwAccess[models.NetworkID("*")] = make(map[models.RsrcID]models.RsrcPermissionScope)
+		return
 	}
 	if len(user.UserGroups) > 0 {
 		for gID := range user.UserGroups {
@@ -562,6 +647,10 @@ func GetUserNetworkRolesWithRemoteVPNAccess(user models.User) (gwAccess map[mode
 			if err != nil {
 				continue
 			}
+			if _, ok := userG.NetworkRoles[models.AllNetworks]; ok {
+				gwAccess[models.NetworkID("*")] = make(map[models.RsrcID]models.RsrcPermissionScope)
+				return
+			}
 			for netID, roleMap := range userG.NetworkRoles {
 				for roleID := range roleMap {
 					role, err := logic.GetRole(roleID)
@@ -662,7 +751,9 @@ func GetFilteredNodesByUserAccess(user models.User, nodes []models.Node) (filter
 
 	nodesMap := make(map[string]struct{})
 	allNetworkRoles := make(map[models.UserRoleID]struct{})
-
+	defer func() {
+		filteredNodes = logic.AddStaticNodestoList(filteredNodes)
+	}()
 	if len(user.NetworkRoles) > 0 {
 		for _, netRoles := range user.NetworkRoles {
 			for netRoleI := range netRoles {
@@ -671,7 +762,8 @@ func GetFilteredNodesByUserAccess(user models.User, nodes []models.Node) (filter
 		}
 	}
 	if _, ok := user.NetworkRoles[models.AllNetworks]; ok {
-		return nodes
+		filteredNodes = nodes
+		return
 	}
 	if len(user.UserGroups) > 0 {
 		for userGID := range user.UserGroups {
@@ -679,7 +771,8 @@ func GetFilteredNodesByUserAccess(user models.User, nodes []models.Node) (filter
 			if err == nil {
 				if len(userG.NetworkRoles) > 0 {
 					if _, ok := userG.NetworkRoles[models.AllNetworks]; ok {
-						return nodes
+						filteredNodes = nodes
+						return
 					}
 					for _, netRoles := range userG.NetworkRoles {
 						for netRoleI := range netRoles {
@@ -792,6 +885,16 @@ func IsGroupsValid(groups map[models.UserGroupID]struct{}) error {
 	return nil
 }
 
+func IsGroupValid(groupID models.UserGroupID) error {
+
+	_, 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 {
 
@@ -1036,3 +1139,97 @@ func UpdateUserGwAccess(currentUser, changeUser models.User) {
 	}
 
 }
+
+func CreateDefaultUserPolicies(netID models.NetworkID) {
+	if netID.String() == "" {
+		return
+	}
+
+	if !logic.IsAclExists(fmt.Sprintf("%s.%s-grp", netID, models.NetworkAdmin)) {
+		defaultUserAcl := models.Acl{
+			ID:        fmt.Sprintf("%s.%s-grp", netID, models.NetworkAdmin),
+			Name:      "Network Admin",
+			MetaData:  "This Policy allows all network admins to communicate with all remote access gateways",
+			Default:   true,
+			NetworkID: netID,
+			RuleType:  models.UserPolicy,
+			Src: []models.AclPolicyTag{
+				{
+					ID:    models.UserGroupAclID,
+					Value: fmt.Sprintf("%s-%s-grp", netID, models.NetworkAdmin),
+				},
+				{
+					ID:    models.UserGroupAclID,
+					Value: fmt.Sprintf("global-%s-grp", models.NetworkAdmin),
+				},
+			},
+			Dst: []models.AclPolicyTag{
+				{
+					ID:    models.DeviceAclID,
+					Value: fmt.Sprintf("%s.%s", netID, models.RemoteAccessTagName),
+				}},
+			AllowedDirection: models.TrafficDirectionUni,
+			Enabled:          true,
+			CreatedBy:        "auto",
+			CreatedAt:        time.Now().UTC(),
+		}
+		logic.InsertAcl(defaultUserAcl)
+	}
+
+	if !logic.IsAclExists(fmt.Sprintf("%s.%s-grp", netID, models.NetworkUser)) {
+		defaultUserAcl := models.Acl{
+			ID:        fmt.Sprintf("%s.%s-grp", netID, models.NetworkUser),
+			Name:      "Network User",
+			MetaData:  "This Policy allows all network users to communicate with all remote access gateways",
+			Default:   true,
+			NetworkID: netID,
+			RuleType:  models.UserPolicy,
+			Src: []models.AclPolicyTag{
+				{
+					ID:    models.UserGroupAclID,
+					Value: fmt.Sprintf("%s-%s-grp", netID, models.NetworkUser),
+				},
+				{
+					ID:    models.UserGroupAclID,
+					Value: fmt.Sprintf("global-%s-grp", models.NetworkUser),
+				},
+			},
+
+			Dst: []models.AclPolicyTag{
+				{
+					ID:    models.DeviceAclID,
+					Value: fmt.Sprintf("%s.%s", netID, models.RemoteAccessTagName),
+				}},
+			AllowedDirection: models.TrafficDirectionUni,
+			Enabled:          true,
+			CreatedBy:        "auto",
+			CreatedAt:        time.Now().UTC(),
+		}
+		logic.InsertAcl(defaultUserAcl)
+	}
+
+}
+
+func GetUserGroupsInNetwork(netID models.NetworkID) (networkGrps map[models.UserGroupID]models.UserGroup) {
+	groups, _ := ListUserGroups()
+	networkGrps = make(map[models.UserGroupID]models.UserGroup)
+	for _, grp := range groups {
+		if _, ok := grp.NetworkRoles[models.AllNetworks]; ok {
+			networkGrps[grp.ID] = grp
+			continue
+		}
+		if _, ok := grp.NetworkRoles[netID]; ok {
+			networkGrps[grp.ID] = grp
+		}
+	}
+	return
+}
+
+func AddGlobalNetRolesToAdmins(u models.User) {
+	if u.PlatformRoleID != models.SuperAdminRole && u.PlatformRoleID != models.AdminRole {
+		return
+	}
+	u.UserGroups = make(map[models.UserGroupID]struct{})
+	u.UserGroups[models.UserGroupID(fmt.Sprintf("global-%s-grp", models.NetworkAdmin))] = struct{}{}
+	logic.UpsertUser(u)
+}

+ 7 - 2
pro/remote_access_client.go

@@ -43,13 +43,18 @@ func racAutoDisableHook() error {
 	currentTime := time.Now()
 	validityDuration := servercfg.GetJwtValidityDuration()
 	for _, user := range users {
+		if user.PlatformRoleID == models.AdminRole ||
+			user.PlatformRoleID == models.SuperAdminRole {
+			continue
+		}
 		if !currentTime.After(user.LastLoginTime.Add(validityDuration)) {
 			continue
 		}
 		for _, client := range clients {
+			if client.RemoteAccessClientID == "" {
+				continue
+			}
 			if (client.OwnerID == user.UserName) &&
-				user.PlatformRoleID != models.SuperAdminRole &&
-				user.PlatformRoleID != models.AdminRole &&
 				client.Enabled {
 				slog.Info(fmt.Sprintf("disabling ext client %s for user %s due to RAC autodisabling", client.ClientID, client.OwnerID))
 				if err := disableExtClient(&client); err != nil {

+ 6 - 6
pro/types.go

@@ -4,7 +4,7 @@
 package pro
 
 import (
-	"fmt"
+	"errors"
 )
 
 const (
@@ -13,7 +13,7 @@ const (
 	server_id_key              = "nm-server-id"
 )
 
-var errValidation = fmt.Errorf(license_validation_err_msg)
+var errValidation = errors.New(license_validation_err_msg)
 
 // LicenseKey - the license key struct representation with associated data
 type LicenseKey struct {
@@ -32,14 +32,14 @@ type LicenseKey struct {
 
 // ValidatedLicense - the validated license struct
 type ValidatedLicense struct {
-	LicenseValue     string `json:"license_value" binding:"required"`     // license that validation is being requested for
+	LicenseValue     string `json:"license_value"     binding:"required"` // license that validation is being requested for
 	EncryptedLicense string `json:"encrypted_license" binding:"required"` // to be decrypted by Netmaker using Netmaker server's private key
 }
 
 // LicenseSecret - the encrypted struct for sending user-id
 type LicenseSecret struct {
 	AssociatedID string `json:"associated_id" binding:"required"` // UUID for user foreign key to User table
-	Usage        Usage  `json:"limits" binding:"required"`
+	Usage        Usage  `json:"limits"        binding:"required"`
 }
 
 // Usage - struct for license usage
@@ -71,9 +71,9 @@ func (l *Usage) SetDefaults() {
 
 // ValidateLicenseRequest - used for request to validate license endpoint
 type ValidateLicenseRequest struct {
-	LicenseKey     string `json:"license_key" binding:"required"`
+	LicenseKey     string `json:"license_key"       binding:"required"`
 	NmServerPubKey string `json:"nm_server_pub_key" binding:"required"` // Netmaker server public key used to send data back to Netmaker for the Netmaker server to decrypt (eg output from validating license)
-	EncryptedPart  string `json:"secret" binding:"required"`
+	EncryptedPart  string `json:"secret"            binding:"required"`
 }
 
 type licenseResponseCache struct {

+ 9 - 9
release.md

@@ -1,21 +1,21 @@
-# Netmaker v0.25.0
+# Netmaker v0.26.0
 
 ## Whats New ✨
-- Advanced User Management with Network Roles and Groups
-- User Invitation via Email and Magic Links
+- New ACLs and Tag Management System
+- Managed DNS system (Linux)
+- Simplified User Mgmt With Default Roles and Groups (Hidden away network roles)
+- New Add a Node Flow for netclient and static wireguard files
 
 ## What's Fixed/Improved 🛠
-
-- Scalability Improvements
-- Optimised Traffic Flow Over MQ
-- Improved Peer Updates with Batching
+- Metrics Data
+- FailOver Stability Fixes
+- Scalability Fixes
 
 ## Known Issues 🐞
 
-- Erratic Traffic Data In Metrics.
 - Adding Custom Private/Public Key For Remote Access Gw Clients Doesn't Get Propagated To Other Peers.
 - IPv6 DNS Entries Are Not Working.
 - Stale Peer On The Interface, When Forced Removed From Multiple Networks At Once.
-- Can Still Ping Domain Name Even When DNS Toggle Is Switched Off.
+- Can Still Ping The Domain Name Even When The DNS Toggle Is Switched Off.
 - WireGuard DNS issue on most flavours of Ubuntu 24.04 and some other newer Linux distributions. The issue is affecting the Remote Access Client (RAC) and the plain WireGuard external clients. Workaround can be found here https://help.netmaker.io/en/articles/9612016-extclient-rac-dns-issue-on-ubuntu-24-04.
 

+ 4 - 1
scripts/netmaker.default.env

@@ -90,4 +90,7 @@ EMAIL_SENDER_PASSWORD=
 PEER_UPDATE_BATCH=true
 # batch peer update size when PEER_UPDATE_BATCH is enabled
 PEER_UPDATE_BATCH_SIZE=50
-
+# default domain for internal DNS lookup
+DEFAULT_DOMAIN=netmaker.hosted
+# managed dns setting, set to true to resolve dns entries on netmaker network
+MANAGE_DNS=false

+ 6 - 48
scripts/nm-quick.sh

@@ -167,12 +167,12 @@ configure_netclient() {
 	nmctl host update $HOST_ID --default
 	sleep 5
 	nmctl node create_remote_access_gateway netmaker $NODE_ID
-	#setup failOver
-	sleep 5
-	curl --location --request POST "https://api.${NETMAKER_BASE_DOMAIN}/api/v1/node/${NODE_ID}/failover" --header "Authorization: Bearer ${MASTER_KEY}"
+	
 	sleep 2
 	# create network for internet access vpn
 	if [ "$INSTALL_TYPE" = "pro" ]; then
+	    #setup failOver
+		curl --location --request POST "https://api.${NETMAKER_BASE_DOMAIN}/api/v1/node/${NODE_ID}/failover" --header "Authorization: Bearer ${MASTER_KEY}"
 		INET_NODE_ID=$(sudo cat /etc/netclient/nodes.json | jq -r '."internet-access-vpn".id')
 		nmctl node create_remote_access_gateway internet-access-vpn $INET_NODE_ID
 		out=$(nmctl node list -o json | jq -r '.[] | select(.id=='\"$INET_NODE_ID\"') | .ingressdns = "8.8.8.8"')
@@ -181,7 +181,6 @@ configure_netclient() {
 		curl --location --request PUT "https://api.${NETMAKER_BASE_DOMAIN}/api/nodes/internet-access-vpn/${INET_NODE_ID}" --data "$out" --header "Authorization: Bearer ${MASTER_KEY}"
 		curl --location --request POST "https://api.${NETMAKER_BASE_DOMAIN}/api/nodes/internet-access-vpn/${INET_NODE_ID}/inet_gw" --data '{}' --header "Authorization: Bearer ${MASTER_KEY}"
 	fi
-	
 	set -e
 }
 
@@ -593,52 +592,11 @@ set_install_vars() {
 		done
 	fi
 	wait_seconds 1
-	unset GET_MQ_USERNAME
-	unset GET_MQ_PASSWORD
-	unset CONFIRM_MQ_PASSWORD
-	echo "Enter Credentials For MQ..."
-	
-	read -p "MQ Username (click 'enter' to use 'netmaker'): " GET_MQ_USERNAME
-	if [ -z "$GET_MQ_USERNAME" ]; then
-		echo "using default username for mq"
-		MQ_USERNAME="netmaker"
-	else
-		MQ_USERNAME="$GET_MQ_USERNAME"
-	fi
-
-	if test -z "$MQ_PASSWORD"; then
-		MQ_PASSWORD=$(
+	MQ_USERNAME="netmaker"
+	MQ_PASSWORD=$(
 			tr -dc A-Za-z0-9 </dev/urandom | head -c 30
 			echo ''
 		)
-	fi
-
-
-	select domain_option in "Auto Generated / Config Password" "Input Your Own Password"; do
-		case $REPLY in
-		1)
-			echo "using random password for mq"
-			break
-			;;
-		2)
-			while true; do
-				echo "Enter your Password For MQ: "
-				read -s GET_MQ_PASSWORD
-				echo "Enter your password again to confirm: "
-				read -s CONFIRM_MQ_PASSWORD
-				if [ ${GET_MQ_PASSWORD} != ${CONFIRM_MQ_PASSWORD} ]; then
-					echo "wrong password entered, try again..."
-					continue
-				fi
-				MQ_PASSWORD="$GET_MQ_PASSWORD"
-				echo "MQ Password Saved Successfully!!"
-				break
-			done
-			break
-			;;
-		*) echo "invalid option $REPLY" ;;
-		esac
-	done
 	
 
 	wait_seconds 2
@@ -921,7 +879,7 @@ main (){
 		source "$CONFIG_PATH"
 	fi
 
-	INSTALL_TYPE="pro"
+	INSTALL_TYPE="ce"
 	while getopts :cudpv flag; do
 	case "${flag}" in
 	c)

+ 37 - 1
servercfg/serverconf.go

@@ -5,13 +5,14 @@ import (
 	"io"
 	"net/http"
 	"os"
+	"regexp"
 	"strconv"
 	"strings"
 	"time"
 
 	"github.com/gravitl/netmaker/config"
-
 	"github.com/gravitl/netmaker/models"
+	"golang.org/x/exp/slog"
 )
 
 // EmqxBrokerType denotes the broker type for EMQX MQTT
@@ -92,6 +93,8 @@ func GetServerConfig() config.ServerConfig {
 	cfg.JwtValidityDuration = GetJwtValidityDuration()
 	cfg.RacAutoDisable = GetRacAutoDisable()
 	cfg.MetricInterval = GetMetricInterval()
+	cfg.ManageDNS = GetManageDNS()
+	cfg.DefaultDomain = GetDefaultDomain()
 	return cfg
 }
 
@@ -136,6 +139,8 @@ func GetServerInfo() models.ServerConfig {
 	cfg.Version = GetVersion()
 	cfg.IsPro = IsPro
 	cfg.MetricInterval = GetMetricInterval()
+	cfg.ManageDNS = GetManageDNS()
+	cfg.DefaultDomain = GetDefaultDomain()
 	return cfg
 }
 
@@ -650,6 +655,37 @@ func GetMetricInterval() string {
 	return mi
 }
 
+// GetManageDNS - if manage DNS enabled or not
+func GetManageDNS() bool {
+	enabled := false
+	if os.Getenv("MANAGE_DNS") != "" {
+		enabled = os.Getenv("MANAGE_DNS") == "true"
+	}
+	return enabled
+}
+
+// GetDefaultDomain - get the default domain
+func GetDefaultDomain() string {
+	//default netmaker.hosted
+	domain := "netmaker.hosted"
+	if os.Getenv("DEFAULT_DOMAIN") != "" {
+		if validateDomain(os.Getenv("DEFAULT_DOMAIN")) {
+			domain = os.Getenv("DEFAULT_DOMAIN")
+		} else {
+			slog.Warn("invalid value, set to default domain: netmaker.hosted", "warn", os.Getenv("DEFAULT_DOMAIN"))
+		}
+	}
+	return domain
+}
+
+func validateDomain(domain string) bool {
+	domainPattern := `[a-zA-Z0-9][a-zA-Z0-9_-]{0,62}(\.[a-zA-Z0-9][a-zA-Z0-9_-]{0,62})*(\.[a-zA-Z][a-zA-Z0-9]{0,10}){1}`
+
+	exp := regexp.MustCompile("^" + domainPattern + "$")
+
+	return exp.MatchString(domain)
+}
+
 // GetBatchPeerUpdate - if batch peer update
 func GetBatchPeerUpdate() bool {
 	enabled := true

+ 29 - 0
servercfg/serverconf_test.go

@@ -0,0 +1,29 @@
+package servercfg
+
+import (
+	"testing"
+
+	"github.com/matryer/is"
+)
+
+func TestValidateDomain(t *testing.T) {
+
+	t.Run("", func(t *testing.T) {
+		is := is.New(t)
+		valid := validateDomain("netmaker.hosted")
+		is.Equal(valid, true)
+	})
+
+	t.Run("", func(t *testing.T) {
+		is := is.New(t)
+		valid := validateDomain("ipv4test1.hosted")
+		is.Equal(valid, true)
+	})
+
+	t.Run("", func(t *testing.T) {
+		is := is.New(t)
+		valid := validateDomain("ip_4?")
+		is.Equal(valid, false)
+	})
+
+}

+ 1 - 1
serverctl/serverctl.go

@@ -40,7 +40,7 @@ func setNodeDefaults() error {
 		return err
 	}
 	for i := range nodes {
-		logic.SetNodeDefaults(&nodes[i])
+		logic.SetNodeDefaults(&nodes[i], false)
 		logic.UpdateNode(&nodes[i], &nodes[i])
 		currentNodeACL, err := nodeacls.FetchNodeACL(nodeacls.NetworkID(nodes[i].Network), nodeacls.NodeID(nodes[i].ID.String()))
 		if (err != nil && (database.IsEmptyRecord(err) || strings.Contains(err.Error(), "no node ACL present"))) || currentNodeACL == nil {

+ 1 - 1
swagger.yaml

@@ -3098,7 +3098,7 @@ tags:
 
     Note: While a MasterKey exists (configurable via env var or config file), it should be considered a backup option, used only when server access is lost. By default, this key is "secret key," but it's crucial to change this and keep it secure in your instance.
 
-    For more information on configuration and security best practices, refer to the [Netmaker documentation](https://docs.netmaker.org/index.html).
+    For more information on configuration and security best practices, refer to the [Netmaker documentation](https://docs.netmaker.io/).
   name: Authentication
 - description: |
     Check out our [Pricing](https://www.netmaker.io/pricing). And Feel Free to [Contact Us](https://www.netmaker.io/contact) if you have any questions or need some clarifications.