Browse Source

Merge pull request #3646 from gravitl/release-v1.1.0

v1.1.0
Abhishek K 1 day ago
parent
commit
9e74334dfd
90 changed files with 3251 additions and 1739 deletions
  1. 1 0
      .github/ISSUE_TEMPLATE/bug-report.yml
  2. 1 1
      .github/workflows/branchtest.yml
  3. 1 1
      .github/workflows/docker-builder.yml
  4. 1 1
      .github/workflows/docs.yml
  5. 2 2
      .github/workflows/publish-docker.yml
  6. 4 4
      .github/workflows/test.yml
  7. 1 1
      Dockerfile
  8. 1 1
      Dockerfile-quick
  9. 1 1
      README.md
  10. 0 5
      auth/auth.go
  11. 66 19
      auth/host_session.go
  12. 1 1
      compose/docker-compose.netclient.yml
  13. 1 0
      config/config.go
  14. 1 0
      controllers/controller.go
  15. 8 0
      controllers/dns.go
  16. 93 7
      controllers/egress.go
  17. 1 19
      controllers/enrollmentkeys.go
  18. 28 3
      controllers/ext_client.go
  19. 167 3
      controllers/hosts.go
  20. 5 0
      controllers/middleware.go
  21. 2 5
      controllers/network.go
  22. 5 2
      controllers/node.go
  23. 33 13
      controllers/server.go
  24. 250 14
      controllers/user.go
  25. 20 20
      go.mod
  26. 46 46
      go.sum
  27. 1 1
      k8s/client/netclient-daemonset.yaml
  28. 1 1
      k8s/client/netclient.yaml
  29. 1 1
      k8s/server/netmaker-ui.yaml
  30. 506 200
      logic/acls.go
  31. 25 21
      logic/auth.go
  32. 38 0
      logic/dns.go
  33. 119 12
      logic/egress.go
  34. 7 38
      logic/extpeers.go
  35. 2 2
      logic/gateway.go
  36. 25 3
      logic/hosts.go
  37. 7 3
      logic/jwts.go
  38. 30 19
      logic/networks.go
  39. 1 1
      logic/nodes.go
  40. 6 2
      logic/peers.go
  41. 5 0
      logic/server.go
  42. 68 18
      logic/settings.go
  43. 2 2
      logic/status.go
  44. 33 0
      logic/user_mgmt.go
  45. 1 1
      logic/users.go
  46. 35 0
      logic/util.go
  47. 2 2
      main.go
  48. 125 6
      migrate/migrate.go
  49. 1 0
      models/egress.go
  50. 0 2
      models/events.go
  51. 2 0
      models/extclient.go
  52. 8 5
      models/host.go
  53. 24 17
      models/mqtt.go
  54. 1 0
      models/network.go
  55. 45 37
      models/settings.go
  56. 1 0
      models/ssocache.go
  57. 28 0
      models/structs.go
  58. 8 7
      models/user_mgmt.go
  59. 1 1
      mq/handlers.go
  60. 2 0
      mq/publishers.go
  61. 4 0
      pro/auth/auth.go
  62. 15 4
      pro/auth/azure-ad.go
  63. 15 4
      pro/auth/github.go
  64. 15 4
      pro/auth/google.go
  65. 1 1
      pro/auth/headless_callback.go
  66. 14 4
      pro/auth/oidc.go
  67. 33 4
      pro/auth/sync.go
  68. 0 19
      pro/controllers/metrics.go
  69. 31 0
      pro/controllers/networks.go
  70. 395 89
      pro/controllers/users.go
  71. 88 10
      pro/idp/azure/azure.go
  72. 76 6
      pro/idp/google/google.go
  73. 1 0
      pro/idp/idp.go
  74. 7 6
      pro/initialize.go
  75. 3 0
      pro/license.go
  76. 338 796
      pro/logic/acls.go
  77. 0 67
      pro/logic/nodes.go
  78. 6 12
      pro/logic/security.go
  79. 13 0
      pro/logic/server.go
  80. 148 34
      pro/logic/user_mgmt.go
  81. 5 2
      pro/types.go
  82. 4 6
      pro/util.go
  83. 16 12
      release.md
  84. 16 8
      schema/egress.go
  85. 1 0
      schema/models.go
  86. 46 0
      schema/pending_hosts.go
  87. 1 1
      scripts/netmaker.default.env
  88. 1 76
      servercfg/serverconf.go
  89. 1 1
      swagger.yaml
  90. 56 2
      utils/utils.go

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

@@ -31,6 +31,7 @@ body:
       label: Version
       label: Version
       description: What version are you running?
       description: What version are you running?
       options:
       options:
+        - v1.1.0
         - v1.0.0
         - v1.0.0
         - v0.99.0
         - v0.99.0
         - v0.90.0
         - v0.90.0

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

@@ -41,7 +41,7 @@ jobs:
             echo "NETCLIENT_BRANCH=develop" >> $GITHUB_ENV
             echo "NETCLIENT_BRANCH=develop" >> $GITHUB_ENV
           fi
           fi
       - name: Checkout netclient repository
       - name: Checkout netclient repository
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
         with:
           repository: gravitl/netclient
           repository: gravitl/netclient
           fetch-depth: 0
           fetch-depth: 0

+ 1 - 1
.github/workflows/docker-builder.yml

@@ -12,7 +12,7 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
     - name: Checkout
     - name: Checkout
-      uses: actions/checkout@v4
+      uses: actions/checkout@v5
     - name: SetUp Buildx
     - name: SetUp Buildx
       uses: docker/setup-buildx-action@v3
       uses: docker/setup-buildx-action@v3
     - name: Login to Dockerhub
     - name: Login to Dockerhub

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

@@ -13,7 +13,7 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
       - name: Checkout
       - name: Checkout
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
         with:
           repository: gravitl/netmaker
           repository: gravitl/netmaker
           ref: ${{ github.event.inputs.branch || 'master' }}
           ref: ${{ github.event.inputs.branch || 'master' }}

+ 2 - 2
.github/workflows/publish-docker.yml

@@ -29,7 +29,7 @@ jobs:
             echo "TAG=${TAG}" >> $GITHUB_ENV
             echo "TAG=${TAG}" >> $GITHUB_ENV
       -
       -
         name: Checkout
         name: Checkout
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
       -
       -
         name: Set up QEMU
         name: Set up QEMU
         uses: docker/setup-qemu-action@v3
         uses: docker/setup-qemu-action@v3
@@ -69,7 +69,7 @@ jobs:
             echo "TAG=${TAG}" >> $GITHUB_ENV
             echo "TAG=${TAG}" >> $GITHUB_ENV
       -
       -
         name: Checkout
         name: Checkout
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
       -
       -
         name: Set up QEMU
         name: Set up QEMU
         uses: docker/setup-qemu-action@v3
         uses: docker/setup-qemu-action@v3

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

@@ -11,7 +11,7 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
       - name: Checkout
       - name: Checkout
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
       - name: Setup Go
       - name: Setup Go
         uses: actions/setup-go@v5
         uses: actions/setup-go@v5
         with:
         with:
@@ -25,7 +25,7 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
       - name: Checkout
       - name: Checkout
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
       - name: Setup go
       - name: Setup go
         uses: actions/setup-go@v5
         uses: actions/setup-go@v5
         with:
         with:
@@ -42,7 +42,7 @@ jobs:
     runs-on: ubuntu-22.04
     runs-on: ubuntu-22.04
     steps:
     steps:
       - name: Checkout
       - name: Checkout
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
       - name: Setup Go
       - name: Setup Go
         uses: actions/setup-go@v5
         uses: actions/setup-go@v5
         with:
         with:
@@ -62,7 +62,7 @@ jobs:
     runs-on: ubuntu-22.04
     runs-on: ubuntu-22.04
     steps:
     steps:
       - name: Checkout
       - name: Checkout
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
       - name: Setup Go
       - name: Setup Go
         uses: actions/setup-go@v5
         uses: actions/setup-go@v5
         with:
         with:

+ 1 - 1
Dockerfile

@@ -6,7 +6,7 @@ COPY . .
 
 
 RUN GOOS=linux CGO_ENABLED=1 go build -ldflags="-s -w " -tags ${tags} .
 RUN GOOS=linux CGO_ENABLED=1 go build -ldflags="-s -w " -tags ${tags} .
 # RUN go build -tags=ee . -o netmaker main.go
 # RUN go build -tags=ee . -o netmaker main.go
-FROM alpine:3.22.0
+FROM alpine:3.22.1
 
 
 # add a c lib
 # add a c lib
 # set the working directory
 # set the working directory

+ 1 - 1
Dockerfile-quick

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

+ 1 - 1
README.md

@@ -16,7 +16,7 @@
 
 
 <p align="center">
 <p align="center">
   <a href="https://github.com/gravitl/netmaker/releases">
   <a href="https://github.com/gravitl/netmaker/releases">
-    <img src="https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square" />
+    <img src="https://img.shields.io/badge/Version-1.1.0-informational?style=flat-square" />
   </a>
   </a>
   <a href="https://hub.docker.com/r/gravitl/netmaker/tags">
   <a href="https://hub.docker.com/r/gravitl/netmaker/tags">
     <img src="https://img.shields.io/docker/pulls/gravitl/netmaker?label=downloads" />
     <img src="https://img.shields.io/docker/pulls/gravitl/netmaker?label=downloads" />

+ 0 - 5
auth/auth.go

@@ -3,7 +3,6 @@ package auth
 import (
 import (
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
-	"golang.org/x/oauth2"
 )
 )
 
 
 // == consts ==
 // == consts ==
@@ -11,10 +10,6 @@ const (
 	node_signin_length = 64
 	node_signin_length = 64
 )
 )
 
 
-var (
-	auth_provider *oauth2.Config
-)
-
 func isUserIsAllowed(username, network string) (*models.User, error) {
 func isUserIsAllowed(username, network string) (*models.User, error) {
 
 
 	user, err := logic.GetUser(username)
 	user, err := logic.GetUser(username)

+ 66 - 19
auth/host_session.go

@@ -1,6 +1,7 @@
 package auth
 package auth
 
 
 import (
 import (
+	"context"
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
 	"log/slog"
 	"log/slog"
@@ -9,12 +10,14 @@ import (
 
 
 	"github.com/google/uuid"
 	"github.com/google/uuid"
 	"github.com/gorilla/websocket"
 	"github.com/gorilla/websocket"
+	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic/hostactions"
 	"github.com/gravitl/netmaker/logic/hostactions"
 	"github.com/gravitl/netmaker/logic/pro/netcache"
 	"github.com/gravitl/netmaker/logic/pro/netcache"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/mq"
+	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 )
 )
 
 
@@ -77,7 +80,7 @@ func SessionHandler(conn *websocket.Conn) {
 		_, err := logic.VerifyAuthRequest(models.UserAuthParams{
 		_, err := logic.VerifyAuthRequest(models.UserAuthParams{
 			UserName: registerMessage.User,
 			UserName: registerMessage.User,
 			Password: registerMessage.Password,
 			Password: registerMessage.Password,
-		})
+		}, logic.NetclientApp)
 		if err != nil {
 		if err != nil {
 			err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
 			err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
 			if err != nil {
 			if err != nil {
@@ -110,7 +113,7 @@ func SessionHandler(conn *websocket.Conn) {
 			return
 			return
 		}
 		}
 	} else { // handle SSO / OAuth
 	} else { // handle SSO / OAuth
-		if auth_provider == nil {
+		if !logic.IsOAuthConfigured() {
 			err = conn.WriteMessage(messageType, []byte("Oauth not configured"))
 			err = conn.WriteMessage(messageType, []byte("Oauth not configured"))
 			if err != nil {
 			if err != nil {
 				logger.Log(0, "error during message writing:", err.Error())
 				logger.Log(0, "error during message writing:", err.Error())
@@ -223,7 +226,7 @@ func SessionHandler(conn *websocket.Conn) {
 		if err = conn.WriteMessage(messageType, reponseData); err != nil {
 		if err = conn.WriteMessage(messageType, reponseData); err != nil {
 			logger.Log(0, "error during message writing:", err.Error())
 			logger.Log(0, "error during message writing:", err.Error())
 		}
 		}
-		go CheckNetRegAndHostUpdate(netsToAdd[:], &result.Host, uuid.Nil, []models.TagID{})
+		go CheckNetRegAndHostUpdate(models.EnrollmentKey{Networks: netsToAdd}, &result.Host, "")
 	case <-timeout: // the read from req.answerCh has timed out
 	case <-timeout: // the read from req.answerCh has timed out
 		logger.Log(0, "timeout signal recv,exiting oauth socket conn")
 		logger.Log(0, "timeout signal recv,exiting oauth socket conn")
 		break
 		break
@@ -237,35 +240,79 @@ func SessionHandler(conn *websocket.Conn) {
 }
 }
 
 
 // CheckNetRegAndHostUpdate - run through networks and send a host update
 // CheckNetRegAndHostUpdate - run through networks and send a host update
-func CheckNetRegAndHostUpdate(networks []string, h *models.Host, relayNodeId uuid.UUID, tags []models.TagID) {
+func CheckNetRegAndHostUpdate(key models.EnrollmentKey, h *models.Host, username string) {
 	// publish host update through MQ
 	// publish host update through MQ
-	for i := range networks {
-		network := networks[i]
-		if ok, _ := logic.NetworkExists(network); ok {
-			newNode, err := logic.UpdateHostNetwork(h, network, true)
+	for _, netID := range key.Networks {
+		if network, err := logic.GetNetwork(netID); err == nil {
+			if network.AutoJoin == "false" {
+				if logic.DoesHostExistinTheNetworkAlready(h, models.NetworkID(netID)) {
+					continue
+				}
+				if err := (&schema.PendingHost{
+					HostID:  h.ID.String(),
+					Network: netID,
+				}).CheckIfPendingHostExists(db.WithContext(context.TODO())); err == nil {
+					continue
+				}
+				keyB, _ := json.Marshal(key)
+				// add host to pending host table
+				p := schema.PendingHost{
+					ID:            uuid.NewString(),
+					HostID:        h.ID.String(),
+					Hostname:      h.Name,
+					Network:       netID,
+					PublicKey:     h.PublicKey.String(),
+					OS:            h.OS,
+					Location:      h.Location,
+					Version:       h.Version,
+					EnrollmentKey: keyB,
+					RequestedAt:   time.Now().UTC(),
+				}
+				p.Create(db.WithContext(context.TODO()))
+				continue
+			}
+
+			logic.LogEvent(&models.Event{
+				Action: models.JoinHostToNet,
+				Source: models.Subject{
+					ID:   key.Value,
+					Name: key.Tags[0],
+					Type: models.EnrollmentKeySub,
+				},
+				TriggeredBy: username,
+				Target: models.Subject{
+					ID:   h.ID.String(),
+					Name: h.Name,
+					Type: models.DeviceSub,
+				},
+				NetworkID: models.NetworkID(netID),
+				Origin:    models.Dashboard,
+			})
+
+			newNode, err := logic.UpdateHostNetwork(h, netID, true)
 			if err == nil || strings.Contains(err.Error(), "host already part of network") {
 			if err == nil || strings.Contains(err.Error(), "host already part of network") {
-				if len(tags) > 0 {
+				if len(key.Groups) > 0 {
 					newNode.Tags = make(map[models.TagID]struct{})
 					newNode.Tags = make(map[models.TagID]struct{})
-					for _, tagI := range tags {
+					for _, tagI := range key.Groups {
 						newNode.Tags[tagI] = struct{}{}
 						newNode.Tags[tagI] = struct{}{}
 					}
 					}
 					logic.UpsertNode(newNode)
 					logic.UpsertNode(newNode)
 				}
 				}
-				if relayNodeId != uuid.Nil && !newNode.IsRelayed {
+				if key.Relay != uuid.Nil && !newNode.IsRelayed {
 					// check if relay node exists and acting as relay
 					// check if relay node exists and acting as relay
-					relaynode, err := logic.GetNodeByID(relayNodeId.String())
+					relaynode, err := logic.GetNodeByID(key.Relay.String())
 					if err == nil && relaynode.IsGw && relaynode.Network == newNode.Network {
 					if err == nil && relaynode.IsGw && relaynode.Network == newNode.Network {
-						slog.Error(fmt.Sprintf("adding relayed node %s to relay %s on network %s", newNode.ID.String(), relayNodeId.String(), network))
+						slog.Error(fmt.Sprintf("adding relayed node %s to relay %s on network %s", newNode.ID.String(), key.Relay.String(), netID))
 						newNode.IsRelayed = true
 						newNode.IsRelayed = true
-						newNode.RelayedBy = relayNodeId.String()
+						newNode.RelayedBy = key.Relay.String()
 						updatedRelayNode := relaynode
 						updatedRelayNode := relaynode
 						updatedRelayNode.RelayedNodes = append(updatedRelayNode.RelayedNodes, newNode.ID.String())
 						updatedRelayNode.RelayedNodes = append(updatedRelayNode.RelayedNodes, newNode.ID.String())
 						logic.UpdateRelayed(&relaynode, &updatedRelayNode)
 						logic.UpdateRelayed(&relaynode, &updatedRelayNode)
 						if err := logic.UpsertNode(&updatedRelayNode); err != nil {
 						if err := logic.UpsertNode(&updatedRelayNode); err != nil {
-							slog.Error("failed to update node", "nodeid", relayNodeId.String())
+							slog.Error("failed to update node", "nodeid", key.Relay.String())
 						}
 						}
 						if err := logic.UpsertNode(newNode); err != nil {
 						if err := logic.UpsertNode(newNode); err != nil {
-							slog.Error("failed to update node", "nodeid", relayNodeId.String())
+							slog.Error("failed to update node", "nodeid", key.Relay.String())
 						}
 						}
 					} else {
 					} else {
 						slog.Error("failed to relay node. maybe specified relay node is actually not a relay? Or the relayed node is not in the same network with relay?", "err", err)
 						slog.Error("failed to relay node. maybe specified relay node is actually not a relay? Or the relayed node is not in the same network with relay?", "err", err)
@@ -275,7 +322,7 @@ func CheckNetRegAndHostUpdate(networks []string, h *models.Host, relayNodeId uui
 					continue
 					continue
 				}
 				}
 			} else {
 			} else {
-				logger.Log(0, "failed to add host to network:", h.ID.String(), h.Name, network, err.Error())
+				logger.Log(0, "failed to add host to network:", h.ID.String(), h.Name, netID, err.Error())
 				continue
 				continue
 			}
 			}
 			logger.Log(1, "added new node", newNode.ID.String(), "to host", h.Name)
 			logger.Log(1, "added new node", newNode.ID.String(), "to host", h.Name)
@@ -288,10 +335,10 @@ func CheckNetRegAndHostUpdate(networks []string, h *models.Host, relayNodeId uui
 				// make  host failover
 				// make  host failover
 				logic.CreateFailOver(*newNode)
 				logic.CreateFailOver(*newNode)
 				// make host remote access gateway
 				// make host remote access gateway
-				logic.CreateIngressGateway(network, newNode.ID.String(), models.IngressRequest{})
+				logic.CreateIngressGateway(netID, newNode.ID.String(), models.IngressRequest{})
 				logic.CreateRelay(models.RelayRequest{
 				logic.CreateRelay(models.RelayRequest{
 					NodeID: newNode.ID.String(),
 					NodeID: newNode.ID.String(),
-					NetID:  network,
+					NetID:  netID,
 				})
 				})
 			}
 			}
 		}
 		}

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

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

+ 1 - 0
config/config.go

@@ -89,6 +89,7 @@ type ServerConfig struct {
 	DeployedByOperator         bool          `yaml:"deployed_by_operator"`
 	DeployedByOperator         bool          `yaml:"deployed_by_operator"`
 	Environment                string        `yaml:"environment"`
 	Environment                string        `yaml:"environment"`
 	JwtValidityDuration        time.Duration `yaml:"jwt_validity_duration" swaggertype:"primitive,integer" format:"int64"`
 	JwtValidityDuration        time.Duration `yaml:"jwt_validity_duration" swaggertype:"primitive,integer" format:"int64"`
+	JwtValidityDurationClients time.Duration `yaml:"jwt_validity_duration_clients" swaggertype:"primitive,integer" format:"int64"`
 	RacRestrictToSingleNetwork bool          `yaml:"rac_restrict_to_single_network"`
 	RacRestrictToSingleNetwork bool          `yaml:"rac_restrict_to_single_network"`
 	CacheEnabled               string        `yaml:"caching_enabled"`
 	CacheEnabled               string        `yaml:"caching_enabled"`
 	EndpointDetection          bool          `yaml:"endpoint_detection"`
 	EndpointDetection          bool          `yaml:"endpoint_detection"`

+ 1 - 0
controllers/controller.go

@@ -56,6 +56,7 @@ func HandleRESTRequests(wg *sync.WaitGroup, ctx context.Context) {
 			"Content-Type",
 			"Content-Type",
 			"authorization",
 			"authorization",
 			"From-Ui",
 			"From-Ui",
+			"X-Application-Name",
 		},
 		},
 	)
 	)
 	originsOk := handlers.AllowedOrigins(strings.Split(servercfg.GetAllowedOrigin(), ","))
 	originsOk := handlers.AllowedOrigins(strings.Split(servercfg.GetAllowedOrigin(), ","))

+ 8 - 0
controllers/dns.go

@@ -87,6 +87,9 @@ func createNs(w http.ResponseWriter, r *http.Request) {
 	if req.Tags == nil {
 	if req.Tags == nil {
 		req.Tags = make(datatypes.JSONMap)
 		req.Tags = make(datatypes.JSONMap)
 	}
 	}
+	if req.Nodes == nil {
+		req.Nodes = make(datatypes.JSONMap)
+	}
 	if gNs, ok := logic.GlobalNsList[req.Name]; ok {
 	if gNs, ok := logic.GlobalNsList[req.Name]; ok {
 		req.Servers = gNs.IPs
 		req.Servers = gNs.IPs
 	}
 	}
@@ -107,6 +110,7 @@ func createNs(w http.ResponseWriter, r *http.Request) {
 		MatchDomains: req.MatchDomains,
 		MatchDomains: req.MatchDomains,
 		Servers:      req.Servers,
 		Servers:      req.Servers,
 		Tags:         req.Tags,
 		Tags:         req.Tags,
+		Nodes:        req.Nodes,
 		Status:       true,
 		Status:       true,
 		CreatedBy:    r.Header.Get("user"),
 		CreatedBy:    r.Header.Get("user"),
 		CreatedAt:    time.Now().UTC(),
 		CreatedAt:    time.Now().UTC(),
@@ -198,6 +202,9 @@ func updateNs(w http.ResponseWriter, r *http.Request) {
 	if updateNs.Tags == nil {
 	if updateNs.Tags == nil {
 		updateNs.Tags = make(datatypes.JSONMap)
 		updateNs.Tags = make(datatypes.JSONMap)
 	}
 	}
+	if updateNs.Nodes == nil {
+		updateNs.Nodes = make(datatypes.JSONMap)
+	}
 
 
 	ns := schema.Nameserver{ID: updateNs.ID}
 	ns := schema.Nameserver{ID: updateNs.ID}
 	err = ns.Get(db.WithContext(r.Context()))
 	err = ns.Get(db.WithContext(r.Context()))
@@ -239,6 +246,7 @@ func updateNs(w http.ResponseWriter, r *http.Request) {
 	ns.MatchAll = updateNs.MatchAll
 	ns.MatchAll = updateNs.MatchAll
 	ns.Description = updateNs.Description
 	ns.Description = updateNs.Description
 	ns.Name = updateNs.Name
 	ns.Name = updateNs.Name
+	ns.Nodes = updateNs.Nodes
 	ns.Status = updateNs.Status
 	ns.Status = updateNs.Status
 	ns.UpdatedAt = time.Now().UTC()
 	ns.UpdatedAt = time.Now().UTC()
 
 

+ 93 - 7
controllers/egress.go

@@ -45,14 +45,27 @@ func createEgress(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 	var egressRange string
 	var egressRange string
+	var cidrErr error
 	if !req.IsInetGw {
 	if !req.IsInetGw {
-		egressRange, err = logic.NormalizeCIDR(req.Range)
-		if err != nil {
-			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		if req.Range != "" {
+			egressRange, cidrErr = logic.NormalizeCIDR(req.Range)
+		}
+		isDomain := logic.IsFQDN(req.Range)
+		if cidrErr != nil && !isDomain {
+			if cidrErr != nil {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(cidrErr, "badrequest"))
+			} else {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("bad domain name"), "badrequest"))
+			}
 			return
 			return
 		}
 		}
+		if isDomain {
+			req.Domain = req.Range
+			egressRange = ""
+		}
 	} else {
 	} else {
 		egressRange = "*"
 		egressRange = "*"
+		req.Domain = ""
 	}
 	}
 
 
 	e := schema.Egress{
 	e := schema.Egress{
@@ -61,6 +74,8 @@ func createEgress(w http.ResponseWriter, r *http.Request) {
 		Network:     req.Network,
 		Network:     req.Network,
 		Description: req.Description,
 		Description: req.Description,
 		Range:       egressRange,
 		Range:       egressRange,
+		Domain:      req.Domain,
+		DomainAns:   []string{},
 		Nat:         req.Nat,
 		Nat:         req.Nat,
 		Nodes:       make(datatypes.JSONMap),
 		Nodes:       make(datatypes.JSONMap),
 		Tags:        make(datatypes.JSONMap),
 		Tags:        make(datatypes.JSONMap),
@@ -108,7 +123,35 @@ func createEgress(w http.ResponseWriter, r *http.Request) {
 	// 	}
 	// 	}
 
 
 	// }
 	// }
-	go mq.PublishPeerUpdate(false)
+	if req.Domain != "" {
+		if req.Nodes != nil {
+			for nodeID := range req.Nodes {
+				node, err := logic.GetNodeByID(nodeID)
+				if err != nil {
+					continue
+				}
+				host, _ := logic.GetHost(node.HostID.String())
+				if host == nil {
+					continue
+				}
+				mq.HostUpdate(&models.HostUpdate{
+					Action: models.EgressUpdate,
+					Host:   *host,
+					EgressDomain: models.EgressDomain{
+						ID:     e.ID,
+						Host:   *host,
+						Node:   node,
+						Domain: e.Domain,
+					},
+					Node: node,
+				})
+			}
+		}
+
+	} else {
+		go mq.PublishPeerUpdate(false)
+	}
+
 	logic.ReturnSuccessResponseWithJson(w, r, e, "created egress resource")
 	logic.ReturnSuccessResponseWithJson(w, r, e, "created egress resource")
 }
 }
 
 
@@ -161,14 +204,25 @@ func updateEgress(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 	var egressRange string
 	var egressRange string
+	var cidrErr error
 	if !req.IsInetGw {
 	if !req.IsInetGw {
-		egressRange, err = logic.NormalizeCIDR(req.Range)
-		if err != nil {
-			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		egressRange, cidrErr = logic.NormalizeCIDR(req.Range)
+		isDomain := logic.IsFQDN(req.Range)
+		if cidrErr != nil && !isDomain {
+			if cidrErr != nil {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(cidrErr, "badrequest"))
+			} else {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("bad domain name"), "badrequest"))
+			}
 			return
 			return
 		}
 		}
+		if isDomain {
+			req.Domain = req.Range
+			egressRange = ""
+		}
 	} else {
 	} else {
 		egressRange = "*"
 		egressRange = "*"
+		req.Domain = ""
 	}
 	}
 
 
 	e := schema.Egress{ID: req.ID}
 	e := schema.Egress{ID: req.ID}
@@ -209,10 +263,14 @@ func updateEgress(w http.ResponseWriter, r *http.Request) {
 	for nodeID, metric := range req.Nodes {
 	for nodeID, metric := range req.Nodes {
 		e.Nodes[nodeID] = metric
 		e.Nodes[nodeID] = metric
 	}
 	}
+	if e.Domain != req.Domain {
+		e.DomainAns = datatypes.JSONSlice[string]{}
+	}
 	e.Range = egressRange
 	e.Range = egressRange
 	e.Description = req.Description
 	e.Description = req.Description
 	e.Name = req.Name
 	e.Name = req.Name
 	e.Nat = req.Nat
 	e.Nat = req.Nat
+	e.Domain = req.Domain
 	e.Status = req.Status
 	e.Status = req.Status
 	e.UpdatedAt = time.Now().UTC()
 	e.UpdatedAt = time.Now().UTC()
 	if err := logic.ValidateEgressReq(&e); err != nil {
 	if err := logic.ValidateEgressReq(&e); err != nil {
@@ -238,6 +296,34 @@ func updateEgress(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 	event.Diff.New = e
 	event.Diff.New = e
 	logic.LogEvent(event)
 	logic.LogEvent(event)
+	if req.Domain != "" {
+		if req.Nodes != nil {
+			for nodeID := range req.Nodes {
+				node, err := logic.GetNodeByID(nodeID)
+				if err != nil {
+					continue
+				}
+				host, _ := logic.GetHost(node.HostID.String())
+				if host == nil {
+					continue
+				}
+				mq.HostUpdate(&models.HostUpdate{
+					Action: models.EgressUpdate,
+					Host:   *host,
+					EgressDomain: models.EgressDomain{
+						ID:     e.ID,
+						Host:   *host,
+						Node:   node,
+						Domain: e.Domain,
+					},
+					Node: node,
+				})
+			}
+		}
+
+	} else {
+		go mq.PublishPeerUpdate(false)
+	}
 	go mq.PublishPeerUpdate(false)
 	go mq.PublishPeerUpdate(false)
 	logic.ReturnSuccessResponseWithJson(w, r, e, "updated egress resource")
 	logic.ReturnSuccessResponseWithJson(w, r, e, "updated egress resource")
 }
 }

+ 1 - 19
controllers/enrollmentkeys.go

@@ -414,28 +414,10 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 		ServerConf:    server,
 		ServerConf:    server,
 		RequestedHost: *host,
 		RequestedHost: *host,
 	}
 	}
-	for _, netID := range enrollmentKey.Networks {
-		logic.LogEvent(&models.Event{
-			Action: models.JoinHostToNet,
-			Source: models.Subject{
-				ID:   enrollmentKey.Value,
-				Name: enrollmentKey.Tags[0],
-				Type: models.EnrollmentKeySub,
-			},
-			TriggeredBy: r.Header.Get("user"),
-			Target: models.Subject{
-				ID:   newHost.ID.String(),
-				Name: newHost.Name,
-				Type: models.DeviceSub,
-			},
-			NetworkID: models.NetworkID(netID),
-			Origin:    models.Dashboard,
-		})
-	}
 
 
 	logger.Log(0, host.Name, host.ID.String(), "registered with Netmaker")
 	logger.Log(0, host.Name, host.ID.String(), "registered with Netmaker")
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(&response)
 	json.NewEncoder(w).Encode(&response)
 	// notify host of changes, peer and node updates
 	// notify host of changes, peer and node updates
-	go auth.CheckNetRegAndHostUpdate(enrollmentKey.Networks, host, enrollmentKey.Relay, enrollmentKey.Groups)
+	go auth.CheckNetRegAndHostUpdate(*enrollmentKey, host, r.Header.Get("user"))
 }
 }

+ 28 - 3
controllers/ext_client.go

@@ -703,10 +703,34 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 			return
 			return
 		}
 		}
+
+		// if device id is sent, we don't want to create another extclient for the same user
+		// and gw, with the same device id.
+		if customExtClient.DeviceID != "" {
+			// let's first confirm that none of the user's extclients for this gw have device id.
+			for _, extclient := range extclients {
+				if extclient.DeviceID == customExtClient.DeviceID &&
+					extclient.OwnerID == caller.UserName && nodeid == extclient.IngressGatewayID {
+					err = errors.New("remote client config already exists on the gateway")
+					slog.Error("failed to create extclient", "user", userName, "error", err)
+					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+					return
+				}
+			}
+		}
+
 		for _, extclient := range extclients {
 		for _, extclient := range extclients {
 			if extclient.RemoteAccessClientID != "" &&
 			if extclient.RemoteAccessClientID != "" &&
-				extclient.RemoteAccessClientID == customExtClient.RemoteAccessClientID && extclient.OwnerID == caller.UserName && nodeid == extclient.IngressGatewayID {
-				// extclient on the gw already exists for the remote access client
+				extclient.RemoteAccessClientID == customExtClient.RemoteAccessClientID &&
+				extclient.OwnerID == caller.UserName && nodeid == extclient.IngressGatewayID {
+				if customExtClient.DeviceID != "" && extclient.DeviceID == "" {
+					// This extclient doesn’t include a device ID (and neither do the others).
+					// We patch it by assigning the device ID from the incoming request.
+					// When clients see that the config already exists, they will fetch
+					// the one with their device ID. And we will return this one.
+					extclient.DeviceID = customExtClient.DeviceID
+					_ = logic.SaveExtClient(&extclient)
+				}
 				err = errors.New("remote client config already exists on the gateway")
 				err = errors.New("remote client config already exists on the gateway")
 				slog.Error("failed to create extclient", "user", userName, "error", err)
 				slog.Error("failed to create extclient", "user", userName, "error", err)
 				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
@@ -744,6 +768,7 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 		extclient.Enabled = parentNetwork.DefaultACL == "yes"
 		extclient.Enabled = parentNetwork.DefaultACL == "yes"
 	}
 	}
 	extclient.Os = customExtClient.Os
 	extclient.Os = customExtClient.Os
+	extclient.DeviceID = customExtClient.DeviceID
 	extclient.DeviceName = customExtClient.DeviceName
 	extclient.DeviceName = customExtClient.DeviceName
 	if customExtClient.IsAlreadyConnectedToInetGw {
 	if customExtClient.IsAlreadyConnectedToInetGw {
 		slog.Warn("RAC/Client is already connected to internet gateway. this may mask their real IP address", "client IP", customExtClient.PublicEndpoint)
 		slog.Warn("RAC/Client is already connected to internet gateway. this may mask their real IP address", "client IP", customExtClient.PublicEndpoint)
@@ -897,7 +922,7 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 		update.Location = logic.GetHostLocInfo(logic.GetClientIP(r), os.Getenv("IP_INFO_TOKEN"))
 		update.Location = logic.GetHostLocInfo(logic.GetClientIP(r), os.Getenv("IP_INFO_TOKEN"))
 	}
 	}
 	newclient := logic.UpdateExtClient(&oldExtClient, &update)
 	newclient := logic.UpdateExtClient(&oldExtClient, &update)
-	if err := logic.DeleteExtClient(oldExtClient.Network, oldExtClient.ClientID); err != nil {
+	if err := logic.DeleteExtClient(oldExtClient.Network, oldExtClient.ClientID, true); err != nil {
 		slog.Error(
 		slog.Error(
 			"failed to delete ext client",
 			"failed to delete ext client",
 			"user",
 			"user",

+ 167 - 3
controllers/hosts.go

@@ -10,10 +10,13 @@ import (
 	"github.com/google/uuid"
 	"github.com/google/uuid"
 	"github.com/gorilla/mux"
 	"github.com/gorilla/mux"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/logic/hostactions"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/mq"
+	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/crypto/bcrypt"
 	"golang.org/x/crypto/bcrypt"
 	"golang.org/x/exp/slog"
 	"golang.org/x/exp/slog"
@@ -51,6 +54,12 @@ func hostHandlers(r *mux.Router) {
 		Methods(http.MethodPut)
 		Methods(http.MethodPut)
 	r.HandleFunc("/api/v1/host/{hostid}/peer_info", Authorize(true, false, "host", http.HandlerFunc(getHostPeerInfo))).
 	r.HandleFunc("/api/v1/host/{hostid}/peer_info", Authorize(true, false, "host", http.HandlerFunc(getHostPeerInfo))).
 		Methods(http.MethodGet)
 		Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/pending_hosts", logic.SecurityCheck(true, http.HandlerFunc(getPendingHosts))).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/pending_hosts/approve/{id}", logic.SecurityCheck(true, http.HandlerFunc(approvePendingHost))).
+		Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/pending_hosts/reject/{id}", logic.SecurityCheck(true, http.HandlerFunc(rejectPendingHost))).
+		Methods(http.MethodPost)
 	r.HandleFunc("/api/emqx/hosts", logic.SecurityCheck(true, http.HandlerFunc(delEmqxHosts))).
 	r.HandleFunc("/api/emqx/hosts", logic.SecurityCheck(true, http.HandlerFunc(delEmqxHosts))).
 		Methods(http.MethodDelete)
 		Methods(http.MethodDelete)
 	r.HandleFunc("/api/v1/auth-register/host", socketHandler)
 	r.HandleFunc("/api/v1/auth-register/host", socketHandler)
@@ -244,11 +253,13 @@ func pull(w http.ResponseWriter, r *http.Request) {
 		ChangeDefaultGw:   hPU.ChangeDefaultGw,
 		ChangeDefaultGw:   hPU.ChangeDefaultGw,
 		DefaultGwIp:       hPU.DefaultGwIp,
 		DefaultGwIp:       hPU.DefaultGwIp,
 		IsInternetGw:      hPU.IsInternetGw,
 		IsInternetGw:      hPU.IsInternetGw,
+		NameServers:       hPU.NameServers,
+		EgressWithDomains: hPU.EgressWithDomains,
 		EndpointDetection: logic.IsEndpointDetectionEnabled(),
 		EndpointDetection: logic.IsEndpointDetectionEnabled(),
 		DnsNameservers:    hPU.DnsNameservers,
 		DnsNameservers:    hPU.DnsNameservers,
 	}
 	}
 
 
-	logger.Log(1, hostID, "completed a pull")
+	logger.Log(1, hostID, host.Name, "completed a pull")
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(&response)
 	json.NewEncoder(w).Encode(&response)
 }
 }
@@ -365,7 +376,6 @@ func hostUpdateFallback(w http.ResponseWriter, r *http.Request) {
 	switch hostUpdate.Action {
 	switch hostUpdate.Action {
 	case models.CheckIn:
 	case models.CheckIn:
 		sendPeerUpdate = mq.HandleHostCheckin(&hostUpdate.Host, currentHost)
 		sendPeerUpdate = mq.HandleHostCheckin(&hostUpdate.Host, currentHost)
-
 	case models.UpdateHost:
 	case models.UpdateHost:
 		if hostUpdate.Host.PublicKey != currentHost.PublicKey {
 		if hostUpdate.Host.PublicKey != currentHost.PublicKey {
 			//remove old peer entry
 			//remove old peer entry
@@ -375,12 +385,24 @@ func hostUpdateFallback(w http.ResponseWriter, r *http.Request) {
 		err := logic.UpsertHost(currentHost)
 		err := logic.UpsertHost(currentHost)
 		if err != nil {
 		if err != nil {
 			slog.Error("failed to update host", "id", currentHost.ID, "error", err)
 			slog.Error("failed to update host", "id", currentHost.ID, "error", err)
-			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
 			return
 			return
 		}
 		}
 
 
 	case models.UpdateMetrics:
 	case models.UpdateMetrics:
 		mq.UpdateMetricsFallBack(hostUpdate.Node.ID.String(), hostUpdate.NewMetrics)
 		mq.UpdateMetricsFallBack(hostUpdate.Node.ID.String(), hostUpdate.NewMetrics)
+	case models.EgressUpdate:
+		e := schema.Egress{ID: hostUpdate.EgressDomain.ID}
+		err = e.Get(db.WithContext(r.Context()))
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
+			return
+		}
+		if len(hostUpdate.Node.EgressGatewayRanges) > 0 {
+			e.DomainAns = hostUpdate.Node.EgressGatewayRanges
+			e.Update(db.WithContext(r.Context()))
+		}
+		sendPeerUpdate = true
 	}
 	}
 
 
 	if sendPeerUpdate {
 	if sendPeerUpdate {
@@ -454,6 +476,10 @@ func deleteHost(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
+	// delete if any pending reqs
+	(&schema.PendingHost{
+		HostID: currHost.ID.String(),
+	}).DeleteAllPendingHosts(db.WithContext(r.Context()))
 	logic.LogEvent(&models.Event{
 	logic.LogEvent(&models.Event{
 		Action: models.Delete,
 		Action: models.Delete,
 		Source: models.Subject{
 		Source: models.Subject{
@@ -1145,3 +1171,141 @@ func getHostPeerInfo(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 	logic.ReturnSuccessResponseWithJson(w, r, peerInfo, "fetched host peer info")
 	logic.ReturnSuccessResponseWithJson(w, r, peerInfo, "fetched host peer info")
 }
 }
+
+// @Summary     List pending hosts in a network
+// @Router      /api/v1/pending_hosts [get]
+// @Tags        Hosts
+// @Security    oauth
+// @Success     200 {array} schema.PendingHost
+// @Failure     500 {object} models.ErrorResponse
+func getPendingHosts(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
+	}
+	pendingHosts, err := (&schema.PendingHost{
+		Network: netID,
+	}).List(db.WithContext(r.Context()))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, models.ErrorResponse{
+			Code:    http.StatusBadRequest,
+			Message: err.Error(),
+		})
+		return
+	}
+	logger.Log(2, r.Header.Get("user"), "fetched all hosts")
+	logic.ReturnSuccessResponseWithJson(w, r, pendingHosts, "returned pending hosts in "+netID)
+}
+
+// @Summary     approve pending hosts in a network
+// @Router      /api/v1/pending_hosts/approve/{id} [post]
+// @Tags        Hosts
+// @Security    oauth
+// @Success     200 {array} models.ApiNode
+// @Failure     500 {object} models.ErrorResponse
+func approvePendingHost(w http.ResponseWriter, r *http.Request) {
+	id := mux.Vars(r)["id"]
+	p := &schema.PendingHost{ID: id}
+	err := p.Get(db.WithContext(r.Context()))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, models.ErrorResponse{
+			Code:    http.StatusBadRequest,
+			Message: err.Error(),
+		})
+		return
+	}
+	h, err := logic.GetHost(p.HostID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, models.ErrorResponse{
+			Code:    http.StatusBadRequest,
+			Message: err.Error(),
+		})
+		return
+	}
+	key := models.EnrollmentKey{}
+	json.Unmarshal(p.EnrollmentKey, &key)
+	newNode, err := logic.UpdateHostNetwork(h, p.Network, true)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, models.ErrorResponse{
+			Code:    http.StatusBadRequest,
+			Message: err.Error(),
+		})
+		return
+	}
+	if len(key.Groups) > 0 {
+		newNode.Tags = make(map[models.TagID]struct{})
+		for _, tagI := range key.Groups {
+			newNode.Tags[tagI] = struct{}{}
+		}
+		logic.UpsertNode(newNode)
+	}
+	if key.Relay != uuid.Nil && !newNode.IsRelayed {
+		// check if relay node exists and acting as relay
+		relaynode, err := logic.GetNodeByID(key.Relay.String())
+		if err == nil && relaynode.IsGw && relaynode.Network == newNode.Network {
+			slog.Error(fmt.Sprintf("adding relayed node %s to relay %s on network %s", newNode.ID.String(), key.Relay.String(), p.Network))
+			newNode.IsRelayed = true
+			newNode.RelayedBy = key.Relay.String()
+			updatedRelayNode := relaynode
+			updatedRelayNode.RelayedNodes = append(updatedRelayNode.RelayedNodes, newNode.ID.String())
+			logic.UpdateRelayed(&relaynode, &updatedRelayNode)
+			if err := logic.UpsertNode(&updatedRelayNode); err != nil {
+				slog.Error("failed to update node", "nodeid", key.Relay.String())
+			}
+			if err := logic.UpsertNode(newNode); err != nil {
+				slog.Error("failed to update node", "nodeid", key.Relay.String())
+			}
+		} else {
+			slog.Error("failed to relay node. maybe specified relay node is actually not a relay? Or the relayed node is not in the same network with relay?", "err", err)
+		}
+	}
+
+	logger.Log(1, "added new node", newNode.ID.String(), "to host", h.Name)
+	hostactions.AddAction(models.HostUpdate{
+		Action: models.JoinHostToNetwork,
+		Host:   *h,
+		Node:   *newNode,
+	})
+	if h.IsDefault {
+		// make  host failover
+		logic.CreateFailOver(*newNode)
+		// make host remote access gateway
+		logic.CreateIngressGateway(p.Network, newNode.ID.String(), models.IngressRequest{})
+		logic.CreateRelay(models.RelayRequest{
+			NodeID: newNode.ID.String(),
+			NetID:  p.Network,
+		})
+	}
+	p.Delete(db.WithContext(r.Context()))
+	go mq.PublishPeerUpdate(false)
+	logic.ReturnSuccessResponseWithJson(w, r, newNode.ConvertToAPINode(), "added pending host to "+p.Network)
+}
+
+// @Summary     reject pending hosts in a network
+// @Router      /api/v1/pending_hosts/reject/{id} [post]
+// @Tags        Hosts
+// @Security    oauth
+// @Success     200 {array} models.ApiNode
+// @Failure     500 {object} models.ErrorResponse
+func rejectPendingHost(w http.ResponseWriter, r *http.Request) {
+	id := mux.Vars(r)["id"]
+	p := &schema.PendingHost{ID: id}
+	err := p.Get(db.WithContext(r.Context()))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, models.ErrorResponse{
+			Code:    http.StatusBadRequest,
+			Message: err.Error(),
+		})
+		return
+	}
+	err = p.Delete(db.WithContext(r.Context()))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, models.ErrorResponse{
+			Code:    http.StatusBadRequest,
+			Message: err.Error(),
+		})
+		return
+	}
+	logic.ReturnSuccessResponseWithJson(w, r, p, "deleted pending host from "+p.Network)
+}

+ 5 - 0
controllers/middleware.go

@@ -60,6 +60,11 @@ func userMiddleWare(handler http.Handler) http.Handler {
 		if strings.Contains(route, "networks") {
 		if strings.Contains(route, "networks") {
 			r.Header.Set("TARGET_RSRC", models.NetworkRsrc.String())
 			r.Header.Set("TARGET_RSRC", models.NetworkRsrc.String())
 		}
 		}
+		// check 'graph' after 'networks', otherwise the
+		// header will be overwritten.
+		if strings.Contains(route, "graph") {
+			r.Header.Set("TARGET_RSRC", models.HostRsrc.String())
+		}
 		if strings.Contains(route, "acls") {
 		if strings.Contains(route, "acls") {
 			r.Header.Set("TARGET_RSRC", models.AclRsrc.String())
 			r.Header.Set("TARGET_RSRC", models.AclRsrc.String())
 		}
 		}

+ 2 - 5
controllers/network.go

@@ -330,7 +330,7 @@ func updateNetworkACLv2(w http.ResponseWriter, r *http.Request) {
 	if servercfg.IsPro {
 	if servercfg.IsPro {
 		for _, client := range networkClientsMap {
 		for _, client := range networkClientsMap {
 			client := client
 			client := client
-			err := logic.DeleteExtClient(client.Network, client.ClientID)
+			err := logic.DeleteExtClient(client.Network, client.ClientID, true)
 			if err != nil {
 			if err != nil {
 				slog.Error(
 				slog.Error(
 					"failed to delete client during update",
 					"failed to delete client during update",
@@ -701,10 +701,7 @@ func updateNetwork(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 		return
 	}
 	}
-	netNew := netOld
-	netNew.NameServers = payload.NameServers
-	netNew.DefaultACL = payload.DefaultACL
-	_, _, _, err = logic.UpdateNetwork(&netOld, &netNew)
+	err = logic.UpdateNetwork(&netOld, &payload)
 	if err != nil {
 	if err != nil {
 		slog.Info("failed to update network", "user", r.Header.Get("user"), "err", err)
 		slog.Info("failed to update network", "user", r.Header.Get("user"), "err", err)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))

+ 5 - 2
controllers/node.go

@@ -417,7 +417,7 @@ func getNetworkNodeStatus(w http.ResponseWriter, r *http.Request) {
 
 
 	}
 	}
 	nodes = logic.AddStaticNodestoList(nodes)
 	nodes = logic.AddStaticNodestoList(nodes)
-	nodes = logic.AddStatusToNodes(nodes, false)
+	nodes = logic.AddStatusToNodes(nodes, true)
 	// return all the nodes in JSON/API format
 	// return all the nodes in JSON/API format
 	apiNodesStatusMap := logic.GetNodesStatusAPI(nodes[:])
 	apiNodesStatusMap := logic.GetNodesStatusAPI(nodes[:])
 	logger.Log(3, r.Header.Get("user"), "fetched all nodes they have access to")
 	logger.Log(3, r.Header.Get("user"), "fetched all nodes they have access to")
@@ -683,8 +683,11 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 		logic.SetInternetGw(newNode, newNode.InetNodeReq)
 		logic.SetInternetGw(newNode, newNode.InetNodeReq)
 	}
 	}
 	if currentNode.IsInternetGateway && newNode.IsInternetGateway {
 	if currentNode.IsInternetGateway && newNode.IsInternetGateway {
+		// logic.UnsetInternetGw resets newNode.InetNodeReq.
+		// So, keeping a copy to pass into logic.SetInternetGw.
+		req := newNode.InetNodeReq
 		logic.UnsetInternetGw(newNode)
 		logic.UnsetInternetGw(newNode)
-		logic.SetInternetGw(newNode, newNode.InetNodeReq)
+		logic.SetInternetGw(newNode, req)
 	}
 	}
 	if !newNode.IsInternetGateway {
 	if !newNode.IsInternetGateway {
 		logic.UnsetInternetGw(newNode)
 		logic.UnsetInternetGw(newNode)

+ 33 - 13
controllers/server.go

@@ -1,8 +1,12 @@
 package controller
 package controller
 
 
 import (
 import (
+	"context"
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
+	"fmt"
+	"github.com/gravitl/netmaker/db"
+	"github.com/gravitl/netmaker/schema"
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp"
 	"net/http"
 	"net/http"
 	"os"
 	"os"
@@ -57,6 +61,7 @@ func serverHandlers(r *mux.Router) {
 		Methods(http.MethodPost)
 		Methods(http.MethodPost)
 	r.HandleFunc("/api/server/mem_profile", logic.SecurityCheck(false, http.HandlerFunc(memProfile))).
 	r.HandleFunc("/api/server/mem_profile", logic.SecurityCheck(false, http.HandlerFunc(memProfile))).
 		Methods(http.MethodPost)
 		Methods(http.MethodPost)
+	r.HandleFunc("/api/server/feature_flags", getFeatureFlags).Methods(http.MethodGet)
 }
 }
 
 
 func cpuProfile(w http.ResponseWriter, r *http.Request) {
 func cpuProfile(w http.ResponseWriter, r *http.Request) {
@@ -110,10 +115,7 @@ func getUsage(w http.ResponseWriter, _ *http.Request) {
 	if err == nil {
 	if err == nil {
 		serverUsage.Ingresses = len(ingresses)
 		serverUsage.Ingresses = len(ingresses)
 	}
 	}
-	egresses, err := logic.GetAllEgresses()
-	if err == nil {
-		serverUsage.Egresses = len(egresses)
-	}
+	serverUsage.Egresses, _ = (&schema.Egress{}).Count(db.WithContext(context.TODO()))
 	relays, err := logic.GetRelays()
 	relays, err := logic.GetRelays()
 	if err == nil {
 	if err == nil {
 		serverUsage.Relays = len(relays)
 		serverUsage.Relays = len(relays)
@@ -273,6 +275,24 @@ func updateSettings(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 	currSettings := logic.GetServerSettings()
 	currSettings := logic.GetServerSettings()
+
+	if req.AuthProvider != currSettings.AuthProvider && req.AuthProvider == "" {
+		superAdmin, err := logic.GetSuperAdmin()
+		if err != nil {
+			err = fmt.Errorf("failed to get super admin: %v", err)
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+			return
+		}
+
+		if superAdmin.AuthType == models.OAuth {
+			err := fmt.Errorf(
+				"cannot remove IdP integration because an OAuth user has the super-admin role; transfer the super-admin role to another user first",
+			)
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+			return
+		}
+	}
+
 	err := logic.UpsertServerSettings(req)
 	err := logic.UpsertServerSettings(req)
 	if err != nil {
 	if err != nil {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to update server settings "+err.Error()), "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to update server settings "+err.Error()), "internal"))
@@ -376,15 +396,6 @@ func identifySettingsUpdateAction(old, new models.ServerSettings) models.Action
 		return models.UpdateMonitoringAndDebuggingSettings
 		return models.UpdateMonitoringAndDebuggingSettings
 	}
 	}
 
 
-	if old.Theme != new.Theme {
-		return models.UpdateDisplaySettings
-	}
-
-	if old.TextSize != new.TextSize ||
-		old.ReducedMotion != new.ReducedMotion {
-		return models.UpdateAccessibilitySettings
-	}
-
 	if old.EmailSenderAddr != new.EmailSenderAddr ||
 	if old.EmailSenderAddr != new.EmailSenderAddr ||
 		old.EmailSenderUser != new.EmailSenderUser ||
 		old.EmailSenderUser != new.EmailSenderUser ||
 		old.EmailSenderPassword != new.EmailSenderPassword ||
 		old.EmailSenderPassword != new.EmailSenderPassword ||
@@ -409,3 +420,12 @@ func identifySettingsUpdateAction(old, new models.ServerSettings) models.Action
 
 
 	return models.Update
 	return models.Update
 }
 }
+
+// @Summary     Get feature flags for this server.
+// @Router      /api/server/feature_flags [get]
+// @Tags        Server
+// @Security    oauth2
+// @Success     200 {object} config.ServerSettings
+func getFeatureFlags(w http.ResponseWriter, r *http.Request) {
+	logic.ReturnSuccessResponseWithJson(w, r, logic.GetFeatureFlags(), "")
+}

+ 250 - 14
controllers/user.go

@@ -6,13 +6,14 @@ import (
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
-	"github.com/pquerna/otp"
-	"golang.org/x/crypto/bcrypt"
 	"image/png"
 	"image/png"
 	"net/http"
 	"net/http"
 	"reflect"
 	"reflect"
 	"time"
 	"time"
 
 
+	"github.com/pquerna/otp"
+	"golang.org/x/crypto/bcrypt"
+
 	"github.com/google/uuid"
 	"github.com/google/uuid"
 	"github.com/gorilla/mux"
 	"github.com/gorilla/mux"
 	"github.com/gorilla/websocket"
 	"github.com/gorilla/websocket"
@@ -49,6 +50,8 @@ func userHandlers(r *mux.Router) {
 	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUser)))).Methods(http.MethodGet)
 	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUser)))).Methods(http.MethodGet)
 	r.HandleFunc("/api/users/{username}/enable", logic.SecurityCheck(true, http.HandlerFunc(enableUserAccount))).Methods(http.MethodPost)
 	r.HandleFunc("/api/users/{username}/enable", logic.SecurityCheck(true, http.HandlerFunc(enableUserAccount))).Methods(http.MethodPost)
 	r.HandleFunc("/api/users/{username}/disable", logic.SecurityCheck(true, http.HandlerFunc(disableUserAccount))).Methods(http.MethodPost)
 	r.HandleFunc("/api/users/{username}/disable", logic.SecurityCheck(true, http.HandlerFunc(disableUserAccount))).Methods(http.MethodPost)
+	r.HandleFunc("/api/users/{username}/settings", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUserSettings)))).Methods(http.MethodGet)
+	r.HandleFunc("/api/users/{username}/settings", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(updateUserSettings)))).Methods(http.MethodPut)
 	r.HandleFunc("/api/v1/users", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUserV1)))).Methods(http.MethodGet)
 	r.HandleFunc("/api/v1/users", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUserV1)))).Methods(http.MethodGet)
 	r.HandleFunc("/api/users", logic.SecurityCheck(true, http.HandlerFunc(getUsers))).Methods(http.MethodGet)
 	r.HandleFunc("/api/users", logic.SecurityCheck(true, http.HandlerFunc(getUsers))).Methods(http.MethodGet)
 	r.HandleFunc("/api/v1/users/roles", logic.SecurityCheck(true, http.HandlerFunc(ListRoles))).Methods(http.MethodGet)
 	r.HandleFunc("/api/v1/users/roles", logic.SecurityCheck(true, http.HandlerFunc(ListRoles))).Methods(http.MethodGet)
@@ -255,6 +258,10 @@ func deleteUserAccessTokens(w http.ResponseWriter, r *http.Request) {
 // @Failure     401 {object} models.ErrorResponse
 // @Failure     401 {object} models.ErrorResponse
 // @Failure     500 {object} models.ErrorResponse
 // @Failure     500 {object} models.ErrorResponse
 func authenticateUser(response http.ResponseWriter, request *http.Request) {
 func authenticateUser(response http.ResponseWriter, request *http.Request) {
+	appName := request.Header.Get("X-Application-Name")
+	if appName == "" {
+		appName = logic.NetmakerDesktopApp
+	}
 
 
 	// Auth request consists of Mac Address and Password (from node that is authorizing
 	// Auth request consists of Mac Address and Password (from node that is authorizing
 	// in case of Master, auth is ignored and mac is set to "mastermac"
 	// in case of Master, auth is ignored and mac is set to "mastermac"
@@ -289,7 +296,7 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 		return
 		return
 	}
 	}
 
 
-	if !user.IsSuperAdmin && !logic.IsBasicAuthEnabled() {
+	if user.PlatformRoleID != models.SuperAdminRole && !logic.IsBasicAuthEnabled() {
 		logic.ReturnErrorResponse(
 		logic.ReturnErrorResponse(
 			response,
 			response,
 			request,
 			request,
@@ -313,7 +320,7 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 	}
 	}
 
 
 	username := authRequest.UserName
 	username := authRequest.UserName
-	jwt, err := logic.VerifyAuthRequest(authRequest)
+	jwt, err := logic.VerifyAuthRequest(authRequest, appName)
 	if err != nil {
 	if err != nil {
 		logger.Log(0, username, "user validation failed: ",
 		logger.Log(0, username, "user validation failed: ",
 			err.Error())
 			err.Error())
@@ -637,6 +644,11 @@ func completeTOTPSetup(w http.ResponseWriter, r *http.Request) {
 func verifyTOTP(w http.ResponseWriter, r *http.Request) {
 func verifyTOTP(w http.ResponseWriter, r *http.Request) {
 	username := r.Header.Get("user")
 	username := r.Header.Get("user")
 
 
+	appName := r.Header.Get("X-Application-Name")
+	if appName == "" {
+		appName = logic.NetmakerDesktopApp
+	}
+
 	var req models.UserTOTPVerificationParams
 	var req models.UserTOTPVerificationParams
 	err := json.NewDecoder(r.Body).Decode(&req)
 	err := json.NewDecoder(r.Body).Decode(&req)
 	if err != nil {
 	if err != nil {
@@ -662,7 +674,7 @@ func verifyTOTP(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 
 
 	if totp.Validate(req.TOTP, user.TOTPSecret) {
 	if totp.Validate(req.TOTP, user.TOTPSecret) {
-		jwt, err := logic.CreateUserJWT(user.UserName, user.PlatformRoleID)
+		jwt, err := logic.CreateUserJWT(user.UserName, user.PlatformRoleID, appName)
 		if err != nil {
 		if err != nil {
 			err = fmt.Errorf("error creating token: %v", err)
 			err = fmt.Errorf("error creating token: %v", err)
 			logger.Log(0, err.Error())
 			logger.Log(0, err.Error())
@@ -765,6 +777,52 @@ func enableUserAccount(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
+	var caller *models.User
+	var isMaster bool
+	if r.Header.Get("user") == logic.MasterUser {
+		isMaster = true
+	} else {
+		caller, err = logic.GetUser(r.Header.Get("user"))
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+			return
+		}
+	}
+
+	if !isMaster && caller.UserName == user.UserName {
+		// This implies that a user is trying to enable themselves.
+		// This can never happen, since a disabled user cannot be
+		// authenticated.
+		err := fmt.Errorf("cannot enable self")
+		logger.Log(0, err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
+		return
+	}
+
+	switch user.PlatformRoleID {
+	case models.SuperAdminRole:
+		// This can never happen, since a superadmin user cannot
+		// be disabled.
+	case models.AdminRole:
+		if !isMaster && caller.PlatformRoleID != models.SuperAdminRole {
+			err = fmt.Errorf("%s cannot enable an admin", caller.PlatformRoleID)
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
+			return
+		}
+	case models.PlatformUser:
+		if !isMaster && caller.PlatformRoleID != models.SuperAdminRole && caller.PlatformRoleID != models.AdminRole {
+			err = fmt.Errorf("%s cannot enable a platform-user", caller.PlatformRoleID)
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
+			return
+		}
+	case models.ServiceUser:
+		if !isMaster && caller.PlatformRoleID != models.SuperAdminRole && caller.PlatformRoleID != models.AdminRole {
+			err = fmt.Errorf("%s cannot enable a service-user", caller.PlatformRoleID)
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
+			return
+		}
+	}
+
 	user.AccountDisabled = false
 	user.AccountDisabled = false
 	err = logic.UpsertUser(*user)
 	err = logic.UpsertUser(*user)
 	if err != nil {
 	if err != nil {
@@ -791,13 +849,51 @@ func disableUserAccount(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
-	if user.PlatformRoleID == models.SuperAdminRole {
-		err = errors.New("cannot disable super-admin user account")
-		logger.Log(0, err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+	var caller *models.User
+	var isMaster bool
+	if r.Header.Get("user") == logic.MasterUser {
+		isMaster = true
+	} else {
+		caller, err = logic.GetUser(r.Header.Get("user"))
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+			return
+		}
+	}
+
+	if !isMaster && caller.UserName == user.UserName {
+		// This implies that a user is trying to disable themselves.
+		// This should not be allowed.
+		err = fmt.Errorf("cannot disable self")
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
 		return
 		return
 	}
 	}
 
 
+	switch user.PlatformRoleID {
+	case models.SuperAdminRole:
+		err = errors.New("cannot disable a super-admin")
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
+		return
+	case models.AdminRole:
+		if !isMaster && caller.PlatformRoleID != models.SuperAdminRole {
+			err = fmt.Errorf("%s cannot disable an admin", caller.PlatformRoleID)
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
+			return
+		}
+	case models.PlatformUser:
+		if !isMaster && caller.PlatformRoleID != models.SuperAdminRole && caller.PlatformRoleID != models.AdminRole {
+			err = fmt.Errorf("%s cannot disable a platform-user", caller.PlatformRoleID)
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
+			return
+		}
+	case models.ServiceUser:
+		if !isMaster && caller.PlatformRoleID != models.SuperAdminRole && caller.PlatformRoleID != models.AdminRole {
+			err = fmt.Errorf("%s cannot disable a service-user", caller.PlatformRoleID)
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
+			return
+		}
+	}
+
 	user.AccountDisabled = true
 	user.AccountDisabled = true
 	err = logic.UpsertUser(*user)
 	err = logic.UpsertUser(*user)
 	if err != nil {
 	if err != nil {
@@ -805,9 +901,71 @@ func disableUserAccount(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 	}
 	}
 
 
+	go func() {
+		extclients, err := logic.GetAllExtClients()
+		if err != nil {
+			logger.Log(0, "failed to get user extclients:", err.Error())
+			return
+		}
+
+		for _, extclient := range extclients {
+			if extclient.OwnerID == user.UserName {
+				err = logic.DeleteExtClientAndCleanup(extclient)
+				if err != nil {
+					logger.Log(0, "failed to delete user extclient:", err.Error())
+				} else {
+					err := mq.PublishDeletedClientPeerUpdate(&extclient)
+					if err != nil {
+						logger.Log(0, "failed to publish deleted client peer update:", err.Error())
+					}
+				}
+			}
+		}
+	}()
+
 	logic.ReturnSuccessResponse(w, r, "user account disabled")
 	logic.ReturnSuccessResponse(w, r, "user account disabled")
 }
 }
 
 
+// @Summary     Get a user's preferences and settings
+// @Router      /api/users/{username}/settings [get]
+// @Tags        Users
+// @Param       username path string true "Username of the user"
+// @Success     200 {object} models.SuccessResponse
+func getUserSettings(w http.ResponseWriter, r *http.Request) {
+	userID := r.Header.Get("user")
+	userSettings := logic.GetUserSettings(userID)
+	logic.ReturnSuccessResponseWithJson(w, r, userSettings, "fetched user settings")
+}
+
+// @Summary     Update a user's preferences and settings
+// @Router      /api/users/{username}/settings [put]
+// @Tags        Users
+// @Param       username path string true "Username of the user"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func updateUserSettings(w http.ResponseWriter, r *http.Request) {
+	userID := r.Header.Get("user")
+	var req models.UserSettings
+	err := json.NewDecoder(r.Body).Decode(&req)
+	if err != nil {
+		logger.Log(0, "failed to decode request body: ", err.Error())
+		err = fmt.Errorf("invalid request body: %v", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	err = logic.UpsertUserSettings(userID, req)
+	if err != nil {
+		err = fmt.Errorf("failed to update user settings: %v", err.Error())
+		logger.Log(0, err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+
+	logic.ReturnSuccessResponseWithJson(w, r, req, "updated user settings")
+}
+
 // swagger:route GET /api/v1/users user getUserV1
 // swagger:route GET /api/v1/users user getUserV1
 //
 //
 // Get an individual user with role info.
 // Get an individual user with role info.
@@ -833,6 +991,9 @@ func getUserV1(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
+	user.NumAccessTokens, _ = (&schema.UserAccessToken{
+		UserName: user.UserName,
+	}).CountByUser(r.Context())
 	userRoleTemplate, err := logic.GetRole(user.PlatformRoleID)
 	userRoleTemplate, err := logic.GetRole(user.PlatformRoleID)
 	if err != nil {
 	if err != nil {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
@@ -845,11 +1006,9 @@ func getUserV1(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 	for gId := range user.UserGroups {
 	for gId := range user.UserGroups {
 		grp, err := logic.GetUserGroup(gId)
 		grp, err := logic.GetUserGroup(gId)
-		if err != nil {
-			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-			return
+		if err == nil {
+			resp.UserGroups[gId] = grp
 		}
 		}
-		resp.UserGroups[gId] = grp
 	}
 	}
 	logger.Log(2, r.Header.Get("user"), "fetched user", usernameFetched)
 	logger.Log(2, r.Header.Get("user"), "fetched user", usernameFetched)
 	logic.ReturnSuccessResponseWithJson(w, r, resp, "fetched user with role info")
 	logic.ReturnSuccessResponseWithJson(w, r, resp, "fetched user with role info")
@@ -871,12 +1030,16 @@ func getUsers(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	w.Header().Set("Content-Type", "application/json")
 
 
 	users, err := logic.GetUsers()
 	users, err := logic.GetUsers()
-
 	if err != nil {
 	if err != nil {
 		logger.Log(0, "failed to fetch users: ", err.Error())
 		logger.Log(0, "failed to fetch users: ", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
+	for i := range users {
+		users[i].NumAccessTokens, _ = (&schema.UserAccessToken{
+			UserName: users[i].UserName,
+		}).CountByUser(r.Context())
+	}
 
 
 	logic.SortUsers(users[:])
 	logic.SortUsers(users[:])
 	logger.Log(2, r.Header.Get("user"), "fetched users")
 	logger.Log(2, r.Header.Get("user"), "fetched users")
@@ -959,6 +1122,7 @@ func transferSuperAdmin(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
+	u.IsSuperAdmin = true
 	u.PlatformRoleID = models.SuperAdminRole
 	u.PlatformRoleID = models.SuperAdminRole
 	err = logic.UpsertUser(*u)
 	err = logic.UpsertUser(*u)
 	if err != nil {
 	if err != nil {
@@ -966,6 +1130,8 @@ func transferSuperAdmin(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
+
+	caller.IsSuperAdmin = false
 	caller.PlatformRoleID = models.AdminRole
 	caller.PlatformRoleID = models.AdminRole
 	err = logic.UpsertUser(*caller)
 	err = logic.UpsertUser(*caller)
 	if err != nil {
 	if err != nil {
@@ -1254,6 +1420,67 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 	logic.LogEvent(&e)
 	logic.LogEvent(&e)
 	go mq.PublishPeerUpdate(false)
 	go mq.PublishPeerUpdate(false)
+	go func() {
+		// Populating all the networks the user has access to by
+		// being a member of groups.
+		userMembershipNetworkAccess := make(map[models.NetworkID]struct{})
+		for groupID := range user.UserGroups {
+			userGroup, _ := logic.GetUserGroup(groupID)
+			for netID := range userGroup.NetworkRoles {
+				userMembershipNetworkAccess[netID] = struct{}{}
+			}
+		}
+
+		extclients, err := logic.GetAllExtClients()
+		if err != nil {
+			slog.Error("failed to fetch extclients", "error", err)
+			return
+		}
+
+		for _, extclient := range extclients {
+			if extclient.OwnerID != user.UserName {
+				continue
+			}
+
+			var shouldDelete bool
+			if user.PlatformRoleID == models.SuperAdminRole || user.PlatformRoleID == models.AdminRole {
+				// Super-admin and Admin's access is not determined by group membership
+				// or network roles. Even if a user is removed from the group, they
+				// continue to have access to the network.
+				// So, no need to delete the extclient.
+				shouldDelete = false
+			} else {
+				_, hasAccess := user.NetworkRoles[models.NetworkID(extclient.Network)]
+				if hasAccess {
+					// The user has access to the network by themselves and not by
+					// virtue of being a member of the group.
+					// So, no need to delete the extclient.
+					shouldDelete = false
+				} else {
+					_, hasAccessThroughGroups := userMembershipNetworkAccess[models.NetworkID(extclient.Network)]
+					if !hasAccessThroughGroups {
+						// The user does not have access to the network by either
+						// being a Super-admin or Admin, by network roles or by virtue
+						// of being a member a group that has access to the network.
+						// So, delete the extclient.
+						shouldDelete = true
+					}
+				}
+			}
+
+			if shouldDelete {
+				err = logic.DeleteExtClientAndCleanup(extclient)
+				if err != nil {
+					slog.Error("failed to delete extclient",
+						"id", extclient.ClientID, "owner", user.UserName, "error", err)
+				} else {
+					if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil {
+						slog.Error("error setting ext peers: " + err.Error())
+					}
+				}
+			}
+		}
+	}()
 	logger.Log(1, username, "was updated")
 	logger.Log(1, username, "was updated")
 	json.NewEncoder(w).Encode(logic.ToReturnUser(*user))
 	json.NewEncoder(w).Encode(logic.ToReturnUser(*user))
 }
 }
@@ -1324,6 +1551,14 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
 			return
 			return
 		}
 		}
 	}
 	}
+
+	if user.AuthType == models.OAuth || user.ExternalIdentityProviderID != "" {
+		err = fmt.Errorf("cannot delete idp user %s", username)
+		logger.Log(0, username, "failed to delete user: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
 	err = logic.DeleteUser(username)
 	err = logic.DeleteUser(username)
 	if err != nil {
 	if err != nil {
 		logger.Log(0, username,
 		logger.Log(0, username,
@@ -1366,6 +1601,7 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
 				}
 				}
 			}
 			}
 		}
 		}
+		_ = logic.DeleteUserInvite(user.UserName)
 		mq.PublishPeerUpdate(false)
 		mq.PublishPeerUpdate(false)
 		if servercfg.IsDNSMode() {
 		if servercfg.IsDNSMode() {
 			logic.SetDNS()
 			logic.SetDNS()

+ 20 - 20
go.mod

@@ -7,24 +7,24 @@ toolchain go1.23.7
 require (
 require (
 	github.com/blang/semver v3.5.1+incompatible
 	github.com/blang/semver v3.5.1+incompatible
 	github.com/eclipse/paho.mqtt.golang v1.5.0
 	github.com/eclipse/paho.mqtt.golang v1.5.0
-	github.com/go-playground/validator/v10 v10.26.0
+	github.com/go-playground/validator/v10 v10.27.0
 	github.com/golang-jwt/jwt/v4 v4.5.2
 	github.com/golang-jwt/jwt/v4 v4.5.2
 	github.com/google/uuid v1.6.0
 	github.com/google/uuid v1.6.0
 	github.com/gorilla/handlers v1.5.2
 	github.com/gorilla/handlers v1.5.2
 	github.com/gorilla/mux v1.8.1
 	github.com/gorilla/mux v1.8.1
 	github.com/lib/pq v1.10.9
 	github.com/lib/pq v1.10.9
-	github.com/mattn/go-sqlite3 v1.14.28
+	github.com/mattn/go-sqlite3 v1.14.32
 	github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa
 	github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa
 	github.com/seancfoley/ipaddress-go v1.7.1
 	github.com/seancfoley/ipaddress-go v1.7.1
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
-	github.com/stretchr/testify v1.10.0
+	github.com/stretchr/testify v1.11.0
 	github.com/txn2/txeh v1.5.5
 	github.com/txn2/txeh v1.5.5
 	go.uber.org/automaxprocs v1.6.0
 	go.uber.org/automaxprocs v1.6.0
-	golang.org/x/crypto v0.39.0
-	golang.org/x/net v0.41.0 // indirect
+	golang.org/x/crypto v0.41.0
+	golang.org/x/net v0.43.0 // indirect
 	golang.org/x/oauth2 v0.30.0
 	golang.org/x/oauth2 v0.30.0
-	golang.org/x/sys v0.33.0 // indirect
-	golang.org/x/text v0.26.0 // indirect
+	golang.org/x/sys v0.35.0 // indirect
+	golang.org/x/text v0.28.0 // indirect
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb
 	gopkg.in/yaml.v3 v3.0.1
 	gopkg.in/yaml.v3 v3.0.1
 )
 )
@@ -32,11 +32,11 @@ require (
 require (
 require (
 	filippo.io/edwards25519 v1.1.0
 	filippo.io/edwards25519 v1.1.0
 	github.com/c-robinson/iplib v1.0.8
 	github.com/c-robinson/iplib v1.0.8
-	github.com/posthog/posthog-go v1.5.12
+	github.com/posthog/posthog-go v1.6.4
 )
 )
 
 
 require (
 require (
-	github.com/coreos/go-oidc/v3 v3.14.1
+	github.com/coreos/go-oidc/v3 v3.15.0
 	github.com/gorilla/websocket v1.5.3
 	github.com/gorilla/websocket v1.5.3
 	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
 	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
 )
 )
@@ -49,31 +49,31 @@ require (
 	github.com/okta/okta-sdk-golang/v5 v5.0.6
 	github.com/okta/okta-sdk-golang/v5 v5.0.6
 	github.com/pquerna/otp v1.5.0
 	github.com/pquerna/otp v1.5.0
 	github.com/spf13/cobra v1.9.1
 	github.com/spf13/cobra v1.9.1
-	google.golang.org/api v0.238.0
+	google.golang.org/api v0.248.0
 	gopkg.in/mail.v2 v2.3.1
 	gopkg.in/mail.v2 v2.3.1
-	gorm.io/datatypes v1.2.5
+	gorm.io/datatypes v1.2.6
 	gorm.io/driver/postgres v1.6.0
 	gorm.io/driver/postgres v1.6.0
 	gorm.io/driver/sqlite v1.6.0
 	gorm.io/driver/sqlite v1.6.0
-	gorm.io/gorm v1.30.0
+	gorm.io/gorm v1.30.1
 )
 )
 
 
 require (
 require (
-	cloud.google.com/go/auth v0.16.2 // indirect
+	cloud.google.com/go/auth v0.16.5 // indirect
 	cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
 	cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
-	cloud.google.com/go/compute/metadata v0.7.0 // indirect
+	cloud.google.com/go/compute/metadata v0.8.0 // indirect
 	github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
 	github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
 	github.com/cenkalti/backoff/v4 v4.1.3 // indirect
 	github.com/cenkalti/backoff/v4 v4.1.3 // indirect
 	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
 	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
 	github.com/gabriel-vasile/mimetype v1.4.8 // indirect
 	github.com/gabriel-vasile/mimetype v1.4.8 // indirect
 	github.com/go-jose/go-jose/v3 v3.0.3 // indirect
 	github.com/go-jose/go-jose/v3 v3.0.3 // indirect
 	github.com/go-jose/go-jose/v4 v4.0.5 // indirect
 	github.com/go-jose/go-jose/v4 v4.0.5 // indirect
-	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-sql-driver/mysql v1.8.1 // indirect
 	github.com/go-sql-driver/mysql v1.8.1 // indirect
 	github.com/goccy/go-json v0.10.2 // indirect
 	github.com/goccy/go-json v0.10.2 // indirect
 	github.com/google/s2a-go v0.1.9 // indirect
 	github.com/google/s2a-go v0.1.9 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
-	github.com/googleapis/gax-go/v2 v2.14.2 // indirect
+	github.com/googleapis/gax-go/v2 v2.15.0 // indirect
 	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
 	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jackc/pgpassfile v1.0.0 // indirect
 	github.com/jackc/pgpassfile v1.0.0 // indirect
@@ -100,9 +100,9 @@ require (
 	go.opentelemetry.io/otel v1.36.0 // indirect
 	go.opentelemetry.io/otel v1.36.0 // indirect
 	go.opentelemetry.io/otel/metric v1.36.0 // indirect
 	go.opentelemetry.io/otel/metric v1.36.0 // indirect
 	go.opentelemetry.io/otel/trace v1.36.0 // indirect
 	go.opentelemetry.io/otel/trace v1.36.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
-	google.golang.org/grpc v1.73.0 // indirect
-	google.golang.org/protobuf v1.36.6 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
+	google.golang.org/grpc v1.74.2 // indirect
+	google.golang.org/protobuf v1.36.7 // indirect
 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
 	gorm.io/driver/mysql v1.5.6 // indirect
 	gorm.io/driver/mysql v1.5.6 // indirect
 )
 )
@@ -116,5 +116,5 @@ require (
 	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	golang.org/x/sync v0.15.0 // indirect
+	golang.org/x/sync v0.16.0 // indirect
 )
 )

+ 46 - 46
go.sum

@@ -1,9 +1,9 @@
-cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4=
-cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA=
+cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=
+cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=
 cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
 cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
 cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
 cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
-cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
-cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
+cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=
+cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
@@ -14,8 +14,8 @@ github.com/c-robinson/iplib v1.0.8 h1:exDRViDyL9UBLcfmlxxkY5odWX5092nPsQIykHXhIn
 github.com/c-robinson/iplib v1.0.8/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo=
 github.com/c-robinson/iplib v1.0.8/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo=
 github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
 github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
 github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
 github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
-github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
-github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
+github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg=
+github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 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=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -34,8 +34,8 @@ github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQr
 github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
 github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
 github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
 github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@@ -44,8 +44,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
 github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 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 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
-github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
+github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
+github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
 github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
 github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
 github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
 github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
 github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
 github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
@@ -68,8 +68,8 @@ 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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
 github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
 github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
 github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
-github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
-github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
+github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
+github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
 github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e h1:XmA6L9IPRdUr28a+SK/oMchGgQy159wvzXA5tJ7l+40=
 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/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 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
@@ -128,8 +128,8 @@ github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwM
 github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
-github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
+github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
 github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
 github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
 github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
 github.com/okta/okta-sdk-golang/v5 v5.0.6 h1:p7ptDMB1KxQ/7xSh+6FhMSybwl+ubTV4f1oL4N0Bu6U=
 github.com/okta/okta-sdk-golang/v5 v5.0.6 h1:p7ptDMB1KxQ/7xSh+6FhMSybwl+ubTV4f1oL4N0Bu6U=
@@ -140,8 +140,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/posthog/posthog-go v1.5.12 h1:nxK/z5QLCFxwzxV8GNvVd4Y1wJ++zJSWMGEtzU+/HLM=
-github.com/posthog/posthog-go v1.5.12/go.mod h1:ZPCind3bz8xDLK0Zhvpv1fQav6WfRcQDqTMfMXmna98=
+github.com/posthog/posthog-go v1.6.4 h1:vPo6Z8T1R+aUBugXg1+psD8qZYSOtFktzhj6H8rzOBI=
+github.com/posthog/posthog-go v1.6.4/go.mod h1:LcC1Nu4AgvV22EndTtrMXTy+7RGVC0MhChSw7Qk5XkY=
 github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
 github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
 github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
 github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
 github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
 github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
@@ -175,8 +175,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
-github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
+github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
 github.com/txn2/txeh v1.5.5 h1:UN4e/lCK5HGw/gGAi2GCVrNKg0GTCUWs7gs5riaZlz4=
 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/txn2/txeh v1.5.5/go.mod h1:qYzGG9kCzeVEI12geK4IlanHWY8X4uy/I3NcW7mk8g4=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@@ -202,8 +202,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 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.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
 golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
 golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
-golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
-golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
+golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
+golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
 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/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=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -214,15 +214,15 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
-golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
-golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
+golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
+golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
 golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
 golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
 golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
 golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
-golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
+golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -232,8 +232,8 @@ 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.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.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
-golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
+golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -246,8 +246,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.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.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
-golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
+golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
+golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
 golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
 golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
 golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -257,18 +257,18 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb h1:9aqVcYEDHmSNb0uOWukxV5lHV09WqiSiCuhEgWNETLY=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb h1:9aqVcYEDHmSNb0uOWukxV5lHV09WqiSiCuhEgWNETLY=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ=
-google.golang.org/api v0.238.0 h1:+EldkglWIg/pWjkq97sd+XxH7PxakNYoe/rkSTbnvOs=
-google.golang.org/api v0.238.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
-google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
-google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
-google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
-google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
-google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
-google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
-google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+google.golang.org/api v0.248.0 h1:hUotakSkcwGdYUqzCRc5yGYsg4wXxpkKlW5ryVqvC1Y=
+google.golang.org/api v0.248.0/go.mod h1:yAFUAF56Li7IuIQbTFoLwXTCI6XCFKueOlS7S9e4F9k=
+google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
+google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
+google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
+google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
+google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
+google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -279,16 +279,16 @@ gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I=
-gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4=
+gorm.io/datatypes v1.2.6 h1:KafLdXvFUhzNeL2ncm03Gl3eTLONQfNKZ+wJ+9Y4Nck=
+gorm.io/datatypes v1.2.6/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
 gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
 gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
 gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
 gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
 gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
 gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
 gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
 gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
 gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
 gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
 gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
 gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
-gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g=
-gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g=
+gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
+gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
 gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
 gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
-gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
-gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
+gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
+gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=

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

@@ -16,7 +16,7 @@ spec:
       hostNetwork: true
       hostNetwork: true
       containers:
       containers:
       - name: netclient
       - name: netclient
-        image: gravitl/netclient:v1.0.0
+        image: gravitl/netclient:v1.1.0
         env:
         env:
         - name: TOKEN
         - name: TOKEN
           value: "TOKEN_VALUE"
           value: "TOKEN_VALUE"

+ 1 - 1
k8s/client/netclient.yaml

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

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

@@ -15,7 +15,7 @@ spec:
     spec:
     spec:
       containers:
       containers:
       - name: netmaker-ui
       - name: netmaker-ui
-        image: gravitl/netmaker-ui:v1.0.0
+        image: gravitl/netmaker-ui:v1.1.0
         ports:
         ports:
         - containerPort: 443
         - containerPort: 443
         env:
         env:

+ 506 - 200
logic/acls.go

@@ -18,11 +18,18 @@ import (
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 )
 )
 
 
-// TODO: Write Diff Funcs
+var GetFwRulesForNodeAndPeerOnGw = getFwRulesForNodeAndPeerOnGw
 
 
-var IsNodeAllowedToCommunicate = isNodeAllowedToCommunicate
+var GetTagMapWithNodesByNetwork = getTagMapWithNodesByNetwork
 
 
-var GetFwRulesForNodeAndPeerOnGw = getFwRulesForNodeAndPeerOnGw
+var GetEgressUserRulesForNode = func(targetnode *models.Node,
+	rules map[string]models.AclRule) map[string]models.AclRule {
+	return rules
+}
+var GetUserAclRulesForNode = func(targetnode *models.Node,
+	rules map[string]models.AclRule) map[string]models.AclRule {
+	return rules
+}
 
 
 var GetFwRulesForUserNodesOnGw = func(node models.Node, nodes []models.Node) (rules []models.FwRule) { return }
 var GetFwRulesForUserNodesOnGw = func(node models.Node, nodes []models.Node) (rules []models.FwRule) { return }
 
 
@@ -47,6 +54,9 @@ func GetFwRulesOnIngressGateway(node models.Node) (rules []models.FwRule) {
 		if !nodeI.IsStatic || nodeI.IsUserNode {
 		if !nodeI.IsStatic || nodeI.IsUserNode {
 			continue
 			continue
 		}
 		}
+		if !node.StaticNode.Enabled {
+			continue
+		}
 		// if nodeI.StaticNode.IngressGatewayID != node.ID.String() {
 		// if nodeI.StaticNode.IngressGatewayID != node.ID.String() {
 		// 	continue
 		// 	continue
 		// }
 		// }
@@ -119,25 +129,37 @@ func GetFwRulesOnIngressGateway(node models.Node) (rules []models.FwRule) {
 			}
 			}
 
 
 			if relayedNode.Address.IP != nil {
 			if relayedNode.Address.IP != nil {
-				relayedFwRule := models.FwRule{
+				rules = append(rules, models.FwRule{
 					AllowedProtocol: models.ALL,
 					AllowedProtocol: models.ALL,
 					AllowedPorts:    []string{},
 					AllowedPorts:    []string{},
 					Allow:           true,
 					Allow:           true,
-				}
-				relayedFwRule.DstIP = relayedNode.AddressIPNet4()
-				relayedFwRule.SrcIP = node.NetworkRange
-				rules = append(rules, relayedFwRule)
+					DstIP:           relayedNode.AddressIPNet4(),
+					SrcIP:           node.NetworkRange,
+				})
+				rules = append(rules, models.FwRule{
+					AllowedProtocol: models.ALL,
+					AllowedPorts:    []string{},
+					Allow:           true,
+					DstIP:           node.NetworkRange,
+					SrcIP:           relayedNode.AddressIPNet4(),
+				})
 			}
 			}
 
 
 			if relayedNode.Address6.IP != nil {
 			if relayedNode.Address6.IP != nil {
-				relayedFwRule := models.FwRule{
+				rules = append(rules, models.FwRule{
 					AllowedProtocol: models.ALL,
 					AllowedProtocol: models.ALL,
 					AllowedPorts:    []string{},
 					AllowedPorts:    []string{},
 					Allow:           true,
 					Allow:           true,
-				}
-				relayedFwRule.DstIP = relayedNode.AddressIPNet6()
-				relayedFwRule.SrcIP = node.NetworkRange6
-				rules = append(rules, relayedFwRule)
+					DstIP:           relayedNode.AddressIPNet6(),
+					SrcIP:           node.NetworkRange6,
+				})
+				rules = append(rules, models.FwRule{
+					AllowedProtocol: models.ALL,
+					AllowedPorts:    []string{},
+					Allow:           true,
+					DstIP:           node.NetworkRange6,
+					SrcIP:           relayedNode.AddressIPNet6(),
+				})
 			}
 			}
 
 
 		}
 		}
@@ -273,35 +295,70 @@ func getFwRulesForNodeAndPeerOnGw(node, peer models.Node, allowedPolicies []mode
 				if err != nil {
 				if err != nil {
 					continue
 					continue
 				}
 				}
-				dstI.Value = e.Range
+				if len(e.DomainAns) > 0 {
+					for _, domainAnsI := range e.DomainAns {
+						dstI.Value = domainAnsI
+
+						ip, cidr, err := net.ParseCIDR(dstI.Value)
+						if err == nil {
+							if ip.To4() != nil {
+								if node.Address.IP != nil {
+									rules = append(rules, models.FwRule{
+										SrcIP: net.IPNet{
+											IP:   node.Address.IP,
+											Mask: net.CIDRMask(32, 32),
+										},
+										DstIP: *cidr,
+										Allow: true,
+									})
+								}
+							} else {
+								if node.Address6.IP != nil {
+									rules = append(rules, models.FwRule{
+										SrcIP: net.IPNet{
+											IP:   node.Address6.IP,
+											Mask: net.CIDRMask(128, 128),
+										},
+										DstIP: *cidr,
+										Allow: true,
+									})
+								}
+							}
 
 
-				ip, cidr, err := net.ParseCIDR(dstI.Value)
-				if err == nil {
-					if ip.To4() != nil {
-						if node.Address.IP != nil {
-							rules = append(rules, models.FwRule{
-								SrcIP: net.IPNet{
-									IP:   node.Address.IP,
-									Mask: net.CIDRMask(32, 32),
-								},
-								DstIP: *cidr,
-								Allow: true,
-							})
-						}
-					} else {
-						if node.Address6.IP != nil {
-							rules = append(rules, models.FwRule{
-								SrcIP: net.IPNet{
-									IP:   node.Address6.IP,
-									Mask: net.CIDRMask(128, 128),
-								},
-								DstIP: *cidr,
-								Allow: true,
-							})
 						}
 						}
 					}
 					}
+				} else {
+					dstI.Value = e.Range
+
+					ip, cidr, err := net.ParseCIDR(dstI.Value)
+					if err == nil {
+						if ip.To4() != nil {
+							if node.Address.IP != nil {
+								rules = append(rules, models.FwRule{
+									SrcIP: net.IPNet{
+										IP:   node.Address.IP,
+										Mask: net.CIDRMask(32, 32),
+									},
+									DstIP: *cidr,
+									Allow: true,
+								})
+							}
+						} else {
+							if node.Address6.IP != nil {
+								rules = append(rules, models.FwRule{
+									SrcIP: net.IPNet{
+										IP:   node.Address6.IP,
+										Mask: net.CIDRMask(128, 128),
+									},
+									DstIP: *cidr,
+									Allow: true,
+								})
+							}
+						}
 
 
+					}
 				}
 				}
+
 			}
 			}
 		}
 		}
 	}
 	}
@@ -345,6 +402,9 @@ func GetStaticNodeIps(node models.Node) (ips []net.IP) {
 		if !extclient.IsUserNode && defaultDevicePolicy.Enabled {
 		if !extclient.IsUserNode && defaultDevicePolicy.Enabled {
 			continue
 			continue
 		}
 		}
+		if !extclient.StaticNode.Enabled {
+			continue
+		}
 		if extclient.StaticNode.Address != "" {
 		if extclient.StaticNode.Address != "" {
 			ips = append(ips, extclient.StaticNode.AddressIPNet4().IP)
 			ips = append(ips, extclient.StaticNode.AddressIPNet4().IP)
 		}
 		}
@@ -375,7 +435,66 @@ var MigrateToGws = func() {
 
 
 }
 }
 
 
-func CheckIfNodeHasAccessToAllResources(targetnode *models.Node, acls []models.Acl) bool {
+var CheckIfAnyPolicyisUniDirectional = func(targetNode models.Node, acls []models.Acl) bool {
+	return false
+}
+
+func GetAclRulesForNode(targetnodeI *models.Node) (rules map[string]models.AclRule) {
+	targetnode := *targetnodeI
+	defer func() {
+		//if !targetnode.IsIngressGateway {
+		rules = GetUserAclRulesForNode(&targetnode, rules)
+		//}
+	}()
+	rules = make(map[string]models.AclRule)
+	if IsNodeAllowedToCommunicateWithAllRsrcs(targetnode) {
+		aclRule := models.AclRule{
+			ID:              fmt.Sprintf("%s-all-allowed-node-rule", targetnode.ID.String()),
+			AllowedProtocol: models.ALL,
+			Direction:       models.TrafficDirectionBi,
+			Allowed:         true,
+			IPList:          []net.IPNet{targetnode.NetworkRange},
+			IP6List:         []net.IPNet{targetnode.NetworkRange6},
+			Dst:             []net.IPNet{targetnode.AddressIPNet4()},
+			Dst6:            []net.IPNet{targetnode.AddressIPNet6()},
+		}
+		e := schema.Egress{Network: targetnode.Network}
+		egressRanges4 := []net.IPNet{}
+		egressRanges6 := []net.IPNet{}
+		eli, _ := e.ListByNetwork(db.WithContext(context.Background()))
+		for _, eI := range eli {
+			if !eI.Status || len(eI.Nodes) == 0 {
+				continue
+			}
+			if _, ok := eI.Nodes[targetnode.ID.String()]; ok {
+				if eI.Range != "" {
+					_, cidr, err := net.ParseCIDR(eI.Range)
+					if err == nil {
+						if cidr.IP.To4() != nil {
+							egressRanges4 = append(egressRanges4, *cidr)
+						} else {
+							egressRanges6 = append(egressRanges6, *cidr)
+						}
+					}
+				}
+			}
+		}
+		if len(egressRanges4) > 0 {
+			aclRule.Dst = append(aclRule.Dst, egressRanges4...)
+		}
+		if len(egressRanges6) > 0 {
+			aclRule.Dst6 = append(aclRule.Dst6, egressRanges6...)
+		}
+		rules[aclRule.ID] = aclRule
+		return
+	}
+	var taggedNodes map[models.TagID][]models.Node
+	if targetnode.IsIngressGateway {
+		taggedNodes = GetTagMapWithNodesByNetwork(models.NetworkID(targetnode.Network), false)
+	} else {
+		taggedNodes = GetTagMapWithNodesByNetwork(models.NetworkID(targetnode.Network), true)
+	}
+	acls := ListDevicePolicies(models.NetworkID(targetnode.Network))
 	var targetNodeTags = make(map[models.TagID]struct{})
 	var targetNodeTags = make(map[models.TagID]struct{})
 	if targetnode.Mutex != nil {
 	if targetnode.Mutex != nil {
 		targetnode.Mutex.Lock()
 		targetnode.Mutex.Lock()
@@ -389,113 +508,57 @@ func CheckIfNodeHasAccessToAllResources(targetnode *models.Node, acls []models.A
 	}
 	}
 	targetNodeTags[models.TagID(targetnode.ID.String())] = struct{}{}
 	targetNodeTags[models.TagID(targetnode.ID.String())] = struct{}{}
 	targetNodeTags["*"] = struct{}{}
 	targetNodeTags["*"] = struct{}{}
-	if targetnode.IsGw {
-		targetNodeTags[models.TagID(fmt.Sprintf("%s.%s", targetnode.Network, models.GwTagName))] = struct{}{}
-	}
 	for _, acl := range acls {
 	for _, acl := range acls {
-		if !acl.Enabled || acl.RuleType != models.DevicePolicy {
+		if !acl.Enabled {
 			continue
 			continue
 		}
 		}
 		srcTags := ConvAclTagToValueMap(acl.Src)
 		srcTags := ConvAclTagToValueMap(acl.Src)
 		dstTags := ConvAclTagToValueMap(acl.Dst)
 		dstTags := ConvAclTagToValueMap(acl.Dst)
-		_, srcAll := srcTags["*"]
-		_, dstAll := dstTags["*"]
-		for nodeTag := range targetNodeTags {
-
-			var existsInSrcTag bool
-			var existsInDstTag bool
-
-			if _, ok := srcTags[nodeTag.String()]; ok {
-				existsInSrcTag = true
-			}
-			if _, ok := srcTags[targetnode.ID.String()]; ok {
-				existsInSrcTag = true
-			}
-			if _, ok := dstTags[nodeTag.String()]; ok {
-				existsInDstTag = true
-			}
-			if _, ok := dstTags[targetnode.ID.String()]; ok {
-				existsInDstTag = true
-			}
-			if acl.AllowedDirection == models.TrafficDirectionBi {
-				if existsInSrcTag && dstAll || existsInDstTag && srcAll {
-					return true
-				}
-			} else {
-				if existsInDstTag && srcAll {
-					return true
-				}
-			}
-		}
-	}
-	return false
-}
-
-var CheckIfAnyPolicyisUniDirectional = func(targetNode models.Node, acls []models.Acl) bool {
-	return false
-}
-
-var CheckIfAnyActiveEgressPolicy = func(targetNode models.Node, acls []models.Acl) bool {
-	if !targetNode.EgressDetails.IsEgressGateway {
-		return false
-	}
-	var targetNodeTags = make(map[models.TagID]struct{})
-	targetNodeTags[models.TagID(targetNode.ID.String())] = struct{}{}
-	targetNodeTags["*"] = struct{}{}
-	if targetNode.IsGw {
-		targetNodeTags[models.TagID(fmt.Sprintf("%s.%s", targetNode.Network, models.GwTagName))] = struct{}{}
-	}
-	for _, acl := range acls {
-		if !acl.Enabled || acl.RuleType != models.DevicePolicy {
-			continue
-		}
-		srcTags := ConvAclTagToValueMap(acl.Src)
+		egressRanges4 := []net.IPNet{}
+		egressRanges6 := []net.IPNet{}
 		for _, dst := range acl.Dst {
 		for _, dst := range acl.Dst {
-			if dst.ID == models.EgressID {
-				e := schema.Egress{ID: dst.Value}
-				err := e.Get(db.WithContext(context.TODO()))
-				if err == nil && e.Status {
-					for nodeTag := range targetNodeTags {
-						if _, ok := srcTags[nodeTag.String()]; ok {
-							return true
-						}
-						if _, ok := srcTags[targetNode.ID.String()]; ok {
-							return true
+			if dst.Value == "*" {
+				e := schema.Egress{Network: targetnode.Network}
+				eli, _ := e.ListByNetwork(db.WithContext(context.Background()))
+				for _, eI := range eli {
+					if !eI.Status || len(eI.Nodes) == 0 {
+						continue
+					}
+					if _, ok := eI.Nodes[targetnode.ID.String()]; ok {
+						if eI.Range != "" {
+							_, cidr, err := net.ParseCIDR(eI.Range)
+							if err == nil {
+								if cidr.IP.To4() != nil {
+									egressRanges4 = append(egressRanges4, *cidr)
+								} else {
+									egressRanges6 = append(egressRanges6, *cidr)
+								}
+							}
 						}
 						}
 					}
 					}
 				}
 				}
+				break
 			}
 			}
-		}
-	}
-	return false
-}
-
-var GetAclRulesForNode = func(targetnodeI *models.Node) (rules map[string]models.AclRule) {
-	targetnode := *targetnodeI
-
-	rules = make(map[string]models.AclRule)
-
-	acls := ListDevicePolicies(models.NetworkID(targetnode.Network))
-	targetNodeTags := make(map[models.TagID]struct{})
-	targetNodeTags[models.TagID(targetnode.ID.String())] = struct{}{}
-	targetNodeTags["*"] = struct{}{}
-	for _, acl := range acls {
-		if !acl.Enabled {
-			continue
-		}
-		srcTags := ConvAclTagToValueMap(acl.Src)
-		dstTags := ConvAclTagToValueMap(acl.Dst)
-		nodes := []models.Node{}
-		for _, dst := range acl.Dst {
 			if dst.ID == models.EgressID {
 			if dst.ID == models.EgressID {
 				e := schema.Egress{ID: dst.Value}
 				e := schema.Egress{ID: dst.Value}
 				err := e.Get(db.WithContext(context.TODO()))
 				err := e.Get(db.WithContext(context.TODO()))
-				if err == nil && e.Status {
-					for nodeID := range e.Nodes {
-						dstTags[nodeID] = struct{}{}
+				if err == nil && e.Status && len(e.Nodes) > 0 {
+					if _, ok := e.Nodes[targetnode.ID.String()]; ok {
+						if e.Range != "" {
+							_, cidr, err := net.ParseCIDR(e.Range)
+							if err == nil {
+								if cidr.IP.To4() != nil {
+									egressRanges4 = append(egressRanges4, *cidr)
+								} else {
+									egressRanges6 = append(egressRanges6, *cidr)
+								}
+							}
+						}
 					}
 					}
+
 				}
 				}
 			}
 			}
+
 		}
 		}
 		_, srcAll := srcTags["*"]
 		_, srcAll := srcTags["*"]
 		_, dstAll := dstTags["*"]
 		_, dstAll := dstTags["*"]
@@ -505,6 +568,14 @@ var GetAclRulesForNode = func(targetnodeI *models.Node) (rules map[string]models
 			AllowedPorts:    acl.Port,
 			AllowedPorts:    acl.Port,
 			Direction:       acl.AllowedDirection,
 			Direction:       acl.AllowedDirection,
 			Allowed:         true,
 			Allowed:         true,
+			Dst:             []net.IPNet{targetnode.AddressIPNet4()},
+			Dst6:            []net.IPNet{targetnode.AddressIPNet6()},
+		}
+		if len(egressRanges4) > 0 {
+			aclRule.Dst = append(aclRule.Dst, egressRanges4...)
+		}
+		if len(egressRanges6) > 0 {
+			aclRule.Dst6 = append(aclRule.Dst6, egressRanges6...)
 		}
 		}
 		for nodeTag := range targetNodeTags {
 		for nodeTag := range targetNodeTags {
 			if acl.AllowedDirection == models.TrafficDirectionBi {
 			if acl.AllowedDirection == models.TrafficDirectionBi {
@@ -531,35 +602,35 @@ var GetAclRulesForNode = func(targetnodeI *models.Node) (rules map[string]models
 							continue
 							continue
 						}
 						}
 						// Get peers in the tags and add allowed rules
 						// Get peers in the tags and add allowed rules
+						nodes := taggedNodes[models.TagID(dst)]
 						if dst != targetnode.ID.String() {
 						if dst != targetnode.ID.String() {
 							node, err := GetNodeByID(dst)
 							node, err := GetNodeByID(dst)
 							if err == nil {
 							if err == nil {
 								nodes = append(nodes, node)
 								nodes = append(nodes, node)
 							}
 							}
 						}
 						}
-					}
 
 
-					for _, node := range nodes {
-						if node.ID == targetnode.ID {
-							continue
-						}
-						if node.IsStatic && node.StaticNode.IngressGatewayID == targetnode.ID.String() {
-							continue
-						}
-						if node.Address.IP != nil {
-							aclRule.IPList = append(aclRule.IPList, node.AddressIPNet4())
-						}
-						if node.Address6.IP != nil {
-							aclRule.IP6List = append(aclRule.IP6List, node.AddressIPNet6())
-						}
-						if node.IsStatic && node.StaticNode.Address != "" {
-							aclRule.IPList = append(aclRule.IPList, node.StaticNode.AddressIPNet4())
-						}
-						if node.IsStatic && node.StaticNode.Address6 != "" {
-							aclRule.IP6List = append(aclRule.IP6List, node.StaticNode.AddressIPNet6())
+						for _, node := range nodes {
+							if node.ID == targetnode.ID {
+								continue
+							}
+							if node.IsStatic && node.StaticNode.IngressGatewayID == targetnode.ID.String() {
+								continue
+							}
+							if node.Address.IP != nil {
+								aclRule.IPList = append(aclRule.IPList, node.AddressIPNet4())
+							}
+							if node.Address6.IP != nil {
+								aclRule.IP6List = append(aclRule.IP6List, node.AddressIPNet6())
+							}
+							if node.IsStatic && node.StaticNode.Address != "" {
+								aclRule.IPList = append(aclRule.IPList, node.StaticNode.AddressIPNet4())
+							}
+							if node.IsStatic && node.StaticNode.Address6 != "" {
+								aclRule.IP6List = append(aclRule.IP6List, node.StaticNode.AddressIPNet6())
+							}
 						}
 						}
 					}
 					}
-
 				}
 				}
 				if existsInDstTag /*&& !existsInSrcTag*/ {
 				if existsInDstTag /*&& !existsInSrcTag*/ {
 					// get all src tags
 					// get all src tags
@@ -568,18 +639,165 @@ var GetAclRulesForNode = func(targetnodeI *models.Node) (rules map[string]models
 							continue
 							continue
 						}
 						}
 						// Get peers in the tags and add allowed rules
 						// Get peers in the tags and add allowed rules
+						nodes := taggedNodes[models.TagID(src)]
 						if src != targetnode.ID.String() {
 						if src != targetnode.ID.String() {
 							node, err := GetNodeByID(src)
 							node, err := GetNodeByID(src)
 							if err == nil {
 							if err == nil {
 								nodes = append(nodes, node)
 								nodes = append(nodes, node)
 							}
 							}
 						}
 						}
+						for _, node := range nodes {
+							if node.ID == targetnode.ID {
+								continue
+							}
+							if node.IsStatic && node.StaticNode.IngressGatewayID == targetnode.ID.String() {
+								continue
+							}
+							if node.Address.IP != nil {
+								aclRule.IPList = append(aclRule.IPList, node.AddressIPNet4())
+							}
+							if node.Address6.IP != nil {
+								aclRule.IP6List = append(aclRule.IP6List, node.AddressIPNet6())
+							}
+							if node.IsStatic && node.StaticNode.Address != "" {
+								aclRule.IPList = append(aclRule.IPList, node.StaticNode.AddressIPNet4())
+							}
+							if node.IsStatic && node.StaticNode.Address6 != "" {
+								aclRule.IP6List = append(aclRule.IP6List, node.StaticNode.AddressIPNet6())
+							}
+						}
 					}
 					}
-					for _, node := range nodes {
-						if node.ID == targetnode.ID {
+				}
+			} else {
+				_, all := dstTags["*"]
+				if _, ok := dstTags[nodeTag.String()]; ok || all {
+					// get all src tags
+					for src := range srcTags {
+						if src == nodeTag.String() {
 							continue
 							continue
 						}
 						}
-						if node.IsStatic && node.StaticNode.IngressGatewayID == targetnode.ID.String() {
+						// Get peers in the tags and add allowed rules
+						nodes := taggedNodes[models.TagID(src)]
+						for _, node := range nodes {
+							if node.ID == targetnode.ID {
+								continue
+							}
+							if node.IsStatic && node.StaticNode.IngressGatewayID == targetnode.ID.String() {
+								continue
+							}
+							if node.Address.IP != nil {
+								aclRule.IPList = append(aclRule.IPList, node.AddressIPNet4())
+							}
+							if node.Address6.IP != nil {
+								aclRule.IP6List = append(aclRule.IP6List, node.AddressIPNet6())
+							}
+							if node.IsStatic && node.StaticNode.Address != "" {
+								aclRule.IPList = append(aclRule.IPList, node.StaticNode.AddressIPNet4())
+							}
+							if node.IsStatic && node.StaticNode.Address6 != "" {
+								aclRule.IP6List = append(aclRule.IP6List, node.StaticNode.AddressIPNet6())
+							}
+						}
+					}
+				}
+			}
+
+		}
+
+		if len(aclRule.IPList) > 0 || len(aclRule.IP6List) > 0 {
+			aclRule.IPList = UniqueIPNetList(aclRule.IPList)
+			aclRule.IP6List = UniqueIPNetList(aclRule.IP6List)
+			rules[acl.ID] = aclRule
+		}
+	}
+	return rules
+}
+
+func GetEgressRulesForNode(targetnode models.Node) (rules map[string]models.AclRule) {
+	rules = make(map[string]models.AclRule)
+	defer func() {
+		rules = GetEgressUserRulesForNode(&targetnode, rules)
+	}()
+	taggedNodes := GetTagMapWithNodesByNetwork(models.NetworkID(targetnode.Network), true)
+
+	acls := ListDevicePolicies(models.NetworkID(targetnode.Network))
+	var targetNodeTags = make(map[models.TagID]struct{})
+	targetNodeTags[models.TagID(targetnode.ID.String())] = struct{}{}
+	targetNodeTags["*"] = struct{}{}
+	if targetnode.IsGw && !servercfg.IsPro {
+		targetNodeTags[models.TagID(fmt.Sprintf("%s.%s", targetnode.Network, models.GwTagName))] = struct{}{}
+	}
+
+	egs, _ := (&schema.Egress{Network: targetnode.Network}).ListByNetwork(db.WithContext(context.TODO()))
+	if len(egs) == 0 {
+		return
+	}
+	var egressIDMap = make(map[string]schema.Egress)
+	for _, egI := range egs {
+		if !egI.Status {
+			continue
+		}
+		if _, ok := egI.Nodes[targetnode.ID.String()]; ok {
+			egressIDMap[egI.ID] = egI
+		}
+	}
+	if len(egressIDMap) == 0 {
+		return
+	}
+	for _, acl := range acls {
+		if !acl.Enabled {
+			continue
+		}
+		srcTags := ConvAclTagToValueMap(acl.Src)
+		dstTags := ConvAclTagToValueMap(acl.Dst)
+		_, dstAll := dstTags["*"]
+		aclRule := models.AclRule{
+			ID:              acl.ID,
+			AllowedProtocol: acl.Proto,
+			AllowedPorts:    acl.Port,
+			Direction:       acl.AllowedDirection,
+			Allowed:         true,
+		}
+		for egressID, egI := range egressIDMap {
+			if _, ok := dstTags[egressID]; ok || dstAll {
+				if servercfg.IsPro && egI.Domain != "" && len(egI.DomainAns) > 0 {
+					for _, domainAnsI := range egI.DomainAns {
+						ip, cidr, err := net.ParseCIDR(domainAnsI)
+						if err == nil {
+							if ip.To4() != nil {
+								aclRule.Dst = append(aclRule.Dst, *cidr)
+							} else {
+								aclRule.Dst6 = append(aclRule.Dst6, *cidr)
+							}
+						}
+					}
+				} else {
+					ip, cidr, err := net.ParseCIDR(egI.Range)
+					if err == nil {
+						if ip.To4() != nil {
+							aclRule.Dst = append(aclRule.Dst, *cidr)
+						} else {
+							aclRule.Dst6 = append(aclRule.Dst6, *cidr)
+						}
+					}
+				}
+
+				_, srcAll := srcTags["*"]
+				if srcAll {
+					if targetnode.NetworkRange.IP != nil {
+						aclRule.IPList = append(aclRule.IPList, targetnode.NetworkRange)
+					}
+					if targetnode.NetworkRange6.IP != nil {
+						aclRule.IP6List = append(aclRule.IP6List, targetnode.NetworkRange6)
+					}
+					continue
+				}
+				// get all src tags
+				for src := range srcTags {
+					// Get peers in the tags and add allowed rules
+					nodes := taggedNodes[models.TagID(src)]
+					for _, node := range nodes {
+						if node.ID == targetnode.ID {
 							continue
 							continue
 						}
 						}
 						if node.Address.IP != nil {
 						if node.Address.IP != nil {
@@ -595,10 +813,8 @@ var GetAclRulesForNode = func(targetnodeI *models.Node) (rules map[string]models
 							aclRule.IP6List = append(aclRule.IP6List, node.StaticNode.AddressIPNet6())
 							aclRule.IP6List = append(aclRule.IP6List, node.StaticNode.AddressIPNet6())
 						}
 						}
 					}
 					}
-
 				}
 				}
 			}
 			}
-
 		}
 		}
 
 
 		if len(aclRule.IPList) > 0 || len(aclRule.IP6List) > 0 {
 		if len(aclRule.IPList) > 0 || len(aclRule.IP6List) > 0 {
@@ -606,14 +822,34 @@ var GetAclRulesForNode = func(targetnodeI *models.Node) (rules map[string]models
 			aclRule.IP6List = UniqueIPNetList(aclRule.IP6List)
 			aclRule.IP6List = UniqueIPNetList(aclRule.IP6List)
 			rules[acl.ID] = aclRule
 			rules[acl.ID] = aclRule
 		}
 		}
+
 	}
 	}
-	return rules
-}
 
 
-var GetEgressRulesForNode = func(targetnode models.Node) (rules map[string]models.AclRule) {
 	return
 	return
 }
 }
-var GetAclRuleForInetGw = func(targetnode models.Node) (rules map[string]models.AclRule) {
+
+func GetAclRuleForInetGw(targetnode models.Node) (rules map[string]models.AclRule) {
+	rules = make(map[string]models.AclRule)
+	if targetnode.IsInternetGateway {
+		aclRule := models.AclRule{
+			ID:              fmt.Sprintf("%s-inet-gw-internal-rule", targetnode.ID.String()),
+			AllowedProtocol: models.ALL,
+			AllowedPorts:    []string{},
+			Direction:       models.TrafficDirectionBi,
+			Allowed:         true,
+		}
+		if targetnode.NetworkRange.IP != nil {
+			aclRule.IPList = append(aclRule.IPList, targetnode.NetworkRange)
+			_, allIpv4, _ := net.ParseCIDR(IPv4Network)
+			aclRule.Dst = append(aclRule.Dst, *allIpv4)
+		}
+		if targetnode.NetworkRange6.IP != nil {
+			aclRule.IP6List = append(aclRule.IP6List, targetnode.NetworkRange6)
+			_, allIpv6, _ := net.ParseCIDR(IPv6Network)
+			aclRule.Dst6 = append(aclRule.Dst6, *allIpv6)
+		}
+		rules[aclRule.ID] = aclRule
+	}
 	return
 	return
 }
 }
 
 
@@ -861,34 +1097,6 @@ func CheckTagGroupPolicy(srcMap, dstMap map[string]struct{}, node, peer models.N
 	return false
 	return false
 }
 }
 
 
-var GetInetClientsFromAclPolicies = func(eID string) (inetClientIDs []string) {
-	e := schema.Egress{ID: eID}
-	err := e.Get(db.WithContext(context.TODO()))
-	if err != nil || !e.Status {
-		return
-	}
-	acls, _ := ListAclsByNetwork(models.NetworkID(e.Network))
-	for _, acl := range acls {
-		for _, dstI := range acl.Dst {
-			if dstI.ID == models.EgressID {
-				if dstI.Value != eID {
-					continue
-				}
-				for _, srcI := range acl.Src {
-					if srcI.Value == "*" {
-						continue
-					}
-					if srcI.ID == models.NodeID {
-						inetClientIDs = append(inetClientIDs, srcI.Value)
-					}
-				}
-			}
-		}
-	}
-	return
-
-}
-
 var (
 var (
 	CreateDefaultTags = func(netID models.NetworkID) {}
 	CreateDefaultTags = func(netID models.NetworkID) {}
 
 
@@ -915,6 +1123,12 @@ func MigrateAclPolicies() {
 			acl.Port = []string{}
 			acl.Port = []string{}
 			UpsertAcl(acl)
 			UpsertAcl(acl)
 		}
 		}
+		if !servercfg.IsPro {
+			if acl.AllowedDirection == models.TrafficDirectionUni {
+				acl.AllowedDirection = models.TrafficDirectionBi
+				UpsertAcl(acl)
+			}
+		}
 	}
 	}
 
 
 }
 }
@@ -934,11 +1148,22 @@ func IsNodeAllowedToCommunicateWithAllRsrcs(node models.Node) bool {
 	} else {
 	} else {
 		nodeId = node.ID.String()
 		nodeId = node.ID.String()
 	}
 	}
-	nodeTags := make(map[models.TagID]struct{})
-
+	var nodeTags map[models.TagID]struct{}
+	if node.Mutex != nil {
+		node.Mutex.Lock()
+		nodeTags = maps.Clone(node.Tags)
+		node.Mutex.Unlock()
+	} else {
+		nodeTags = maps.Clone(node.Tags)
+	}
+	if nodeTags == nil {
+		nodeTags = make(map[models.TagID]struct{})
+	}
+	nodeTags[models.TagID(node.ID.String())] = struct{}{}
+	nodeTags["*"] = struct{}{}
 	nodeTags[models.TagID(nodeId)] = struct{}{}
 	nodeTags[models.TagID(nodeId)] = struct{}{}
-	if node.IsGw {
-		nodeTags[models.TagID(fmt.Sprintf("%s.%s", node.Network, models.GwTagName))] = struct{}{}
+	if !servercfg.IsPro && node.IsGw {
+		node.Tags[models.TagID(fmt.Sprintf("%s.%s", node.Network, models.GwTagName))] = struct{}{}
 	}
 	}
 	// list device policies
 	// list device policies
 	policies := ListDevicePolicies(models.NetworkID(node.Network))
 	policies := ListDevicePolicies(models.NetworkID(node.Network))
@@ -948,6 +1173,9 @@ func IsNodeAllowedToCommunicateWithAllRsrcs(node models.Node) bool {
 		srcMap = nil
 		srcMap = nil
 		dstMap = nil
 		dstMap = nil
 	}()
 	}()
+	if CheckIfAnyPolicyisUniDirectional(node, policies) {
+		return false
+	}
 	for _, policy := range policies {
 	for _, policy := range policies {
 		if !policy.Enabled {
 		if !policy.Enabled {
 			continue
 			continue
@@ -974,8 +1202,14 @@ func IsNodeAllowedToCommunicateWithAllRsrcs(node models.Node) bool {
 }
 }
 
 
 // IsNodeAllowedToCommunicate - check node is allowed to communicate with the peer // ADD ALLOWED DIRECTION - 0 => node -> peer, 1 => peer-> node,
 // IsNodeAllowedToCommunicate - check node is allowed to communicate with the peer // ADD ALLOWED DIRECTION - 0 => node -> peer, 1 => peer-> node,
-func isNodeAllowedToCommunicate(node, peer models.Node, checkDefaultPolicy bool) (bool, []models.Acl) {
+func IsNodeAllowedToCommunicate(node, peer models.Node, checkDefaultPolicy bool) (bool, []models.Acl) {
 	var nodeId, peerId string
 	var nodeId, peerId string
+	// if peer.IsFailOver && node.FailedOverBy != uuid.Nil && node.FailedOverBy == peer.ID {
+	// 	return true, []models.Acl{}
+	// }
+	// if node.IsFailOver && peer.FailedOverBy != uuid.Nil && peer.FailedOverBy == node.ID {
+	// 	return true, []models.Acl{}
+	// }
 	// if node.IsGw && peer.IsRelayed && peer.RelayedBy == node.ID.String() {
 	// if node.IsGw && peer.IsRelayed && peer.RelayedBy == node.ID.String() {
 	// 	return true, []models.Acl{}
 	// 	return true, []models.Acl{}
 	// }
 	// }
@@ -995,17 +1229,29 @@ func isNodeAllowedToCommunicate(node, peer models.Node, checkDefaultPolicy bool)
 		peerId = peer.ID.String()
 		peerId = peer.ID.String()
 	}
 	}
 
 
-	nodeTags := make(map[models.TagID]struct{})
-	peerTags := make(map[models.TagID]struct{})
-
-	nodeTags[models.TagID(nodeId)] = struct{}{}
-	peerTags[models.TagID(peerId)] = struct{}{}
-	if peer.IsGw {
-		peerTags[models.TagID(fmt.Sprintf("%s.%s", peer.Network, models.GwTagName))] = struct{}{}
+	var nodeTags, peerTags map[models.TagID]struct{}
+	if node.Mutex != nil {
+		node.Mutex.Lock()
+		nodeTags = maps.Clone(node.Tags)
+		node.Mutex.Unlock()
+	} else {
+		nodeTags = node.Tags
 	}
 	}
-	if node.IsGw {
-		nodeTags[models.TagID(fmt.Sprintf("%s.%s", node.Network, models.GwTagName))] = struct{}{}
+	if peer.Mutex != nil {
+		peer.Mutex.Lock()
+		peerTags = maps.Clone(peer.Tags)
+		peer.Mutex.Unlock()
+	} else {
+		peerTags = peer.Tags
+	}
+	if nodeTags == nil {
+		nodeTags = make(map[models.TagID]struct{})
+	}
+	if peerTags == nil {
+		peerTags = make(map[models.TagID]struct{})
 	}
 	}
+	nodeTags[models.TagID(nodeId)] = struct{}{}
+	peerTags[models.TagID(peerId)] = struct{}{}
 	if checkDefaultPolicy {
 	if checkDefaultPolicy {
 		// check default policy if all allowed return true
 		// check default policy if all allowed return true
 		defaultPolicy, err := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
 		defaultPolicy, err := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
@@ -1143,6 +1389,9 @@ func GetDefaultPolicy(netID models.NetworkID, ruleType models.AclPolicyType) (mo
 	if ruleType == models.DevicePolicy {
 	if ruleType == models.DevicePolicy {
 		aclID = "all-nodes"
 		aclID = "all-nodes"
 	}
 	}
+	if !servercfg.IsPro && ruleType == models.UserPolicy {
+		return models.Acl{Enabled: true}, nil
+	}
 	acl, err := GetAcl(fmt.Sprintf("%s.%s", netID, aclID))
 	acl, err := GetAcl(fmt.Sprintf("%s.%s", netID, aclID))
 	if err != nil {
 	if err != nil {
 		return models.Acl{}, errors.New("default rule not found")
 		return models.Acl{}, errors.New("default rule not found")
@@ -1385,6 +1634,24 @@ func ValidateCreateAclReq(req models.Acl) error {
 	// if err != nil {
 	// if err != nil {
 	// 	return err
 	// 	return err
 	// }
 	// }
+	for _, src := range req.Src {
+		if src.ID == models.UserGroupAclID {
+			userGroup, err := GetUserGroup(models.UserGroupID(src.Value))
+			if err != nil {
+				return err
+			}
+
+			_, ok := userGroup.NetworkRoles[models.AllNetworks]
+			if ok {
+				continue
+			}
+
+			_, ok = userGroup.NetworkRoles[req.NetworkID]
+			if !ok {
+				return fmt.Errorf("user group %s does not have access to network %s", src.Value, req.NetworkID)
+			}
+		}
+	}
 	return nil
 	return nil
 }
 }
 
 
@@ -1599,3 +1866,42 @@ func CreateDefaultAclNetworkPolicies(netID models.NetworkID) {
 	}
 	}
 	CreateDefaultUserPolicies(netID)
 	CreateDefaultUserPolicies(netID)
 }
 }
+
+func getTagMapWithNodesByNetwork(netID models.NetworkID, withStaticNodes bool) (tagNodesMap map[models.TagID][]models.Node) {
+	tagNodesMap = make(map[models.TagID][]models.Node)
+	nodes, _ := GetNetworkNodes(netID.String())
+	netGwTag := models.TagID(fmt.Sprintf("%s.%s", netID.String(), models.GwTagName))
+	for _, nodeI := range nodes {
+		tagNodesMap[models.TagID(nodeI.ID.String())] = append(tagNodesMap[models.TagID(nodeI.ID.String())], nodeI)
+		if nodeI.IsGw {
+			tagNodesMap[netGwTag] = append(tagNodesMap[netGwTag], nodeI)
+		}
+	}
+	tagNodesMap["*"] = nodes
+	if !withStaticNodes {
+		return
+	}
+	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.RemoteAccessClientID != "" {
+			continue
+		}
+		tagNodesMap[models.TagID(extclient.ClientID)] = []models.Node{
+			{
+				IsStatic:   true,
+				StaticNode: extclient,
+			},
+		}
+		tagNodesMap["*"] = append(tagNodesMap["*"], extclient.ConvertToStaticNode())
+
+	}
+	return tagNodesMap
+}

+ 25 - 21
logic/auth.go

@@ -24,24 +24,18 @@ const (
 	auth_key = "netmaker_auth"
 	auth_key = "netmaker_auth"
 )
 )
 
 
-var (
-	superUser = models.User{}
+const (
+	DashboardApp       = "dashboard"
+	NetclientApp       = "netclient"
+	NetmakerDesktopApp = "netmaker-desktop"
 )
 )
 
 
-func ClearSuperUserCache() {
-	superUser = models.User{}
-}
-
+var IsOAuthConfigured = func() bool { return false }
 var ResetAuthProvider = func() {}
 var ResetAuthProvider = func() {}
 var ResetIDPSyncHook = func() {}
 var ResetIDPSyncHook = func() {}
 
 
 // HasSuperAdmin - checks if server has an superadmin/owner
 // HasSuperAdmin - checks if server has an superadmin/owner
 func HasSuperAdmin() (bool, error) {
 func HasSuperAdmin() (bool, error) {
-
-	if superUser.IsSuperAdmin {
-		return true, nil
-	}
-
 	collection, err := database.FetchRecords(database.USERS_TABLE_NAME)
 	collection, err := database.FetchRecords(database.USERS_TABLE_NAME)
 	if err != nil {
 	if err != nil {
 		if database.IsEmptyRecord(err) {
 		if database.IsEmptyRecord(err) {
@@ -56,7 +50,7 @@ func HasSuperAdmin() (bool, error) {
 		if err != nil {
 		if err != nil {
 			continue
 			continue
 		}
 		}
-		if user.PlatformRoleID == models.SuperAdminRole || user.IsSuperAdmin {
+		if user.PlatformRoleID == models.SuperAdminRole {
 			return true, nil
 			return true, nil
 		}
 		}
 	}
 	}
@@ -178,7 +172,8 @@ func CreateUser(user *models.User) error {
 		user.AuthType = models.OAuth
 		user.AuthType = models.OAuth
 	}
 	}
 	AddGlobalNetRolesToAdmins(user)
 	AddGlobalNetRolesToAdmins(user)
-	_, err = CreateUserJWT(user.UserName, user.PlatformRoleID)
+	// create user will always be called either from API or Dashboard.
+	_, err = CreateUserJWT(user.UserName, user.PlatformRoleID, DashboardApp)
 	if err != nil {
 	if err != nil {
 		logger.Log(0, "failed to generate token", err.Error())
 		logger.Log(0, "failed to generate token", err.Error())
 		return err
 		return err
@@ -207,12 +202,14 @@ func CreateSuperAdmin(u *models.User) error {
 	if hassuperadmin {
 	if hassuperadmin {
 		return errors.New("superadmin user already exists")
 		return errors.New("superadmin user already exists")
 	}
 	}
+	u.IsSuperAdmin = true
+	u.IsAdmin = true
 	u.PlatformRoleID = models.SuperAdminRole
 	u.PlatformRoleID = models.SuperAdminRole
 	return CreateUser(u)
 	return CreateUser(u)
 }
 }
 
 
 // VerifyAuthRequest - verifies an auth request
 // VerifyAuthRequest - verifies an auth request
-func VerifyAuthRequest(authRequest models.UserAuthParams) (string, error) {
+func VerifyAuthRequest(authRequest models.UserAuthParams, appName string) (string, error) {
 	var result models.User
 	var result models.User
 	if authRequest.UserName == "" {
 	if authRequest.UserName == "" {
 		return "", errors.New("username can't be empty")
 		return "", errors.New("username can't be empty")
@@ -245,7 +242,7 @@ func VerifyAuthRequest(authRequest models.UserAuthParams) (string, error) {
 		return tokenString, nil
 		return tokenString, nil
 	} else {
 	} else {
 		// Create a new JWT for the node
 		// Create a new JWT for the node
-		tokenString, err := CreateUserJWT(authRequest.UserName, result.PlatformRoleID)
+		tokenString, err := CreateUserJWT(authRequest.UserName, result.PlatformRoleID, appName)
 		if err != nil {
 		if err != nil {
 			slog.Error("error creating jwt", "error", err)
 			slog.Error("error creating jwt", "error", err)
 			return "", err
 			return "", err
@@ -274,9 +271,7 @@ func UpsertUser(user models.User) error {
 		slog.Error("error inserting user", "user", user.UserName, "error", err.Error())
 		slog.Error("error inserting user", "user", user.UserName, "error", err.Error())
 		return err
 		return err
 	}
 	}
-	if user.IsSuperAdmin {
-		superUser = user
-	}
+
 	return nil
 	return nil
 }
 }
 
 
@@ -314,9 +309,17 @@ func UpdateUser(userchange, user *models.User) (*models.User, error) {
 
 
 		user.Password = userchange.Password
 		user.Password = userchange.Password
 	}
 	}
-	if err := IsGroupsValid(userchange.UserGroups); err != nil {
-		return userchange, errors.New("invalid groups: " + err.Error())
+
+	validUserGroups := make(map[models.UserGroupID]struct{})
+	for userGroupID := range userchange.UserGroups {
+		_, err := GetUserGroup(userGroupID)
+		if err == nil {
+			validUserGroups[userGroupID] = struct{}{}
+		}
 	}
 	}
+
+	userchange.UserGroups = validUserGroups
+
 	if err := IsNetworkRolesValid(userchange.NetworkRoles); err != nil {
 	if err := IsNetworkRolesValid(userchange.NetworkRoles); err != nil {
 		return userchange, errors.New("invalid network roles: " + err.Error())
 		return userchange, errors.New("invalid network roles: " + err.Error())
 	}
 	}
@@ -483,8 +486,9 @@ func GetState(state string) (*models.SsoState, error) {
 }
 }
 
 
 // SetState - sets a state with new expiration
 // SetState - sets a state with new expiration
-func SetState(state string) error {
+func SetState(appName, state string) error {
 	s := models.SsoState{
 	s := models.SsoState{
+		AppName:    appName,
 		Value:      state,
 		Value:      state,
 		Expiration: time.Now().Add(models.DefaultExpDuration),
 		Expiration: time.Now().Add(models.DefaultExpDuration),
 	}
 	}

+ 38 - 0
logic/dns.go

@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
+	"net"
 	"os"
 	"os"
 	"regexp"
 	"regexp"
 	"sort"
 	"sort"
@@ -118,6 +119,31 @@ func GetDNS(network string) ([]models.DNSEntry, error) {
 	return dns, nil
 	return dns, nil
 }
 }
 
 
+func EgressDNs(network string) (entries []models.DNSEntry) {
+	egs, _ := (&schema.Egress{
+		Network: network,
+	}).ListByNetwork(db.WithContext(context.TODO()))
+	for _, egI := range egs {
+		if egI.Domain != "" && len(egI.DomainAns) > 0 {
+			entry := models.DNSEntry{
+				Name: egI.Domain,
+			}
+			for _, domainAns := range egI.DomainAns {
+				ip, _, err := net.ParseCIDR(domainAns)
+				if err == nil {
+					if ip.To4() != nil {
+						entry.Address = ip.String()
+					} else {
+						entry.Address6 = ip.String()
+					}
+				}
+			}
+			entries = append(entries, entry)
+		}
+	}
+	return
+}
+
 // GetExtclientDNS - gets all extclients dns entries
 // GetExtclientDNS - gets all extclients dns entries
 func GetExtclientDNS() []models.DNSEntry {
 func GetExtclientDNS() []models.DNSEntry {
 	extclients, err := GetAllExtClients()
 	extclients, err := GetAllExtClients()
@@ -418,6 +444,18 @@ func validateNameserverReq(ns schema.Nameserver) error {
 			}
 			}
 		}
 		}
 	}
 	}
+	// check if valid broadcast peers are added
+	if len(ns.Nodes) > 0 {
+		for nodeID := range ns.Nodes {
+			node, err := GetNodeByID(nodeID)
+			if err != nil {
+				return errors.New("invalid node")
+			}
+			if node.Network != ns.NetworkID {
+				return errors.New("invalid network node")
+			}
+		}
+	}
 
 
 	return nil
 	return nil
 }
 }

+ 119 - 12
logic/egress.go

@@ -36,6 +36,35 @@ func ValidateEgressReq(e *schema.Egress) error {
 	return nil
 	return nil
 }
 }
 
 
+func DoesUserHaveAccessToEgress(user *models.User, e *schema.Egress, acls []models.Acl) bool {
+
+	if !e.Status {
+		return false
+	}
+	for _, acl := range acls {
+		if !acl.Enabled {
+			continue
+		}
+		dstTags := ConvAclTagToValueMap(acl.Dst)
+		_, all := dstTags["*"]
+
+		if _, ok := dstTags[e.ID]; ok || all {
+			// get all src tags
+			for _, srcAcl := range acl.Src {
+				if srcAcl.ID == models.UserAclID && srcAcl.Value == user.UserName {
+					return true
+				} else if srcAcl.ID == models.UserGroupAclID {
+					// fetch all users in the group
+					if _, ok := user.UserGroups[models.UserGroupID(srcAcl.Value)]; ok {
+						return true
+					}
+				}
+			}
+		}
+	}
+	return false
+}
+
 func DoesNodeHaveAccessToEgress(node *models.Node, e *schema.Egress, acls []models.Acl) bool {
 func DoesNodeHaveAccessToEgress(node *models.Node, e *schema.Egress, acls []models.Acl) bool {
 	nodeTags := maps.Clone(node.Tags)
 	nodeTags := maps.Clone(node.Tags)
 	nodeTags[models.TagID(node.ID.String())] = struct{}{}
 	nodeTags[models.TagID(node.ID.String())] = struct{}{}
@@ -107,12 +136,31 @@ func AddEgressInfoToPeerByAccess(node, targetNode *models.Node, eli []schema.Egr
 				m64 = 256
 				m64 = 256
 			}
 			}
 			m := uint32(m64)
 			m := uint32(m64)
-			req.Ranges = append(req.Ranges, e.Range)
-			req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
-				Network:     e.Range,
-				Nat:         e.Nat,
-				RouteMetric: m,
-			})
+			if e.Range != "" {
+				req.Ranges = append(req.Ranges, e.Range)
+			} else {
+				req.Ranges = append(req.Ranges, e.DomainAns...)
+			}
+
+			if e.Range != "" {
+				req.Ranges = append(req.Ranges, e.Range)
+				req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
+					Network:     e.Range,
+					Nat:         e.Nat,
+					RouteMetric: m,
+				})
+			}
+			if e.Domain != "" && len(e.DomainAns) > 0 {
+				req.Ranges = append(req.Ranges, e.DomainAns...)
+				for _, domainAnsI := range e.DomainAns {
+					req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
+						Network:     domainAnsI,
+						Nat:         e.Nat,
+						RouteMetric: m,
+					})
+				}
+
+			}
 		}
 		}
 	}
 	}
 	if targetNode.Mutex != nil {
 	if targetNode.Mutex != nil {
@@ -132,6 +180,27 @@ func AddEgressInfoToPeerByAccess(node, targetNode *models.Node, eli []schema.Egr
 	}
 	}
 }
 }
 
 
+func GetEgressDomainsByAccess(user *models.User, network models.NetworkID) (domains []string) {
+	acls, _ := ListAclsByNetwork(network)
+	eli, _ := (&schema.Egress{Network: network.String()}).ListByNetwork(db.WithContext(context.TODO()))
+	defaultDevicePolicy, _ := GetDefaultPolicy(network, models.DevicePolicy)
+	isDefaultPolicyActive := defaultDevicePolicy.Enabled
+	for _, e := range eli {
+		if !e.Status || e.Network != network.String() {
+			continue
+		}
+		if !isDefaultPolicyActive {
+			if !DoesUserHaveAccessToEgress(user, &e, acls) {
+				continue
+			}
+		}
+		if e.Domain != "" && len(e.DomainAns) > 0 {
+			domains = append(domains, e.Domain)
+		}
+	}
+	return
+}
+
 func GetNodeEgressInfo(targetNode *models.Node, eli []schema.Egress, acls []models.Acl) {
 func GetNodeEgressInfo(targetNode *models.Node, eli []schema.Egress, acls []models.Acl) {
 
 
 	req := models.EgressGatewayRequest{
 	req := models.EgressGatewayRequest{
@@ -149,12 +218,25 @@ func GetNodeEgressInfo(targetNode *models.Node, eli []schema.Egress, acls []mode
 				m64 = 256
 				m64 = 256
 			}
 			}
 			m := uint32(m64)
 			m := uint32(m64)
-			req.Ranges = append(req.Ranges, e.Range)
-			req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
-				Network:     e.Range,
-				Nat:         e.Nat,
-				RouteMetric: m,
-			})
+			if e.Range != "" {
+				req.Ranges = append(req.Ranges, e.Range)
+				req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
+					Network:     e.Range,
+					Nat:         e.Nat,
+					RouteMetric: m,
+				})
+			}
+			if e.Domain != "" && len(e.DomainAns) > 0 {
+				req.Ranges = append(req.Ranges, e.DomainAns...)
+				for _, domainAnsI := range e.DomainAns {
+					req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
+						Network:     domainAnsI,
+						Nat:         e.Nat,
+						RouteMetric: m,
+					})
+				}
+
+			}
 
 
 		}
 		}
 	}
 	}
@@ -218,3 +300,28 @@ func GetEgressRanges(netID models.NetworkID) (map[string][]string, map[string]st
 	}
 	}
 	return nodeEgressMap, resultMap, nil
 	return nodeEgressMap, resultMap, nil
 }
 }
+
+func ListAllByRoutingNodeWithDomain(egs []schema.Egress, nodeID string) (egWithDomain []models.EgressDomain) {
+	for _, egI := range egs {
+		if !egI.Status || egI.Domain == "" {
+			continue
+		}
+		if _, ok := egI.Nodes[nodeID]; ok {
+			node, err := GetNodeByID(nodeID)
+			if err != nil {
+				continue
+			}
+			host, err := GetHost(node.HostID.String())
+			if err != nil {
+				continue
+			}
+			egWithDomain = append(egWithDomain, models.EgressDomain{
+				ID:     egI.ID,
+				Domain: egI.Domain,
+				Node:   node,
+				Host:   *host,
+			})
+		}
+	}
+	return
+}

+ 7 - 38
logic/extpeers.go

@@ -123,7 +123,7 @@ func UniqueIPNetStrList(ipnets []string) []string {
 }
 }
 
 
 // DeleteExtClient - deletes an existing ext client
 // DeleteExtClient - deletes an existing ext client
-func DeleteExtClient(network string, clientid string) error {
+func DeleteExtClient(network string, clientid string, isUpdate bool) error {
 	key, err := GetRecordKey(clientid, network)
 	key, err := GetRecordKey(clientid, network)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -146,7 +146,7 @@ func DeleteExtClient(network string, clientid string) error {
 		}
 		}
 		deleteExtClientFromCache(key)
 		deleteExtClientFromCache(key)
 	}
 	}
-	if extClient.RemoteAccessClientID != "" {
+	if !isUpdate && extClient.RemoteAccessClientID != "" {
 		LogEvent(&models.Event{
 		LogEvent(&models.Event{
 			Action: models.Disconnect,
 			Action: models.Disconnect,
 			Source: models.Subject{
 			Source: models.Subject{
@@ -173,7 +173,7 @@ func DeleteExtClient(network string, clientid string) error {
 func DeleteExtClientAndCleanup(extClient models.ExtClient) error {
 func DeleteExtClientAndCleanup(extClient models.ExtClient) error {
 
 
 	//delete extClient record
 	//delete extClient record
-	err := DeleteExtClient(extClient.Network, extClient.ClientID)
+	err := DeleteExtClient(extClient.Network, extClient.ClientID, false)
 	if err != nil {
 	if err != nil {
 		slog.Error("DeleteExtClientAndCleanup-remove extClient record: ", "Error", err.Error())
 		slog.Error("DeleteExtClientAndCleanup-remove extClient record: ", "Error", err.Error())
 		return err
 		return err
@@ -433,6 +433,9 @@ func UpdateExtClient(old *models.ExtClient, update *models.CustomExtClient) mode
 	if update.Country != "" && update.Country != old.Country {
 	if update.Country != "" && update.Country != old.Country {
 		new.Country = update.Country
 		new.Country = update.Country
 	}
 	}
+	if update.DeviceID != "" && old.DeviceID == "" {
+		new.DeviceID = update.DeviceID
+	}
 	return new
 	return new
 }
 }
 
 
@@ -508,7 +511,7 @@ func ToggleExtClientConnectivity(client *models.ExtClient, enable bool) (models.
 
 
 	// update in DB
 	// update in DB
 	newClient := UpdateExtClient(client, &update)
 	newClient := UpdateExtClient(client, &update)
-	if err := DeleteExtClient(client.Network, client.ClientID); err != nil {
+	if err := DeleteExtClient(client.Network, client.ClientID, true); err != nil {
 		slog.Error("failed to delete ext client during update", "id", client.ClientID, "network", client.Network, "error", err)
 		slog.Error("failed to delete ext client during update", "id", client.ClientID, "network", client.Network, "error", err)
 		return newClient, err
 		return newClient, err
 	}
 	}
@@ -702,22 +705,6 @@ func GetExtclientAllowedIPs(client models.ExtClient) (allowedIPs []string) {
 	return
 	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 := extI.ConvertToStaticNode()
-				staticNode = append(staticNode, n)
-			}
-		}
-	}
-	return
-}
-
 func GetStaticNodesByNetwork(network models.NetworkID, onlyWg bool) (staticNode []models.Node) {
 func GetStaticNodesByNetwork(network models.NetworkID, onlyWg bool) (staticNode []models.Node) {
 	extClients, err := GetAllExtClients()
 	extClients, err := GetAllExtClients()
 	if err != nil {
 	if err != nil {
@@ -735,21 +722,3 @@ func GetStaticNodesByNetwork(network models.NetworkID, onlyWg bool) (staticNode
 
 
 	return
 	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
-}

+ 2 - 2
logic/gateway.go

@@ -247,7 +247,7 @@ func GetIngressGwUsers(node models.Node) (models.IngressGwUsers, error) {
 		return gwUsers, err
 		return gwUsers, err
 	}
 	}
 	for _, user := range users {
 	for _, user := range users {
-		if !user.IsAdmin && !user.IsSuperAdmin {
+		if user.PlatformRoleID != models.SuperAdminRole && user.PlatformRoleID != models.AdminRole {
 			gwUsers.Users = append(gwUsers.Users, user)
 			gwUsers.Users = append(gwUsers.Users, user)
 		}
 		}
 	}
 	}
@@ -298,7 +298,7 @@ func DeleteGatewayExtClients(gatewayID string, networkName string) error {
 	}
 	}
 	for _, extClient := range currentExtClients {
 	for _, extClient := range currentExtClients {
 		if extClient.IngressGatewayID == gatewayID {
 		if extClient.IngressGatewayID == gatewayID {
-			if err = DeleteExtClient(networkName, extClient.ClientID); err != nil {
+			if err = DeleteExtClient(networkName, extClient.ClientID, false); err != nil {
 				logger.Log(1, "failed to remove ext client", extClient.ClientID)
 				logger.Log(1, "failed to remove ext client", extClient.ClientID)
 				continue
 				continue
 			}
 			}

+ 25 - 3
logic/hosts.go

@@ -17,6 +17,7 @@ import (
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
+	"github.com/gravitl/netmaker/utils"
 )
 )
 
 
 var (
 var (
@@ -125,7 +126,7 @@ func GetAllHostsWithStatus(status models.NodeStatus) ([]models.Host, error) {
 
 
 		nodes := GetHostNodes(&host)
 		nodes := GetHostNodes(&host)
 		for _, node := range nodes {
 		for _, node := range nodes {
-			GetNodeCheckInStatus(&node, false)
+			getNodeCheckInStatus(&node, false)
 			if node.Status == status {
 			if node.Status == status {
 				validHosts = append(validHosts, host)
 				validHosts = append(validHosts, host)
 				break
 				break
@@ -174,6 +175,18 @@ func GetHostsMap() (map[string]models.Host, error) {
 	return currHostMap, nil
 	return currHostMap, nil
 }
 }
 
 
+func DoesHostExistinTheNetworkAlready(h *models.Host, network models.NetworkID) bool {
+	if len(h.Nodes) > 0 {
+		for _, nodeID := range h.Nodes {
+			node, err := GetNodeByID(nodeID)
+			if err == nil && node.Network == network.String() {
+				return true
+			}
+		}
+	}
+	return false
+}
+
 // GetHost - gets a host from db given id
 // GetHost - gets a host from db given id
 func GetHost(hostid string) (*models.Host, error) {
 func GetHost(hostid string) (*models.Host, error) {
 	if servercfg.CacheEnabled() {
 	if servercfg.CacheEnabled() {
@@ -297,16 +310,25 @@ func UpdateHostFromClient(newHost, currHost *models.Host) (sendPeerUpdate bool)
 		sendPeerUpdate = true
 		sendPeerUpdate = true
 	}
 	}
 	isEndpointChanged := false
 	isEndpointChanged := false
-	if currHost.EndpointIP.String() != newHost.EndpointIP.String() {
+	if !currHost.EndpointIP.Equal(newHost.EndpointIP) {
 		currHost.EndpointIP = newHost.EndpointIP
 		currHost.EndpointIP = newHost.EndpointIP
 		sendPeerUpdate = true
 		sendPeerUpdate = true
 		isEndpointChanged = true
 		isEndpointChanged = true
 	}
 	}
-	if currHost.EndpointIPv6.String() != newHost.EndpointIPv6.String() {
+	if !currHost.EndpointIPv6.Equal(newHost.EndpointIPv6) {
 		currHost.EndpointIPv6 = newHost.EndpointIPv6
 		currHost.EndpointIPv6 = newHost.EndpointIPv6
 		sendPeerUpdate = true
 		sendPeerUpdate = true
 		isEndpointChanged = true
 		isEndpointChanged = true
 	}
 	}
+	for i := range newHost.Interfaces {
+		newHost.Interfaces[i].AddressString = newHost.Interfaces[i].Address.String()
+	}
+	utils.SortIfacesByName(currHost.Interfaces)
+	utils.SortIfacesByName(newHost.Interfaces)
+	if !utils.CompareIfaces(currHost.Interfaces, newHost.Interfaces) {
+		currHost.Interfaces = newHost.Interfaces
+		sendPeerUpdate = true
+	}
 
 
 	if isEndpointChanged {
 	if isEndpointChanged {
 		for _, nodeID := range currHost.Nodes {
 		for _, nodeID := range currHost.Nodes {

+ 7 - 3
logic/jwts.go

@@ -83,9 +83,13 @@ func CreateUserAccessJwtToken(username string, role models.UserRoleID, d time.Ti
 }
 }
 
 
 // CreateUserJWT - creates a user jwt token
 // CreateUserJWT - creates a user jwt token
-func CreateUserJWT(username string, role models.UserRoleID) (response string, err error) {
-	settings := GetServerSettings()
-	expirationTime := time.Now().Add(time.Duration(settings.JwtValidityDuration) * time.Minute)
+func CreateUserJWT(username string, role models.UserRoleID, appName string) (response string, err error) {
+	duration := GetJwtValidityDuration()
+	if appName == NetclientApp || appName == NetmakerDesktopApp {
+		duration = GetJwtValidityDurationForClients()
+	}
+
+	expirationTime := time.Now().Add(duration)
 	claims := &models.UserClaims{
 	claims := &models.UserClaims{
 		UserName:  username,
 		UserName:  username,
 		Role:      role,
 		Role:      role,

+ 30 - 19
logic/networks.go

@@ -629,30 +629,41 @@ func IsNetworkNameUnique(network *models.Network) (bool, error) {
 	return isunique, nil
 	return isunique, nil
 }
 }
 
 
+func UpsertNetwork(network models.Network) error {
+	netData, err := json.Marshal(network)
+	if err != nil {
+		return err
+	}
+	err = database.Insert(network.NetID, string(netData), database.NETWORKS_TABLE_NAME)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
 // UpdateNetwork - updates a network with another network's fields
 // UpdateNetwork - updates a network with another network's fields
-func UpdateNetwork(currentNetwork *models.Network, newNetwork *models.Network) (bool, bool, bool, error) {
+func UpdateNetwork(currentNetwork *models.Network, newNetwork *models.Network) error {
 	if err := ValidateNetwork(newNetwork, true); err != nil {
 	if err := ValidateNetwork(newNetwork, true); err != nil {
-		return false, false, false, err
+		return err
 	}
 	}
-	if newNetwork.NetID == currentNetwork.NetID {
-		hasrangeupdate4 := newNetwork.AddressRange != currentNetwork.AddressRange
-		hasrangeupdate6 := newNetwork.AddressRange6 != currentNetwork.AddressRange6
-		hasholepunchupdate := newNetwork.DefaultUDPHolePunch != currentNetwork.DefaultUDPHolePunch
-		data, err := json.Marshal(newNetwork)
-		if err != nil {
-			return false, false, false, err
-		}
-		newNetwork.SetNetworkLastModified()
-		err = database.Insert(newNetwork.NetID, string(data), database.NETWORKS_TABLE_NAME)
-		if err == nil {
-			if servercfg.CacheEnabled() {
-				storeNetworkInCache(newNetwork.NetID, *newNetwork)
-			}
+	if newNetwork.NetID != currentNetwork.NetID {
+		return errors.New("failed to update network " + newNetwork.NetID + ", cannot change netid.")
+	}
+	currentNetwork.AutoJoin = newNetwork.AutoJoin
+	currentNetwork.DefaultACL = newNetwork.DefaultACL
+	currentNetwork.NameServers = newNetwork.NameServers
+	data, err := json.Marshal(currentNetwork)
+	if err != nil {
+		return err
+	}
+	newNetwork.SetNetworkLastModified()
+	err = database.Insert(currentNetwork.NetID, string(data), database.NETWORKS_TABLE_NAME)
+	if err == nil {
+		if servercfg.CacheEnabled() {
+			storeNetworkInCache(newNetwork.NetID, *currentNetwork)
 		}
 		}
-		return hasrangeupdate4, hasrangeupdate6, hasholepunchupdate, err
 	}
 	}
-	// copy values
-	return false, false, false, errors.New("failed to update network " + newNetwork.NetID + ", cannot change netid.")
+	return err
 }
 }
 
 
 // GetNetwork - gets a network from database
 // GetNetwork - gets a network from database

+ 1 - 1
logic/nodes.go

@@ -471,7 +471,7 @@ func AddStatusToNodes(nodes []models.Node, statusCall bool) (nodesWithStatus []m
 		if statusCall {
 		if statusCall {
 			GetNodeStatus(&node, aclDefaultPolicyStatusMap[node.Network])
 			GetNodeStatus(&node, aclDefaultPolicyStatusMap[node.Network])
 		} else {
 		} else {
-			GetNodeCheckInStatus(&node, true)
+			getNodeCheckInStatus(&node, true)
 		}
 		}
 
 
 		nodesWithStatus = append(nodesWithStatus, node)
 		nodesWithStatus = append(nodesWithStatus, node)

+ 6 - 2
logic/peers.go

@@ -76,7 +76,7 @@ func GetHostPeerInfo(host *models.Host) (models.HostPeerInfo, error) {
 
 
 			peerHost, err := GetHost(peer.HostID.String())
 			peerHost, err := GetHost(peer.HostID.String())
 			if err != nil {
 			if err != nil {
-				logger.Log(1, "no peer host", peer.HostID.String(), err.Error())
+				logger.Log(4, "no peer host", peer.HostID.String(), err.Error())
 				continue
 				continue
 			}
 			}
 
 
@@ -182,6 +182,10 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 		acls, _ := ListAclsByNetwork(models.NetworkID(node.Network))
 		acls, _ := ListAclsByNetwork(models.NetworkID(node.Network))
 		eli, _ := (&schema.Egress{Network: node.Network}).ListByNetwork(db.WithContext(context.TODO()))
 		eli, _ := (&schema.Egress{Network: node.Network}).ListByNetwork(db.WithContext(context.TODO()))
 		GetNodeEgressInfo(&node, eli, acls)
 		GetNodeEgressInfo(&node, eli, acls)
+		if node.EgressDetails.IsEgressGateway {
+			egsWithDomain := ListAllByRoutingNodeWithDomain(eli, node.ID.String())
+			hostPeerUpdate.EgressWithDomains = append(hostPeerUpdate.EgressWithDomains, egsWithDomain...)
+		}
 		hostPeerUpdate = SetDefaultGw(node, hostPeerUpdate)
 		hostPeerUpdate = SetDefaultGw(node, hostPeerUpdate)
 		if !hostPeerUpdate.IsInternetGw {
 		if !hostPeerUpdate.IsInternetGw {
 			hostPeerUpdate.IsInternetGw = IsInternetGw(node)
 			hostPeerUpdate.IsInternetGw = IsInternetGw(node)
@@ -231,7 +235,7 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 
 
 			peerHost, err := GetHost(peer.HostID.String())
 			peerHost, err := GetHost(peer.HostID.String())
 			if err != nil {
 			if err != nil {
-				logger.Log(1, "no peer host", peer.HostID.String(), err.Error())
+				logger.Log(4, "no peer host", peer.HostID.String(), err.Error())
 				continue
 				continue
 			}
 			}
 			peerConfig := wgtypes.PeerConfig{
 			peerConfig := wgtypes.PeerConfig{

+ 5 - 0
logic/server.go

@@ -1,7 +1,12 @@
 package logic
 package logic
 
 
+import "github.com/gravitl/netmaker/models"
+
 // EnterpriseCheckFuncs - can be set to run functions for EE
 // EnterpriseCheckFuncs - can be set to run functions for EE
 var EnterpriseCheckFuncs []func()
 var EnterpriseCheckFuncs []func()
+var GetFeatureFlags = func() models.FeatureFlags {
+	return models.FeatureFlags{}
+}
 
 
 // == Join, Checkin, and Leave for Server ==
 // == Join, Checkin, and Leave for Server ==
 
 

+ 68 - 18
logic/settings.go

@@ -15,11 +15,17 @@ import (
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 )
 )
 
 
-var serverSettingsDBKey = "server_cfg"
+var ServerSettingsDBKey = "server_cfg"
 var SettingsMutex = &sync.RWMutex{}
 var SettingsMutex = &sync.RWMutex{}
 
 
+var defaultUserSettings = models.UserSettings{
+	TextSize:      "16",
+	Theme:         models.Dark,
+	ReducedMotion: false,
+}
+
 func GetServerSettings() (s models.ServerSettings) {
 func GetServerSettings() (s models.ServerSettings) {
-	data, err := database.FetchRecord(database.SERVER_SETTINGS, serverSettingsDBKey)
+	data, err := database.FetchRecord(database.SERVER_SETTINGS, ServerSettingsDBKey)
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
@@ -60,31 +66,71 @@ func UpsertServerSettings(s models.ServerSettings) error {
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	err = database.Insert(serverSettingsDBKey, string(data), database.SERVER_SETTINGS)
+	err = database.Insert(ServerSettingsDBKey, string(data), database.SERVER_SETTINGS)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 	return nil
 	return nil
 }
 }
 
 
+func GetUserSettings(userID string) models.UserSettings {
+	data, err := database.FetchRecord(database.SERVER_SETTINGS, userID)
+	if err != nil {
+		return defaultUserSettings
+	}
+	var userSettings models.UserSettings
+	err = json.Unmarshal([]byte(data), &userSettings)
+	if err != nil {
+		return defaultUserSettings
+	}
+
+	return userSettings
+}
+
+func UpsertUserSettings(userID string, userSettings models.UserSettings) error {
+	if userSettings.TextSize == "" {
+		userSettings.TextSize = "16"
+	}
+
+	if userSettings.Theme == "" {
+		userSettings.Theme = models.Dark
+	}
+
+	data, err := json.Marshal(userSettings)
+	if err != nil {
+		return err
+	}
+	return database.Insert(userID, string(data), database.SERVER_SETTINGS)
+}
+
+func DeleteUserSettings(userID string) error {
+	return database.DeleteRecord(database.SERVER_SETTINGS, userID)
+}
+
 func ValidateNewSettings(req models.ServerSettings) bool {
 func ValidateNewSettings(req models.ServerSettings) bool {
 	// TODO: add checks for different fields
 	// TODO: add checks for different fields
+	if req.JwtValidityDuration > 525600 || req.JwtValidityDuration < 5 {
+		return false
+	}
 	return true
 	return true
 }
 }
 
 
 func GetServerSettingsFromEnv() (s models.ServerSettings) {
 func GetServerSettingsFromEnv() (s models.ServerSettings) {
 
 
 	s = models.ServerSettings{
 	s = models.ServerSettings{
-		NetclientAutoUpdate:        servercfg.AutoUpdateEnabled(),
-		Verbosity:                  servercfg.GetVerbosity(),
-		AuthProvider:               os.Getenv("AUTH_PROVIDER"),
-		OIDCIssuer:                 os.Getenv("OIDC_ISSUER"),
-		ClientID:                   os.Getenv("CLIENT_ID"),
-		ClientSecret:               os.Getenv("CLIENT_SECRET"),
-		AzureTenant:                servercfg.GetAzureTenant(),
-		Telemetry:                  servercfg.Telemetry(),
-		BasicAuth:                  servercfg.IsBasicAuthEnabled(),
-		JwtValidityDuration:        servercfg.GetJwtValidityDurationFromEnv() / 60,
+		NetclientAutoUpdate: servercfg.AutoUpdateEnabled(),
+		Verbosity:           servercfg.GetVerbosity(),
+		AuthProvider:        os.Getenv("AUTH_PROVIDER"),
+		OIDCIssuer:          os.Getenv("OIDC_ISSUER"),
+		ClientID:            os.Getenv("CLIENT_ID"),
+		ClientSecret:        os.Getenv("CLIENT_SECRET"),
+		AzureTenant:         servercfg.GetAzureTenant(),
+		Telemetry:           servercfg.Telemetry(),
+		BasicAuth:           servercfg.IsBasicAuthEnabled(),
+		JwtValidityDuration: servercfg.GetJwtValidityDurationFromEnv() / 60,
+		// setting client's jwt validity duration to be the same as that of
+		// dashboard.
+		JwtValidityDurationClients: servercfg.GetJwtValidityDurationFromEnv() / 60,
 		RacRestrictToSingleNetwork: servercfg.GetRacRestrictToSingleNetwork(),
 		RacRestrictToSingleNetwork: servercfg.GetRacRestrictToSingleNetwork(),
 		EndpointDetection:          servercfg.IsEndpointDetectionEnabled(),
 		EndpointDetection:          servercfg.IsEndpointDetectionEnabled(),
 		AllowedEmailDomains:        servercfg.GetAllowedEmailDomains(),
 		AllowedEmailDomains:        servercfg.GetAllowedEmailDomains(),
@@ -99,9 +145,6 @@ func GetServerSettingsFromEnv() (s models.ServerSettings) {
 		DefaultDomain:              servercfg.GetDefaultDomain(),
 		DefaultDomain:              servercfg.GetDefaultDomain(),
 		Stun:                       servercfg.IsStunEnabled(),
 		Stun:                       servercfg.IsStunEnabled(),
 		StunServers:                servercfg.GetStunServers(),
 		StunServers:                servercfg.GetStunServers(),
-		TextSize:                   "16",
-		Theme:                      models.Dark,
-		ReducedMotion:              false,
 	}
 	}
 
 
 	return
 	return
@@ -162,6 +205,7 @@ func GetServerConfig() config.ServerConfig {
 		cfg.IsPro = "yes"
 		cfg.IsPro = "yes"
 	}
 	}
 	cfg.JwtValidityDuration = time.Duration(settings.JwtValidityDuration) * time.Minute
 	cfg.JwtValidityDuration = time.Duration(settings.JwtValidityDuration) * time.Minute
+	cfg.JwtValidityDurationClients = time.Duration(settings.JwtValidityDurationClients) * time.Minute
 	cfg.RacRestrictToSingleNetwork = settings.RacRestrictToSingleNetwork
 	cfg.RacRestrictToSingleNetwork = settings.RacRestrictToSingleNetwork
 	cfg.MetricInterval = settings.MetricInterval
 	cfg.MetricInterval = settings.MetricInterval
 	cfg.ManageDNS = settings.ManageDNS
 	cfg.ManageDNS = settings.ManageDNS
@@ -224,7 +268,13 @@ func Telemetry() string {
 
 
 // GetJwtValidityDuration - returns the JWT validity duration in minutes
 // GetJwtValidityDuration - returns the JWT validity duration in minutes
 func GetJwtValidityDuration() time.Duration {
 func GetJwtValidityDuration() time.Duration {
-	return GetServerConfig().JwtValidityDuration
+	return time.Duration(GetServerSettings().JwtValidityDuration) * time.Minute
+}
+
+// GetJwtValidityDurationForClients returns the JWT validity duration in
+// minutes for clients.
+func GetJwtValidityDurationForClients() time.Duration {
+	return time.Duration(GetServerSettings().JwtValidityDurationClients) * time.Minute
 }
 }
 
 
 // GetRacRestrictToSingleNetwork - returns whether the feature to allow simultaneous network connections via RAC is enabled
 // GetRacRestrictToSingleNetwork - returns whether the feature to allow simultaneous network connections via RAC is enabled
@@ -274,7 +324,7 @@ func GetAuthProviderInfo(settings models.ServerSettings) (pi []string) {
 
 
 	if settings.AuthProvider != "" && settings.ClientID != "" && settings.ClientSecret != "" {
 	if settings.AuthProvider != "" && settings.ClientID != "" && settings.ClientSecret != "" {
 		authProvider = strings.ToLower(settings.AuthProvider)
 		authProvider = strings.ToLower(settings.AuthProvider)
-		if authProvider == "google" || authProvider == "azure-ad" || authProvider == "github" || authProvider == "oidc" || authProvider == "okta" {
+		if authProvider == "google" || authProvider == "azure-ad" || authProvider == "github" || authProvider == "okta" || authProvider == "oidc" {
 			return []string{authProvider, settings.ClientID, settings.ClientSecret}
 			return []string{authProvider, settings.ClientID, settings.ClientSecret}
 		} else {
 		} else {
 			authProvider = ""
 			authProvider = ""

+ 2 - 2
logic/status.go

@@ -6,9 +6,9 @@ import (
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 )
 )
 
 
-var GetNodeStatus = GetNodeCheckInStatus
+var GetNodeStatus = getNodeCheckInStatus
 
 
-func GetNodeCheckInStatus(node *models.Node, t bool) {
+func getNodeCheckInStatus(node *models.Node, t bool) {
 	// On CE check only last check-in time
 	// On CE check only last check-in time
 	if node.IsStatic {
 	if node.IsStatic {
 		if !node.StaticNode.Enabled {
 		if !node.StaticNode.Enabled {

+ 33 - 0
logic/user_mgmt.go

@@ -95,6 +95,7 @@ var CreateDefaultUserPolicies = func(netID models.NetworkID) {
 		InsertAcl(defaultUserAcl)
 		InsertAcl(defaultUserAcl)
 	}
 	}
 }
 }
+var ListUserGroups = func() ([]models.UserGroup, error) { return nil, nil }
 var GetUserGroupsInNetwork = func(netID models.NetworkID) (networkGrps map[models.UserGroupID]models.UserGroup) { return }
 var GetUserGroupsInNetwork = func(netID models.NetworkID) (networkGrps map[models.UserGroupID]models.UserGroup) { return }
 var GetUserGroup = func(groupId models.UserGroupID) (userGrps models.UserGroup, err error) { return }
 var GetUserGroup = func(groupId models.UserGroupID) (userGrps models.UserGroup, err error) { return }
 var AddGlobalNetRolesToAdmins = func(u *models.User) {}
 var AddGlobalNetRolesToAdmins = func(u *models.User) {}
@@ -136,6 +137,38 @@ func ListPlatformRoles() ([]models.UserRolePermissionTemplate, error) {
 	return userRoles, nil
 	return userRoles, nil
 }
 }
 
 
+func GetAllRsrcIDForRsrc(rsrc models.RsrcType) models.RsrcID {
+	switch rsrc {
+	case models.HostRsrc:
+		return models.AllHostRsrcID
+	case models.RelayRsrc:
+		return models.AllRelayRsrcID
+	case models.RemoteAccessGwRsrc:
+		return models.AllRemoteAccessGwRsrcID
+	case models.ExtClientsRsrc:
+		return models.AllExtClientsRsrcID
+	case models.InetGwRsrc:
+		return models.AllInetGwRsrcID
+	case models.EgressGwRsrc:
+		return models.AllEgressGwRsrcID
+	case models.NetworkRsrc:
+		return models.AllNetworkRsrcID
+	case models.EnrollmentKeysRsrc:
+		return models.AllEnrollmentKeysRsrcID
+	case models.UserRsrc:
+		return models.AllUserRsrcID
+	case models.DnsRsrc:
+		return models.AllDnsRsrcID
+	case models.FailOverRsrc:
+		return models.AllFailOverRsrcID
+	case models.AclRsrc:
+		return models.AllAclsRsrcID
+	case models.TagRsrc:
+		return models.AllTagsRsrcID
+	}
+	return ""
+}
+
 func userRolesInit() {
 func userRolesInit() {
 	d, _ := json.Marshal(SuperAdminPermissionTemplate)
 	d, _ := json.Marshal(SuperAdminPermissionTemplate)
 	database.Insert(SuperAdminPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
 	database.Insert(SuperAdminPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)

+ 1 - 1
logic/users.go

@@ -82,7 +82,7 @@ func GetSuperAdmin() (models.ReturnUser, error) {
 		return models.ReturnUser{}, err
 		return models.ReturnUser{}, err
 	}
 	}
 	for _, user := range users {
 	for _, user := range users {
-		if user.IsSuperAdmin || user.PlatformRoleID == models.SuperAdminRole {
+		if user.PlatformRoleID == models.SuperAdminRole {
 			return user, nil
 			return user, nil
 		}
 		}
 	}
 	}

+ 35 - 0
logic/util.go

@@ -12,6 +12,7 @@ import (
 	"net/http"
 	"net/http"
 	"os"
 	"os"
 	"reflect"
 	"reflect"
+	"regexp"
 	"strings"
 	"strings"
 	"time"
 	"time"
 	"unicode"
 	"unicode"
@@ -20,6 +21,7 @@ import (
 	"github.com/c-robinson/iplib"
 	"github.com/c-robinson/iplib"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/models"
 )
 )
 
 
 // IsBase64 - checks if a string is in base64 format
 // IsBase64 - checks if a string is in base64 format
@@ -253,3 +255,36 @@ func GetClientIP(r *http.Request) string {
 	}
 	}
 	return ip
 	return ip
 }
 }
+
+// CompareIfaceSlices compares two slices of Iface for deep equality (order-sensitive)
+func CompareIfaceSlices(a, b []models.Iface) bool {
+	if len(a) != len(b) {
+		return false
+	}
+	for i := range a {
+		if !compareIface(a[i], b[i]) {
+			return false
+		}
+	}
+	return true
+}
+func compareIface(a, b models.Iface) bool {
+	return a.Name == b.Name &&
+		a.Address.IP.Equal(b.Address.IP) &&
+		a.Address.Mask.String() == b.Address.Mask.String() &&
+		a.AddressString == b.AddressString
+}
+
+// IsFQDN checks if the given string is a valid Fully Qualified Domain Name (FQDN)
+func IsFQDN(domain string) bool {
+	// Basic check to ensure the domain is not empty and has at least one dot (.)
+	if domain == "" || !strings.Contains(domain, ".") {
+		return false
+	}
+
+	// Regular expression for validating FQDN (basic check for valid characters and structure)
+	fqdnRegex := `^(?i)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$`
+	re := regexp.MustCompile(fqdnRegex)
+
+	return re.MatchString(domain)
+}

+ 2 - 2
main.go

@@ -35,10 +35,10 @@ import (
 	"golang.org/x/exp/slog"
 	"golang.org/x/exp/slog"
 )
 )
 
 
-var version = "v1.0.0"
+var version = "v1.1.0"
 
 
 //	@title			NetMaker
 //	@title			NetMaker
-//	@version		1.0.0
+//	@version		1.1.0
 //	@description	NetMaker API Docs
 //	@description	NetMaker API Docs
 //	@tag.name	    APIUsage
 //	@tag.name	    APIUsage
 //	@tag.description.markdown
 //	@tag.description.markdown

+ 125 - 6
migrate/migrate.go

@@ -25,7 +25,7 @@ import (
 
 
 // Run - runs all migrations
 // Run - runs all migrations
 func Run() {
 func Run() {
-	settings()
+	migrateSettings()
 	updateEnrollmentKeys()
 	updateEnrollmentKeys()
 	assignSuperAdmin()
 	assignSuperAdmin()
 	createDefaultTagsAndPolicies()
 	createDefaultTagsAndPolicies()
@@ -35,10 +35,24 @@ func Run() {
 	updateHosts()
 	updateHosts()
 	updateNodes()
 	updateNodes()
 	updateAcls()
 	updateAcls()
+	updateNewAcls()
 	logic.MigrateToGws()
 	logic.MigrateToGws()
 	migrateToEgressV1()
 	migrateToEgressV1()
+	updateNetworks()
 	migrateNameservers()
 	migrateNameservers()
 	resync()
 	resync()
+	deleteOldExtclients()
+}
+
+func updateNetworks() {
+	nets, _ := logic.GetNetworks()
+	for _, netI := range nets {
+		if netI.AutoJoin == "" {
+			netI.AutoJoin = "true"
+			logic.UpsertNetwork(netI)
+		}
+	}
+
 }
 }
 
 
 func migrateNameservers() {
 func migrateNameservers() {
@@ -185,7 +199,15 @@ func assignSuperAdmin() {
 		return
 		return
 	}
 	}
 	for _, u := range users {
 	for _, u := range users {
-		if u.IsAdmin {
+		var isAdmin bool
+		if u.PlatformRoleID == models.AdminRole {
+			isAdmin = true
+		}
+		if u.PlatformRoleID == "" && u.IsAdmin {
+			isAdmin = true
+		}
+
+		if isAdmin {
 			user, err := logic.GetUser(u.UserName)
 			user, err := logic.GetUser(u.UserName)
 			if err != nil {
 			if err != nil {
 				slog.Error("error getting user", "user", u.UserName, "error", err.Error())
 				slog.Error("error getting user", "user", u.UserName, "error", err.Error())
@@ -321,6 +343,10 @@ func updateHosts() {
 			}
 			}
 			logic.UpsertHost(&host)
 			logic.UpsertHost(&host)
 		}
 		}
+		if host.IsDefault && !host.AutoUpdate {
+			host.AutoUpdate = true
+			logic.UpsertHost(&host)
+		}
 		if servercfg.IsPro && host.Location == "" {
 		if servercfg.IsPro && host.Location == "" {
 			if host.EndpointIP != nil {
 			if host.EndpointIP != nil {
 				host.Location = logic.GetHostLocInfo(host.EndpointIP.String(), os.Getenv("IP_INFO_TOKEN"))
 				host.Location = logic.GetHostLocInfo(host.EndpointIP.String(), os.Getenv("IP_INFO_TOKEN"))
@@ -513,6 +539,48 @@ func updateAcls() {
 	}
 	}
 }
 }
 
 
+func updateNewAcls() {
+	if servercfg.IsPro {
+		userGroups, _ := logic.ListUserGroups()
+		userGroupMap := make(map[models.UserGroupID]models.UserGroup)
+		for _, userGroup := range userGroups {
+			userGroupMap[userGroup.ID] = userGroup
+		}
+
+		acls := logic.ListAcls()
+		for _, acl := range acls {
+			aclSrc := make([]models.AclPolicyTag, 0)
+			for _, src := range acl.Src {
+				if src.ID == models.UserGroupAclID {
+					userGroup, ok := userGroupMap[models.UserGroupID(src.Value)]
+					if !ok {
+						// if the group doesn't exist, don't add it to the acl's src.
+						continue
+					} else {
+						_, ok := userGroup.NetworkRoles[acl.NetworkID]
+						if !ok {
+							// if the group doesn't have permissions for the acl's
+							// network, don't add it to the acl's src.
+							continue
+						}
+					}
+				}
+				aclSrc = append(aclSrc, src)
+			}
+
+			if len(aclSrc) == 0 {
+				// if there are no acl sources, delete the acl.
+				_ = logic.DeleteAcl(acl)
+			} else if len(aclSrc) != len(acl.Src) {
+				// if some user groups were removed from the acl source,
+				// update the acl.
+				acl.Src = aclSrc
+				_ = logic.UpsertAcl(acl)
+			}
+		}
+	}
+}
+
 func MigrateEmqx() {
 func MigrateEmqx() {
 
 
 	err := mq.SendPullSYN()
 	err := mq.SendPullSYN()
@@ -548,11 +616,18 @@ func syncUsers() {
 			user := user
 			user := user
 			if user.PlatformRoleID == models.AdminRole && !user.IsAdmin {
 			if user.PlatformRoleID == models.AdminRole && !user.IsAdmin {
 				user.IsAdmin = true
 				user.IsAdmin = true
+				user.IsSuperAdmin = false
 				logic.UpsertUser(user)
 				logic.UpsertUser(user)
 			}
 			}
 			if user.PlatformRoleID == models.SuperAdminRole && !user.IsSuperAdmin {
 			if user.PlatformRoleID == models.SuperAdminRole && !user.IsSuperAdmin {
 				user.IsSuperAdmin = true
 				user.IsSuperAdmin = true
-
+				user.IsAdmin = true
+				logic.UpsertUser(user)
+			}
+			if user.PlatformRoleID == models.PlatformUser || user.PlatformRoleID == models.ServiceUser {
+				user.IsSuperAdmin = false
+				user.IsAdmin = false
+				logic.UpsertUser(user)
 			}
 			}
 			if user.PlatformRoleID.String() != "" {
 			if user.PlatformRoleID.String() != "" {
 				logic.MigrateUserRoleAndGroups(user)
 				logic.MigrateUserRoleAndGroups(user)
@@ -570,9 +645,12 @@ func syncUsers() {
 			if len(user.UserGroups) == 0 {
 			if len(user.UserGroups) == 0 {
 				user.UserGroups = make(map[models.UserGroupID]struct{})
 				user.UserGroups = make(map[models.UserGroupID]struct{})
 			}
 			}
+
+			// We reach here only if the platform role id has not been set.
+			//
+			// Thus, we use the boolean fields to assign the role.
 			if user.IsSuperAdmin {
 			if user.IsSuperAdmin {
 				user.PlatformRoleID = models.SuperAdminRole
 				user.PlatformRoleID = models.SuperAdminRole
-
 			} else if user.IsAdmin {
 			} else if user.IsAdmin {
 				user.PlatformRoleID = models.AdminRole
 				user.PlatformRoleID = models.AdminRole
 			} else {
 			} else {
@@ -598,6 +676,16 @@ func createDefaultTagsAndPolicies() {
 		logic.DeleteAcl(models.Acl{ID: fmt.Sprintf("%s.%s", network.NetID, "all-remote-access-gws")})
 		logic.DeleteAcl(models.Acl{ID: fmt.Sprintf("%s.%s", network.NetID, "all-remote-access-gws")})
 	}
 	}
 	logic.MigrateAclPolicies()
 	logic.MigrateAclPolicies()
+	if !servercfg.IsPro {
+		nodes, _ := logic.GetAllNodes()
+		for _, node := range nodes {
+			if node.IsGw {
+				node.Tags = make(map[models.TagID]struct{})
+				node.Tags[models.TagID(fmt.Sprintf("%s.%s", node.Network, models.GwTagName))] = struct{}{}
+				logic.UpsertNode(&node)
+			}
+		}
+	}
 }
 }
 
 
 func migrateToEgressV1() {
 func migrateToEgressV1() {
@@ -707,8 +795,8 @@ func migrateToEgressV1() {
 	}
 	}
 }
 }
 
 
-func settings() {
-	_, err := database.FetchRecords(database.SERVER_SETTINGS)
+func migrateSettings() {
+	_, err := database.FetchRecord(database.SERVER_SETTINGS, logic.ServerSettingsDBKey)
 	if database.IsEmptyRecord(err) {
 	if database.IsEmptyRecord(err) {
 		logic.UpsertServerSettings(logic.GetServerSettingsFromEnv())
 		logic.UpsertServerSettings(logic.GetServerSettingsFromEnv())
 	}
 	}
@@ -719,5 +807,36 @@ func settings() {
 	if settings.DefaultDomain == "" {
 	if settings.DefaultDomain == "" {
 		settings.DefaultDomain = servercfg.GetDefaultDomain()
 		settings.DefaultDomain = servercfg.GetDefaultDomain()
 	}
 	}
+	if settings.JwtValidityDurationClients == 0 {
+		settings.JwtValidityDurationClients = servercfg.GetJwtValidityDurationFromEnv() / 60
+	}
 	logic.UpsertServerSettings(settings)
 	logic.UpsertServerSettings(settings)
 }
 }
+
+func deleteOldExtclients() {
+	extclients, _ := logic.GetAllExtClients()
+	userExtclientMap := make(map[string][]models.ExtClient)
+	for _, extclient := range extclients {
+		if extclient.RemoteAccessClientID == "" {
+			continue
+		}
+
+		if extclient.Enabled {
+			continue
+		}
+
+		if _, ok := userExtclientMap[extclient.OwnerID]; !ok {
+			userExtclientMap[extclient.OwnerID] = make([]models.ExtClient, 0)
+		}
+
+		userExtclientMap[extclient.OwnerID] = append(userExtclientMap[extclient.OwnerID], extclient)
+	}
+
+	for _, userExtclients := range userExtclientMap {
+		if len(userExtclients) > 1 {
+			for _, extclient := range userExtclients[1:] {
+				_ = logic.DeleteExtClient(extclient.Network, extclient.Network, false)
+			}
+		}
+	}
+}

+ 1 - 0
models/egress.go

@@ -8,6 +8,7 @@ type EgressReq struct {
 	Nodes       map[string]int `json:"nodes"`
 	Nodes       map[string]int `json:"nodes"`
 	Tags        []string       `json:"tags"`
 	Tags        []string       `json:"tags"`
 	Range       string         `json:"range"`
 	Range       string         `json:"range"`
+	Domain      string         `json:"domain"`
 	Nat         bool           `json:"nat"`
 	Nat         bool           `json:"nat"`
 	Status      bool           `json:"status"`
 	Status      bool           `json:"status"`
 	IsInetGw    bool           `json:"is_internet_gateway"`
 	IsInetGw    bool           `json:"is_internet_gateway"`

+ 0 - 2
models/events.go

@@ -29,8 +29,6 @@ const (
 	UpdateClientSettings                 Action = "UPDATE_CLIENT_SETTINGS"
 	UpdateClientSettings                 Action = "UPDATE_CLIENT_SETTINGS"
 	UpdateAuthenticationSecuritySettings Action = "UPDATE_AUTHENTICATION_SECURITY_SETTINGS"
 	UpdateAuthenticationSecuritySettings Action = "UPDATE_AUTHENTICATION_SECURITY_SETTINGS"
 	UpdateMonitoringAndDebuggingSettings Action = "UPDATE_MONITORING_AND_DEBUGGING_SETTINGS"
 	UpdateMonitoringAndDebuggingSettings Action = "UPDATE_MONITORING_AND_DEBUGGING_SETTINGS"
-	UpdateDisplaySettings                Action = "UPDATE_DISPLAY_SETTINGS"
-	UpdateAccessibilitySettings          Action = "UPDATE_ACCESSIBILITY_SETTINGS"
 	UpdateSMTPSettings                   Action = "UPDATE_EMAIL_SETTINGS"
 	UpdateSMTPSettings                   Action = "UPDATE_EMAIL_SETTINGS"
 	UpdateIDPSettings                    Action = "UPDATE_IDP_SETTINGS"
 	UpdateIDPSettings                    Action = "UPDATE_IDP_SETTINGS"
 )
 )

+ 2 - 0
models/extclient.go

@@ -24,6 +24,7 @@ type ExtClient struct {
 	PostDown               string              `json:"postdown" bson:"postdown"`
 	PostDown               string              `json:"postdown" bson:"postdown"`
 	Tags                   map[TagID]struct{}  `json:"tags"`
 	Tags                   map[TagID]struct{}  `json:"tags"`
 	Os                     string              `json:"os"`
 	Os                     string              `json:"os"`
+	DeviceID               string              `json:"device_id"`
 	DeviceName             string              `json:"device_name"`
 	DeviceName             string              `json:"device_name"`
 	PublicEndpoint         string              `json:"public_endpoint"`
 	PublicEndpoint         string              `json:"public_endpoint"`
 	Country                string              `json:"country"`
 	Country                string              `json:"country"`
@@ -44,6 +45,7 @@ type CustomExtClient struct {
 	PostDown                   string              `json:"postdown" bson:"postdown" validate:"max=1024"`
 	PostDown                   string              `json:"postdown" bson:"postdown" validate:"max=1024"`
 	Tags                       map[TagID]struct{}  `json:"tags"`
 	Tags                       map[TagID]struct{}  `json:"tags"`
 	Os                         string              `json:"os"`
 	Os                         string              `json:"os"`
+	DeviceID                   string              `json:"device_id"`
 	DeviceName                 string              `json:"device_name"`
 	DeviceName                 string              `json:"device_name"`
 	IsAlreadyConnectedToInetGw bool                `json:"is_already_connected_to_inet_gw"`
 	IsAlreadyConnectedToInetGw bool                `json:"is_already_connected_to_inet_gw"`
 	PublicEndpoint             string              `json:"public_endpoint"`
 	PublicEndpoint             string              `json:"public_endpoint"`

+ 8 - 5
models/host.go

@@ -124,6 +124,8 @@ const (
 	SignalPull HostMqAction = "SIGNAL_PULL"
 	SignalPull HostMqAction = "SIGNAL_PULL"
 	// UpdateMetrics - updates metrics data
 	// UpdateMetrics - updates metrics data
 	UpdateMetrics HostMqAction = "UPDATE_METRICS"
 	UpdateMetrics HostMqAction = "UPDATE_METRICS"
+	// EgressUpdate - const for egress update action
+	EgressUpdate HostMqAction = "EGRESS_UPDATE"
 )
 )
 
 
 // SignalAction - turn peer signal action
 // SignalAction - turn peer signal action
@@ -138,11 +140,12 @@ const (
 
 
 // HostUpdate - struct for host update
 // HostUpdate - struct for host update
 type HostUpdate struct {
 type HostUpdate struct {
-	Action     HostMqAction
-	Host       Host
-	Node       Node
-	Signal     Signal
-	NewMetrics Metrics
+	Action       HostMqAction
+	Host         Host
+	Node         Node
+	Signal       Signal
+	EgressDomain EgressDomain
+	NewMetrics   Metrics
 }
 }
 
 
 // HostTurnRegister - struct for host turn registration
 // HostTurnRegister - struct for host turn registration

+ 24 - 17
models/mqtt.go

@@ -12,27 +12,34 @@ type HostPeerInfo struct {
 
 
 // HostPeerUpdate - struct for host peer updates
 // HostPeerUpdate - struct for host peer updates
 type HostPeerUpdate struct {
 type HostPeerUpdate struct {
-	Host            Host                  `json:"host"`
-	ChangeDefaultGw bool                  `json:"change_default_gw"`
-	DefaultGwIp     net.IP                `json:"default_gw_ip"`
-	IsInternetGw    bool                  `json:"is_inet_gw"`
-	NodeAddrs       []net.IPNet           `json:"nodes_addrs"`
-	Server          string                `json:"server"`
-	ServerVersion   string                `json:"serverversion"`
-	ServerAddrs     []ServerAddr          `json:"serveraddrs"`
-	NodePeers       []wgtypes.PeerConfig  `json:"node_peers"`
-	Peers           []wgtypes.PeerConfig  `json:"host_peers"`
-	PeerIDs         PeerMap               `json:"peerids"`
-	HostNetworkInfo HostInfoMap           `json:"host_network_info,omitempty"`
-	EgressRoutes    []EgressNetworkRoutes `json:"egress_network_routes"`
-	FwUpdate        FwUpdate              `json:"fw_update"`
-	ReplacePeers    bool                  `json:"replace_peers"`
-	NameServers     []string              `json:"name_servers"`
-	DnsNameservers  []Nameserver          `json:"dns_nameservers"`
+	Host              Host                  `json:"host"`
+	ChangeDefaultGw   bool                  `json:"change_default_gw"`
+	DefaultGwIp       net.IP                `json:"default_gw_ip"`
+	IsInternetGw      bool                  `json:"is_inet_gw"`
+	NodeAddrs         []net.IPNet           `json:"nodes_addrs"`
+	Server            string                `json:"server"`
+	ServerVersion     string                `json:"serverversion"`
+	ServerAddrs       []ServerAddr          `json:"serveraddrs"`
+	NodePeers         []wgtypes.PeerConfig  `json:"node_peers"`
+	Peers             []wgtypes.PeerConfig  `json:"host_peers"`
+	PeerIDs           PeerMap               `json:"peerids"`
+	HostNetworkInfo   HostInfoMap           `json:"host_network_info,omitempty"`
+	EgressRoutes      []EgressNetworkRoutes `json:"egress_network_routes"`
+	FwUpdate          FwUpdate              `json:"fw_update"`
+	ReplacePeers      bool                  `json:"replace_peers"`
+	NameServers       []string              `json:"name_servers"`
+	DnsNameservers    []Nameserver          `json:"dns_nameservers"`
+	EgressWithDomains []EgressDomain        `json:"egress_with_domains"`
 	ServerConfig
 	ServerConfig
 	OldPeerUpdateFields
 	OldPeerUpdateFields
 }
 }
 
 
+type EgressDomain struct {
+	ID     string `json:"id"`
+	Node   Node   `json:"node"`
+	Host   Host   `json:"host"`
+	Domain string `json:"domain"`
+}
 type Nameserver struct {
 type Nameserver struct {
 	IPs         []string `json:"ips"`
 	IPs         []string `json:"ips"`
 	MatchDomain string   `json:"match_domain"`
 	MatchDomain string   `json:"match_domain"`

+ 1 - 0
models/network.go

@@ -25,6 +25,7 @@ type Network struct {
 	DefaultMTU          int32    `json:"defaultmtu" bson:"defaultmtu"`
 	DefaultMTU          int32    `json:"defaultmtu" bson:"defaultmtu"`
 	DefaultACL          string   `json:"defaultacl" bson:"defaultacl" yaml:"defaultacl" validate:"checkyesorno"`
 	DefaultACL          string   `json:"defaultacl" bson:"defaultacl" yaml:"defaultacl" validate:"checkyesorno"`
 	NameServers         []string `json:"dns_nameservers"`
 	NameServers         []string `json:"dns_nameservers"`
+	AutoJoin            string   `json:"auto_join"`
 }
 }
 
 
 // SaveData - sensitive fields of a network that should be kept the same
 // SaveData - sensitive fields of a network that should be kept the same

+ 45 - 37
models/settings.go

@@ -9,41 +9,49 @@ const (
 )
 )
 
 
 type ServerSettings struct {
 type ServerSettings struct {
-	NetclientAutoUpdate            bool     `json:"netclientautoupdate"`
-	Verbosity                      int32    `json:"verbosity"`
-	AuthProvider                   string   `json:"authprovider"`
-	OIDCIssuer                     string   `json:"oidcissuer"`
-	ClientID                       string   `json:"client_id"`
-	ClientSecret                   string   `json:"client_secret"`
-	SyncEnabled                    bool     `json:"sync_enabled"`
-	GoogleAdminEmail               string   `json:"google_admin_email"`
-	GoogleSACredsJson              string   `json:"google_sa_creds_json"`
-	AzureTenant                    string   `json:"azure_tenant"`
-	OktaOrgURL                     string   `json:"okta_org_url"`
-	OktaAPIToken                   string   `json:"okta_api_token"`
-	UserFilters                    []string `json:"user_filters"`
-	GroupFilters                   []string `json:"group_filters"`
-	IDPSyncInterval                string   `json:"idp_sync_interval"`
-	Telemetry                      string   `json:"telemetry"`
-	BasicAuth                      bool     `json:"basic_auth"`
-	JwtValidityDuration            int      `json:"jwt_validity_duration"`
-	MFAEnforced                    bool     `json:"mfa_enforced"`
-	RacRestrictToSingleNetwork     bool     `json:"rac_restrict_to_single_network"`
-	EndpointDetection              bool     `json:"endpoint_detection"`
-	AllowedEmailDomains            string   `json:"allowed_email_domains"`
-	EmailSenderAddr                string   `json:"email_sender_addr"`
-	EmailSenderUser                string   `json:"email_sender_user"`
-	EmailSenderPassword            string   `json:"email_sender_password"`
-	SmtpHost                       string   `json:"smtp_host"`
-	SmtpPort                       int      `json:"smtp_port"`
-	MetricInterval                 string   `json:"metric_interval"`
-	MetricsPort                    int      `json:"metrics_port"`
-	ManageDNS                      bool     `json:"manage_dns"`
-	DefaultDomain                  string   `json:"default_domain"`
-	Stun                           bool     `json:"stun"`
-	StunServers                    string   `json:"stun_servers"`
-	Theme                          Theme    `json:"theme"`
-	TextSize                       string   `json:"text_size"`
-	ReducedMotion                  bool     `json:"reduced_motion"`
-	AuditLogsRetentionPeriodInDays int      `json:"audit_logs_retention_period"`
+	NetclientAutoUpdate bool     `json:"netclientautoupdate"`
+	Verbosity           int32    `json:"verbosity"`
+	AuthProvider        string   `json:"authprovider"`
+	OIDCIssuer          string   `json:"oidcissuer"`
+	ClientID            string   `json:"client_id"`
+	ClientSecret        string   `json:"client_secret"`
+	SyncEnabled         bool     `json:"sync_enabled"`
+	GoogleAdminEmail    string   `json:"google_admin_email"`
+	GoogleSACredsJson   string   `json:"google_sa_creds_json"`
+	AzureTenant         string   `json:"azure_tenant"`
+	OktaOrgURL          string   `json:"okta_org_url"`
+	OktaAPIToken        string   `json:"okta_api_token"`
+	UserFilters         []string `json:"user_filters"`
+	GroupFilters        []string `json:"group_filters"`
+	IDPSyncInterval     string   `json:"idp_sync_interval"`
+	Telemetry           string   `json:"telemetry"`
+	BasicAuth           bool     `json:"basic_auth"`
+	// JwtValidityDuration is the validity duration of auth tokens for users
+	// on the dashboard (NMUI).
+	JwtValidityDuration int `json:"jwt_validity_duration"`
+	// JwtValidityDurationClients is the validity duration of auth tokens for
+	// users on the clients (NetDesk).
+	JwtValidityDurationClients     int    `json:"jwt_validity_duration_clients"`
+	MFAEnforced                    bool   `json:"mfa_enforced"`
+	RacRestrictToSingleNetwork     bool   `json:"rac_restrict_to_single_network"`
+	EndpointDetection              bool   `json:"endpoint_detection"`
+	AllowedEmailDomains            string `json:"allowed_email_domains"`
+	EmailSenderAddr                string `json:"email_sender_addr"`
+	EmailSenderUser                string `json:"email_sender_user"`
+	EmailSenderPassword            string `json:"email_sender_password"`
+	SmtpHost                       string `json:"smtp_host"`
+	SmtpPort                       int    `json:"smtp_port"`
+	MetricInterval                 string `json:"metric_interval"`
+	MetricsPort                    int    `json:"metrics_port"`
+	ManageDNS                      bool   `json:"manage_dns"`
+	DefaultDomain                  string `json:"default_domain"`
+	Stun                           bool   `json:"stun"`
+	StunServers                    string `json:"stun_servers"`
+	AuditLogsRetentionPeriodInDays int    `json:"audit_logs_retention_period"`
+}
+
+type UserSettings struct {
+	Theme         Theme  `json:"theme"`
+	TextSize      string `json:"text_size"`
+	ReducedMotion bool   `json:"reduced_motion"`
 }
 }

+ 1 - 0
models/ssocache.go

@@ -7,6 +7,7 @@ const DefaultExpDuration = time.Minute * 5
 
 
 // SsoState - holds SSO sign-in session data
 // SsoState - holds SSO sign-in session data
 type SsoState struct {
 type SsoState struct {
+	AppName    string    `json:"app_name"`
 	Value      string    `json:"value"`
 	Value      string    `json:"value"`
 	Expiration time.Time `json:"expiration"`
 	Expiration time.Time `json:"expiration"`
 }
 }

+ 28 - 0
models/structs.go

@@ -16,6 +16,13 @@ const (
 	PLACEHOLDER_TOKEN_TEXT = "ACCESS_TOKEN"
 	PLACEHOLDER_TOKEN_TEXT = "ACCESS_TOKEN"
 )
 )
 
 
+type FeatureFlags struct {
+	EnableNetworkActivity   bool `json:"enable_network_activity"`
+	EnableOAuth             bool `json:"enable_oauth"`
+	EnableIDPIntegration    bool `json:"enable_idp_integration"`
+	AllowMultiServerLicense bool `json:"allow_multi_server_license"`
+}
+
 // AuthParams - struct for auth params
 // AuthParams - struct for auth params
 type AuthParams struct {
 type AuthParams struct {
 	MacAddress string `json:"macaddress"`
 	MacAddress string `json:"macaddress"`
@@ -255,6 +262,8 @@ type HostPull struct {
 	DefaultGwIp       net.IP                `json:"default_gw_ip"`
 	DefaultGwIp       net.IP                `json:"default_gw_ip"`
 	IsInternetGw      bool                  `json:"is_inet_gw"`
 	IsInternetGw      bool                  `json:"is_inet_gw"`
 	EndpointDetection bool                  `json:"endpoint_detection"`
 	EndpointDetection bool                  `json:"endpoint_detection"`
+	NameServers       []string              `json:"name_servers"`
+	EgressWithDomains []EgressDomain        `json:"egress_with_domains"`
 	DnsNameservers    []Nameserver          `json:"dns_nameservers"`
 	DnsNameservers    []Nameserver          `json:"dns_nameservers"`
 }
 }
 
 
@@ -401,3 +410,22 @@ type RsrcURLInfo struct {
 	Method string
 	Method string
 	Path   string
 	Path   string
 }
 }
+
+type IDPSyncStatus struct {
+	// Status would be one of: in_progress, completed or failed.
+	Status string `json:"status"`
+	// Description is empty if the sync is ongoing or completed,
+	// and describes the error when the sync fails.
+	Description string `json:"description"`
+}
+
+type IDPSyncTestRequest struct {
+	AuthProvider      string `json:"auth_provider"`
+	ClientID          string `json:"client_id"`
+	ClientSecret      string `json:"client_secret"`
+	AzureTenantID     string `json:"azure_tenant_id"`
+	GoogleAdminEmail  string `json:"google_admin_email"`
+	GoogleSACredsJson string `json:"google_sa_creds_json"`
+	OktaOrgURL        string `json:"okta_org_url"`
+	OktaAPIToken      string `json:"okta_api_token"`
+}

+ 8 - 7
models/user_mgmt.go

@@ -62,21 +62,21 @@ var RsrcTypeMap = map[RsrcType]struct{}{
 
 
 const AllNetworks NetworkID = "all_networks"
 const AllNetworks NetworkID = "all_networks"
 const (
 const (
-	HostRsrc           RsrcType = "hosts"
-	RelayRsrc          RsrcType = "relays"
+	HostRsrc           RsrcType = "host"
+	RelayRsrc          RsrcType = "relay"
 	RemoteAccessGwRsrc RsrcType = "remote_access_gw"
 	RemoteAccessGwRsrc RsrcType = "remote_access_gw"
-	GatewayRsrc        RsrcType = "gateways"
-	ExtClientsRsrc     RsrcType = "extclients"
+	GatewayRsrc        RsrcType = "gateway"
+	ExtClientsRsrc     RsrcType = "extclient"
 	InetGwRsrc         RsrcType = "inet_gw"
 	InetGwRsrc         RsrcType = "inet_gw"
 	EgressGwRsrc       RsrcType = "egress"
 	EgressGwRsrc       RsrcType = "egress"
-	NetworkRsrc        RsrcType = "networks"
+	NetworkRsrc        RsrcType = "network"
 	EnrollmentKeysRsrc RsrcType = "enrollment_key"
 	EnrollmentKeysRsrc RsrcType = "enrollment_key"
-	UserRsrc           RsrcType = "users"
+	UserRsrc           RsrcType = "user"
 	AclRsrc            RsrcType = "acl"
 	AclRsrc            RsrcType = "acl"
 	TagRsrc            RsrcType = "tag"
 	TagRsrc            RsrcType = "tag"
 	DnsRsrc            RsrcType = "dns"
 	DnsRsrc            RsrcType = "dns"
 	FailOverRsrc       RsrcType = "fail_over"
 	FailOverRsrc       RsrcType = "fail_over"
-	MetricRsrc         RsrcType = "metrics"
+	MetricRsrc         RsrcType = "metric"
 )
 )
 
 
 const (
 const (
@@ -150,6 +150,7 @@ type UserGroup struct {
 	Default                    bool                                  `json:"default"`
 	Default                    bool                                  `json:"default"`
 	Name                       string                                `json:"name"`
 	Name                       string                                `json:"name"`
 	NetworkRoles               map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
 	NetworkRoles               map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
+	ColorCode                  string                                `json:"color_code"`
 	MetaData                   string                                `json:"meta_data"`
 	MetaData                   string                                `json:"meta_data"`
 }
 }
 
 

+ 1 - 1
mq/handlers.go

@@ -274,7 +274,7 @@ func HandleHostCheckin(h, currentHost *models.Host) bool {
 			return false
 			return false
 		}
 		}
 	}
 	}
-	ifaceDelta := len(h.Interfaces) != len(currentHost.Interfaces) ||
+	ifaceDelta := len(h.Interfaces) != len(currentHost.Interfaces) || !logic.CompareIfaceSlices(h.Interfaces, currentHost.Interfaces) ||
 		!h.EndpointIP.Equal(currentHost.EndpointIP) ||
 		!h.EndpointIP.Equal(currentHost.EndpointIP) ||
 		(len(h.NatType) > 0 && h.NatType != currentHost.NatType) ||
 		(len(h.NatType) > 0 && h.NatType != currentHost.NatType) ||
 		h.DefaultInterface != currentHost.DefaultInterface ||
 		h.DefaultInterface != currentHost.DefaultInterface ||

+ 2 - 0
mq/publishers.go

@@ -253,6 +253,7 @@ func sendPeers() {
 func SendDNSSyncByNetwork(network string) error {
 func SendDNSSyncByNetwork(network string) error {
 
 
 	k, err := logic.GetDNS(network)
 	k, err := logic.GetDNS(network)
+	k = append(k, logic.EgressDNs(network)...)
 	if err == nil && len(k) > 0 {
 	if err == nil && len(k) > 0 {
 		err = PushSyncDNS(k)
 		err = PushSyncDNS(k)
 		if err != nil {
 		if err != nil {
@@ -269,6 +270,7 @@ func sendDNSSync() error {
 	if err == nil && len(networks) > 0 {
 	if err == nil && len(networks) > 0 {
 		for _, v := range networks {
 		for _, v := range networks {
 			k, err := logic.GetDNS(v.NetID)
 			k, err := logic.GetDNS(v.NetID)
+			k = append(k, logic.EgressDNs(v.NetID)...)
 			if err == nil && len(k) > 0 {
 			if err == nil && len(k) > 0 {
 				err = PushSyncDNS(k)
 				err = PushSyncDNS(k)
 				if err != nil {
 				if err != nil {

+ 4 - 0
pro/auth/auth.go

@@ -106,6 +106,10 @@ func ResetAuthProvider() {
 	InitializeAuthProvider()
 	InitializeAuthProvider()
 }
 }
 
 
+func IsOAuthConfigured() bool {
+	return auth_provider != nil
+}
+
 // InitializeAuthProvider - initializes the auth provider if any is present
 // InitializeAuthProvider - initializes the auth provider if any is present
 func InitializeAuthProvider() string {
 func InitializeAuthProvider() string {
 	var functions = getCurrentAuthFunctions()
 	var functions = getCurrentAuthFunctions()

+ 15 - 4
pro/auth/azure-ad.go

@@ -40,13 +40,18 @@ func initAzureAD(redirectURL string, clientID string, clientSecret string) {
 }
 }
 
 
 func handleAzureLogin(w http.ResponseWriter, r *http.Request) {
 func handleAzureLogin(w http.ResponseWriter, r *http.Request) {
+	appName := r.Header.Get("X-Application-Name")
+	if appName == "" {
+		appName = logic.NetmakerDesktopApp
+	}
+
 	var oauth_state_string = logic.RandomString(user_signin_length)
 	var oauth_state_string = logic.RandomString(user_signin_length)
 	if auth_provider == nil {
 	if auth_provider == nil {
 		handleOauthNotConfigured(w)
 		handleOauthNotConfigured(w)
 		return
 		return
 	}
 	}
 
 
-	if err := logic.SetState(oauth_state_string); err != nil {
+	if err := logic.SetState(appName, oauth_state_string); err != nil {
 		handleOauthNotConfigured(w)
 		handleOauthNotConfigured(w)
 		return
 		return
 	}
 	}
@@ -56,9 +61,15 @@ func handleAzureLogin(w http.ResponseWriter, r *http.Request) {
 }
 }
 
 
 func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
-
 	var rState, rCode = getStateAndCode(r)
 	var rState, rCode = getStateAndCode(r)
-	var content, err = getAzureUserInfo(rState, rCode)
+
+	state, err := logic.GetState(rState)
+	if err != nil {
+		handleOauthNotValid(w)
+		return
+	}
+
+	content, err := getAzureUserInfo(rState, rCode)
 	if err != nil {
 	if err != nil {
 		logger.Log(1, "error when getting user info from azure:", err.Error())
 		logger.Log(1, "error when getting user info from azure:", err.Error())
 		if strings.Contains(err.Error(), "invalid oauth state") || strings.Contains(err.Error(), "failed to fetch user email from SSO state") {
 		if strings.Contains(err.Error(), "invalid oauth state") || strings.Contains(err.Error(), "failed to fetch user email from SSO state") {
@@ -179,7 +190,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		Password: newPass,
 		Password: newPass,
 	}
 	}
 
 
-	var jwt, jwtErr = logic.VerifyAuthRequest(authRequest)
+	var jwt, jwtErr = logic.VerifyAuthRequest(authRequest, state.AppName)
 	if jwtErr != nil {
 	if jwtErr != nil {
 		logger.Log(1, "could not parse jwt for user", authRequest.UserName)
 		logger.Log(1, "could not parse jwt for user", authRequest.UserName)
 		return
 		return

+ 15 - 4
pro/auth/github.go

@@ -40,13 +40,18 @@ func initGithub(redirectURL string, clientID string, clientSecret string) {
 }
 }
 
 
 func handleGithubLogin(w http.ResponseWriter, r *http.Request) {
 func handleGithubLogin(w http.ResponseWriter, r *http.Request) {
+	appName := r.Header.Get("X-Application-Name")
+	if appName == "" {
+		appName = logic.NetmakerDesktopApp
+	}
+
 	var oauth_state_string = logic.RandomString(user_signin_length)
 	var oauth_state_string = logic.RandomString(user_signin_length)
 	if auth_provider == nil {
 	if auth_provider == nil {
 		handleOauthNotConfigured(w)
 		handleOauthNotConfigured(w)
 		return
 		return
 	}
 	}
 
 
-	if err := logic.SetState(oauth_state_string); err != nil {
+	if err := logic.SetState(appName, oauth_state_string); err != nil {
 		handleOauthNotConfigured(w)
 		handleOauthNotConfigured(w)
 		return
 		return
 	}
 	}
@@ -56,9 +61,15 @@ func handleGithubLogin(w http.ResponseWriter, r *http.Request) {
 }
 }
 
 
 func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
-
 	var rState, rCode = getStateAndCode(r)
 	var rState, rCode = getStateAndCode(r)
-	var content, err = getGithubUserInfo(rState, rCode)
+
+	state, err := logic.GetState(rState)
+	if err != nil {
+		handleOauthNotValid(w)
+		return
+	}
+
+	content, err := getGithubUserInfo(rState, rCode)
 	if err != nil {
 	if err != nil {
 		logger.Log(1, "error when getting user info from github:", err.Error())
 		logger.Log(1, "error when getting user info from github:", err.Error())
 		if strings.Contains(err.Error(), "invalid oauth state") || strings.Contains(err.Error(), "failed to fetch user email from SSO state") {
 		if strings.Contains(err.Error(), "invalid oauth state") || strings.Contains(err.Error(), "failed to fetch user email from SSO state") {
@@ -170,7 +181,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 		Password: newPass,
 		Password: newPass,
 	}
 	}
 
 
-	var jwt, jwtErr = logic.VerifyAuthRequest(authRequest)
+	var jwt, jwtErr = logic.VerifyAuthRequest(authRequest, state.AppName)
 	if jwtErr != nil {
 	if jwtErr != nil {
 		logger.Log(1, "could not parse jwt for user", authRequest.UserName)
 		logger.Log(1, "could not parse jwt for user", authRequest.UserName)
 		return
 		return

+ 15 - 4
pro/auth/google.go

@@ -40,13 +40,18 @@ func initGoogle(redirectURL string, clientID string, clientSecret string) {
 }
 }
 
 
 func handleGoogleLogin(w http.ResponseWriter, r *http.Request) {
 func handleGoogleLogin(w http.ResponseWriter, r *http.Request) {
+	appName := r.Header.Get("X-Application-Name")
+	if appName == "" {
+		appName = logic.NetmakerDesktopApp
+	}
+
 	var oauth_state_string = logic.RandomString(user_signin_length)
 	var oauth_state_string = logic.RandomString(user_signin_length)
 	if auth_provider == nil {
 	if auth_provider == nil {
 		handleOauthNotConfigured(w)
 		handleOauthNotConfigured(w)
 		return
 		return
 	}
 	}
 	logger.Log(0, "Setting OAuth State ", oauth_state_string)
 	logger.Log(0, "Setting OAuth State ", oauth_state_string)
-	if err := logic.SetState(oauth_state_string); err != nil {
+	if err := logic.SetState(appName, oauth_state_string); err != nil {
 		handleOauthNotConfigured(w)
 		handleOauthNotConfigured(w)
 		return
 		return
 	}
 	}
@@ -56,10 +61,16 @@ func handleGoogleLogin(w http.ResponseWriter, r *http.Request) {
 }
 }
 
 
 func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
-
 	var rState, rCode = getStateAndCode(r)
 	var rState, rCode = getStateAndCode(r)
 	logger.Log(0, "Fetched OAuth State ", rState)
 	logger.Log(0, "Fetched OAuth State ", rState)
-	var content, err = getGoogleUserInfo(rState, rCode)
+
+	state, err := logic.GetState(rState)
+	if err != nil {
+		handleOauthNotValid(w)
+		return
+	}
+
+	content, err := getGoogleUserInfo(rState, rCode)
 	if err != nil {
 	if err != nil {
 		logger.Log(1, "error when getting user info from google:", err.Error())
 		logger.Log(1, "error when getting user info from google:", err.Error())
 		if strings.Contains(err.Error(), "invalid oauth state") {
 		if strings.Contains(err.Error(), "invalid oauth state") {
@@ -162,7 +173,7 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 		Password: newPass,
 		Password: newPass,
 	}
 	}
 
 
-	var jwt, jwtErr = logic.VerifyAuthRequest(authRequest)
+	var jwt, jwtErr = logic.VerifyAuthRequest(authRequest, state.AppName)
 	if jwtErr != nil {
 	if jwtErr != nil {
 		logger.Log(1, "could not parse jwt for user", authRequest.UserName)
 		logger.Log(1, "could not parse jwt for user", authRequest.UserName)
 		return
 		return

+ 1 - 1
pro/auth/headless_callback.go

@@ -86,7 +86,7 @@ func HandleHeadlessSSOCallback(w http.ResponseWriter, r *http.Request) {
 	jwt, jwtErr := logic.VerifyAuthRequest(models.UserAuthParams{
 	jwt, jwtErr := logic.VerifyAuthRequest(models.UserAuthParams{
 		UserName: user.UserName,
 		UserName: user.UserName,
 		Password: newPass,
 		Password: newPass,
-	})
+	}, logic.NetclientApp)
 	if jwtErr != nil {
 	if jwtErr != nil {
 		logger.Log(1, "could not parse jwt for user", userClaims.getUserName())
 		logger.Log(1, "could not parse jwt for user", userClaims.getUserName())
 		return
 		return

+ 14 - 4
pro/auth/oidc.go

@@ -52,13 +52,18 @@ func initOIDC(redirectURL string, clientID string, clientSecret string, issuer s
 }
 }
 
 
 func handleOIDCLogin(w http.ResponseWriter, r *http.Request) {
 func handleOIDCLogin(w http.ResponseWriter, r *http.Request) {
+	appName := r.Header.Get("X-Application-Name")
+	if appName == "" {
+		appName = logic.NetmakerDesktopApp
+	}
+
 	var oauth_state_string = logic.RandomString(user_signin_length)
 	var oauth_state_string = logic.RandomString(user_signin_length)
 	if auth_provider == nil {
 	if auth_provider == nil {
 		handleOauthNotConfigured(w)
 		handleOauthNotConfigured(w)
 		return
 		return
 	}
 	}
 
 
-	if err := logic.SetState(oauth_state_string); err != nil {
+	if err := logic.SetState(appName, oauth_state_string); err != nil {
 		handleOauthNotConfigured(w)
 		handleOauthNotConfigured(w)
 		return
 		return
 	}
 	}
@@ -67,10 +72,15 @@ func handleOIDCLogin(w http.ResponseWriter, r *http.Request) {
 }
 }
 
 
 func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
-
 	var rState, rCode = getStateAndCode(r)
 	var rState, rCode = getStateAndCode(r)
 
 
-	var content, err = getOIDCUserInfo(rState, rCode)
+	state, err := logic.GetState(rState)
+	if err != nil {
+		handleOauthNotValid(w)
+		return
+	}
+
+	content, err := getOIDCUserInfo(rState, rCode)
 	if err != nil {
 	if err != nil {
 		logger.Log(1, "error when getting user info from callback:", err.Error())
 		logger.Log(1, "error when getting user info from callback:", err.Error())
 		if strings.Contains(err.Error(), "invalid oauth state") {
 		if strings.Contains(err.Error(), "invalid oauth state") {
@@ -170,7 +180,7 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 		Password: newPass,
 		Password: newPass,
 	}
 	}
 
 
-	var jwt, jwtErr = logic.VerifyAuthRequest(authRequest)
+	var jwt, jwtErr = logic.VerifyAuthRequest(authRequest, state.AppName)
 	if jwtErr != nil {
 	if jwtErr != nil {
 		logger.Log(1, "could not parse jwt for user", authRequest.UserName, jwtErr.Error())
 		logger.Log(1, "could not parse jwt for user", authRequest.UserName, jwtErr.Error())
 		return
 		return

+ 33 - 4
pro/auth/sync.go

@@ -21,6 +21,8 @@ import (
 var (
 var (
 	cancelSyncHook context.CancelFunc
 	cancelSyncHook context.CancelFunc
 	hookStopWg     sync.WaitGroup
 	hookStopWg     sync.WaitGroup
+	idpSyncMtx     sync.Mutex
+	idpSyncErr     error
 )
 )
 
 
 func ResetIDPSyncHook() {
 func ResetIDPSyncHook() {
@@ -59,6 +61,8 @@ func runIDPSyncHook(ctx context.Context) {
 }
 }
 
 
 func SyncFromIDP() error {
 func SyncFromIDP() error {
+	idpSyncMtx.Lock()
+	defer idpSyncMtx.Unlock()
 	settings := logic.GetServerSettings()
 	settings := logic.GetServerSettings()
 
 
 	var idpClient idp.Client
 	var idpClient idp.Client
@@ -66,14 +70,18 @@ func SyncFromIDP() error {
 	var idpGroups []idp.Group
 	var idpGroups []idp.Group
 	var err error
 	var err error
 
 
+	defer func() {
+		idpSyncErr = err
+	}()
+
 	switch settings.AuthProvider {
 	switch settings.AuthProvider {
 	case "google":
 	case "google":
-		idpClient, err = google.NewGoogleWorkspaceClient()
+		idpClient, err = google.NewGoogleWorkspaceClientFromSettings()
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 	case "azure-ad":
 	case "azure-ad":
-		idpClient = azure.NewAzureEntraIDClient()
+		idpClient = azure.NewAzureEntraIDClientFromSettings()
 	case "okta":
 	case "okta":
 		idpClient, err = okta.NewOktaClientFromSettings()
 		idpClient, err = okta.NewOktaClientFromSettings()
 		if err != nil {
 		if err != nil {
@@ -81,7 +89,8 @@ func SyncFromIDP() error {
 		}
 		}
 	default:
 	default:
 		if settings.AuthProvider != "" {
 		if settings.AuthProvider != "" {
-			return fmt.Errorf("invalid auth provider: %s", settings.AuthProvider)
+			err = fmt.Errorf("invalid auth provider: %s", settings.AuthProvider)
+			return err
 		}
 		}
 	}
 	}
 
 
@@ -110,7 +119,8 @@ func SyncFromIDP() error {
 		return err
 		return err
 	}
 	}
 
 
-	return syncGroups(idpGroups)
+	err = syncGroups(idpGroups)
+	return err
 }
 }
 
 
 func syncUsers(idpUsers []idp.User) error {
 func syncUsers(idpUsers []idp.User) error {
@@ -326,6 +336,25 @@ func syncGroups(idpGroups []idp.Group) error {
 	return nil
 	return nil
 }
 }
 
 
+func GetIDPSyncStatus() models.IDPSyncStatus {
+	if idpSyncMtx.TryLock() {
+		defer idpSyncMtx.Unlock()
+		if idpSyncErr == nil {
+			return models.IDPSyncStatus{
+				Status: "completed",
+			}
+		} else {
+			return models.IDPSyncStatus{
+				Status:      "failed",
+				Description: idpSyncErr.Error(),
+			}
+		}
+	} else {
+		return models.IDPSyncStatus{
+			Status: "in_progress",
+		}
+	}
+}
 func filterUsersByGroupMembership(idpUsers []idp.User, idpGroups []idp.Group) []idp.User {
 func filterUsersByGroupMembership(idpUsers []idp.User, idpGroups []idp.Group) []idp.User {
 	usersMap := make(map[string]int)
 	usersMap := make(map[string]int)
 	for i, user := range idpUsers {
 	for i, user := range idpUsers {

+ 0 - 19
pro/controllers/metrics.go

@@ -20,7 +20,6 @@ func MetricHandlers(r *mux.Router) {
 	r.HandleFunc("/api/metrics/{network}", logic.SecurityCheck(true, http.HandlerFunc(getNetworkNodesMetrics))).Methods(http.MethodGet)
 	r.HandleFunc("/api/metrics/{network}", logic.SecurityCheck(true, http.HandlerFunc(getNetworkNodesMetrics))).Methods(http.MethodGet)
 	r.HandleFunc("/api/metrics", logic.SecurityCheck(true, http.HandlerFunc(getAllMetrics))).Methods(http.MethodGet)
 	r.HandleFunc("/api/metrics", logic.SecurityCheck(true, http.HandlerFunc(getAllMetrics))).Methods(http.MethodGet)
 	r.HandleFunc("/api/metrics-ext/{network}", logic.SecurityCheck(true, http.HandlerFunc(getNetworkExtMetrics))).Methods(http.MethodGet)
 	r.HandleFunc("/api/metrics-ext/{network}", logic.SecurityCheck(true, http.HandlerFunc(getNetworkExtMetrics))).Methods(http.MethodGet)
-	r.HandleFunc("/api/v1/graph/{network}", logic.SecurityCheck(true, http.HandlerFunc(graph))).Methods(http.MethodGet)
 }
 }
 
 
 // get the metrics of a given node
 // get the metrics of a given node
@@ -166,21 +165,3 @@ func getAllMetrics(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(networkMetrics)
 	json.NewEncoder(w).Encode(networkMetrics)
 }
 }
-
-func graph(w http.ResponseWriter, r *http.Request) {
-	w.Header().Set("Content-Type", "application/json")
-
-	var params = mux.Vars(r)
-	network := params["network"]
-	networkNodes, err := logic.GetNetworkNodes(network)
-	if err != nil {
-		logger.Log(1, r.Header.Get("user"), "failed to get network nodes", err.Error())
-		return
-	}
-	networkNodes = logic.AddStaticNodestoList(networkNodes)
-	// return all the nodes in JSON/API format
-	apiNodes := logic.GetAllNodesAPIWithLocation(networkNodes[:])
-	logic.SortApiNodes(apiNodes[:])
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(apiNodes)
-}

+ 31 - 0
pro/controllers/networks.go

@@ -0,0 +1,31 @@
+package controllers
+
+import (
+	"encoding/json"
+	"github.com/gorilla/mux"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic"
+	"net/http"
+)
+
+func NetworkHandlers(r *mux.Router) {
+	r.HandleFunc("/api/v1/networks/{network}/graph", logic.SecurityCheck(true, http.HandlerFunc(getNetworkGraph))).Methods(http.MethodGet)
+}
+
+func getNetworkGraph(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json")
+
+	var params = mux.Vars(r)
+	network := params["network"]
+	networkNodes, err := logic.GetNetworkNodes(network)
+	if err != nil {
+		logger.Log(1, r.Header.Get("user"), "failed to get network nodes", err.Error())
+		return
+	}
+	networkNodes = logic.AddStaticNodestoList(networkNodes)
+	// return all the nodes in JSON/API format
+	apiNodes := logic.GetAllNodesAPIWithLocation(networkNodes[:])
+	logic.SortApiNodes(apiNodes[:])
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(apiNodes)
+}

+ 395 - 89
pro/controllers/users.go

@@ -19,6 +19,10 @@ import (
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/mq"
 	proAuth "github.com/gravitl/netmaker/pro/auth"
 	proAuth "github.com/gravitl/netmaker/pro/auth"
 	"github.com/gravitl/netmaker/pro/email"
 	"github.com/gravitl/netmaker/pro/email"
+	"github.com/gravitl/netmaker/pro/idp"
+	"github.com/gravitl/netmaker/pro/idp/azure"
+	"github.com/gravitl/netmaker/pro/idp/google"
+	"github.com/gravitl/netmaker/pro/idp/okta"
 	proLogic "github.com/gravitl/netmaker/pro/logic"
 	proLogic "github.com/gravitl/netmaker/pro/logic"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/utils"
 	"github.com/gravitl/netmaker/utils"
@@ -44,6 +48,8 @@ func UserHandlers(r *mux.Router) {
 	r.HandleFunc("/api/v1/users/group", logic.SecurityCheck(true, http.HandlerFunc(createUserGroup))).Methods(http.MethodPost)
 	r.HandleFunc("/api/v1/users/group", logic.SecurityCheck(true, http.HandlerFunc(createUserGroup))).Methods(http.MethodPost)
 	r.HandleFunc("/api/v1/users/group", logic.SecurityCheck(true, http.HandlerFunc(updateUserGroup))).Methods(http.MethodPut)
 	r.HandleFunc("/api/v1/users/group", logic.SecurityCheck(true, http.HandlerFunc(updateUserGroup))).Methods(http.MethodPut)
 	r.HandleFunc("/api/v1/users/group", logic.SecurityCheck(true, http.HandlerFunc(deleteUserGroup))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/v1/users/group", logic.SecurityCheck(true, http.HandlerFunc(deleteUserGroup))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/v1/users/add_network_user", logic.SecurityCheck(true, http.HandlerFunc(addUsertoNetwork))).Methods(http.MethodPut)
+	r.HandleFunc("/api/v1/users/remove_network_user", logic.SecurityCheck(true, http.HandlerFunc(removeUserfromNetwork))).Methods(http.MethodPut)
 
 
 	// User Invite Handlers
 	// User Invite Handlers
 	r.HandleFunc("/api/v1/users/invite", userInviteVerify).Methods(http.MethodGet)
 	r.HandleFunc("/api/v1/users/invite", userInviteVerify).Methods(http.MethodGet)
@@ -64,6 +70,8 @@ func UserHandlers(r *mux.Router) {
 	r.HandleFunc("/api/users/ingress/{ingress_id}", logic.SecurityCheck(true, http.HandlerFunc(ingressGatewayUsers))).Methods(http.MethodGet)
 	r.HandleFunc("/api/users/ingress/{ingress_id}", logic.SecurityCheck(true, http.HandlerFunc(ingressGatewayUsers))).Methods(http.MethodGet)
 
 
 	r.HandleFunc("/api/idp/sync", logic.SecurityCheck(true, http.HandlerFunc(syncIDP))).Methods(http.MethodPost)
 	r.HandleFunc("/api/idp/sync", logic.SecurityCheck(true, http.HandlerFunc(syncIDP))).Methods(http.MethodPost)
+	r.HandleFunc("/api/idp/sync/test", logic.SecurityCheck(true, http.HandlerFunc(testIDPSync))).Methods(http.MethodPost)
+	r.HandleFunc("/api/idp/sync/status", logic.SecurityCheck(true, http.HandlerFunc(getIDPSyncStatus))).Methods(http.MethodGet)
 	r.HandleFunc("/api/idp", logic.SecurityCheck(true, http.HandlerFunc(removeIDPIntegration))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/idp", logic.SecurityCheck(true, http.HandlerFunc(removeIDPIntegration))).Methods(http.MethodDelete)
 }
 }
 
 
@@ -464,43 +472,6 @@ func createUserGroup(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
-	networks, err := logic.GetNetworks()
-	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-		return
-	}
-	for _, network := range networks {
-		acl := models.Acl{
-			ID:          uuid.New().String(),
-			Name:        fmt.Sprintf("%s group", userGroupReq.Group.Name),
-			MetaData:    "This Policy allows user group to communicate with all gateways",
-			Default:     false,
-			ServiceType: models.Any,
-			NetworkID:   models.NetworkID(network.NetID),
-			Proto:       models.ALL,
-			RuleType:    models.UserPolicy,
-			Src: []models.AclPolicyTag{
-				{
-					ID:    models.UserGroupAclID,
-					Value: userGroupReq.Group.ID.String(),
-				},
-			},
-			Dst: []models.AclPolicyTag{
-				{
-					ID:    models.NodeTagID,
-					Value: fmt.Sprintf("%s.%s", models.NetworkID(network.NetID), models.GwTagName),
-				}},
-			AllowedDirection: models.TrafficDirectionUni,
-			Enabled:          true,
-			CreatedBy:        "auto",
-			CreatedAt:        time.Now().UTC(),
-		}
-		err = logic.InsertAcl(acl)
-		if err != nil {
-			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-			return
-		}
-	}
 
 
 	for _, userID := range userGroupReq.Members {
 	for _, userID := range userGroupReq.Members {
 		user, err := logic.GetUser(userID)
 		user, err := logic.GetUser(userID)
@@ -528,6 +499,7 @@ func createUserGroup(w http.ResponseWriter, r *http.Request) {
 		},
 		},
 		Origin: models.Dashboard,
 		Origin: models.Dashboard,
 	})
 	})
+	go mq.PublishPeerUpdate(false)
 	logic.ReturnSuccessResponseWithJson(w, r, userGroupReq.Group, "created user group")
 	logic.ReturnSuccessResponseWithJson(w, r, userGroupReq.Group, "created user group")
 }
 }
 
 
@@ -593,11 +565,215 @@ func updateUserGroup(w http.ResponseWriter, r *http.Request) {
 		},
 		},
 		Origin: models.Dashboard,
 		Origin: models.Dashboard,
 	})
 	})
+	replacePeers := false
+	go func() {
+		networksAdded := make([]models.NetworkID, 0)
+		networksRemoved := make([]models.NetworkID, 0)
+
+		for networkID := range userGroup.NetworkRoles {
+			if _, ok := currUserG.NetworkRoles[networkID]; !ok {
+				networksAdded = append(networksAdded, networkID)
+			}
+		}
+
+		for networkID := range currUserG.NetworkRoles {
+			if _, ok := userGroup.NetworkRoles[networkID]; !ok {
+				networksRemoved = append(networksRemoved, networkID)
+			}
+		}
+
+		for _, networkID := range networksAdded {
+			// ensure the network exists.
+			network, err := logic.GetNetwork(networkID.String())
+			if err != nil {
+				continue
+			}
+
+			// insert acl if the network is added to the group.
+			acl := models.Acl{
+				ID:          uuid.New().String(),
+				Name:        fmt.Sprintf("%s group", userGroup.Name),
+				MetaData:    "This Policy allows user group to communicate with all gateways",
+				Default:     false,
+				ServiceType: models.Any,
+				NetworkID:   models.NetworkID(network.NetID),
+				Proto:       models.ALL,
+				RuleType:    models.UserPolicy,
+				Src: []models.AclPolicyTag{
+					{
+						ID:    models.UserGroupAclID,
+						Value: userGroup.ID.String(),
+					},
+				},
+				Dst: []models.AclPolicyTag{
+					{
+						ID:    models.NodeTagID,
+						Value: fmt.Sprintf("%s.%s", models.NetworkID(network.NetID), models.GwTagName),
+					}},
+				AllowedDirection: models.TrafficDirectionUni,
+				Enabled:          true,
+				CreatedBy:        "auto",
+				CreatedAt:        time.Now().UTC(),
+			}
+			_ = logic.InsertAcl(acl)
+			replacePeers = true
+		}
+
+		// since this group doesn't have a role for this network,
+		// there is no point in having this group as src in any
+		// of the network's acls.
+		for _, networkID := range networksRemoved {
+			acls, err := logic.ListAclsByNetwork(networkID)
+			if err != nil {
+				continue
+			}
+
+			for _, acl := range acls {
+				var hasGroupSrc bool
+				newAclSrc := make([]models.AclPolicyTag, 0)
+				for _, src := range acl.Src {
+					if src.ID == models.UserGroupAclID && src.Value == userGroup.ID.String() {
+						hasGroupSrc = true
+					} else {
+						newAclSrc = append(newAclSrc, src)
+					}
+				}
+
+				if hasGroupSrc {
+					if len(newAclSrc) == 0 {
+						// no other src exists, delete acl.
+						_ = logic.DeleteAcl(acl)
+					} else {
+						// other sources exist, update acl.
+						acl.Src = newAclSrc
+						_ = logic.UpsertAcl(acl)
+					}
+					replacePeers = true
+				}
+			}
+		}
+	}()
+
 	// reset configs for service user
 	// reset configs for service user
-	go proLogic.UpdatesUserGwAccessOnGrpUpdates(currUserG.NetworkRoles, userGroup.NetworkRoles)
+	go proLogic.UpdatesUserGwAccessOnGrpUpdates(userGroup.ID, currUserG.NetworkRoles, userGroup.NetworkRoles)
+	go mq.PublishPeerUpdate(replacePeers)
 	logic.ReturnSuccessResponseWithJson(w, r, userGroup, "updated user group")
 	logic.ReturnSuccessResponseWithJson(w, r, userGroup, "updated user group")
 }
 }
 
 
+// swagger:route PUT /api/v1/users/add_network_user user addUsertoNetwork
+//
+// add user to network.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
+func addUsertoNetwork(w http.ResponseWriter, r *http.Request) {
+	username := r.URL.Query().Get("username")
+	if username == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("username is required"), logic.BadReq))
+		return
+	}
+	netID := r.URL.Query().Get("network_id")
+	if netID == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("network is required"), logic.BadReq))
+		return
+	}
+	user, err := logic.GetUser(username)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
+		return
+	}
+	if user.PlatformRoleID != models.ServiceUser {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("can only add service users"), logic.BadReq))
+		return
+	}
+	oldUser := *user
+	user.UserGroups[proLogic.GetDefaultNetworkUserGroupID(models.NetworkID(netID))] = struct{}{}
+	logic.UpsertUser(*user)
+	logic.LogEvent(&models.Event{
+		Action: models.Update,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   user.UserName,
+			Name: user.UserName,
+			Type: models.UserSub,
+		},
+		Diff: models.Diff{
+			Old: oldUser,
+			New: user,
+		},
+		Origin: models.Dashboard,
+	})
+
+	logic.ReturnSuccessResponseWithJson(w, r, user, "updated user group")
+}
+
+// swagger:route PUT /api/v1/users/remove_network_user user removeUserfromNetwork
+//
+// add user to network.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
+func removeUserfromNetwork(w http.ResponseWriter, r *http.Request) {
+	username := r.URL.Query().Get("username")
+	if username == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("username is required"), logic.BadReq))
+		return
+	}
+	netID := r.URL.Query().Get("network_id")
+	if netID == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("network is required"), logic.BadReq))
+		return
+	}
+	user, err := logic.GetUser(username)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
+		return
+	}
+	if user.PlatformRoleID != models.ServiceUser {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("can only add service users"), logic.BadReq))
+		return
+	}
+	oldUser := *user
+	delete(user.UserGroups, proLogic.GetDefaultNetworkUserGroupID(models.NetworkID(netID)))
+	logic.UpsertUser(*user)
+	logic.LogEvent(&models.Event{
+		Action: models.Update,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   user.UserName,
+			Name: user.UserName,
+			Type: models.UserSub,
+		},
+		Diff: models.Diff{
+			Old: oldUser,
+			New: user,
+		},
+		Origin: models.Dashboard,
+	})
+
+	logic.ReturnSuccessResponseWithJson(w, r, user, "updated user group")
+}
+
 // swagger:route DELETE /api/v1/user/group user deleteUserGroup
 // swagger:route DELETE /api/v1/user/group user deleteUserGroup
 //
 //
 // delete user group.
 // delete user group.
@@ -652,7 +828,42 @@ func deleteUserGroup(w http.ResponseWriter, r *http.Request) {
 		},
 		},
 		Origin: models.Dashboard,
 		Origin: models.Dashboard,
 	})
 	})
-	go proLogic.UpdatesUserGwAccessOnGrpUpdates(userG.NetworkRoles, make(map[models.NetworkID]map[models.UserRoleID]struct{}))
+	replacePeers := false
+	go func() {
+		for networkID := range userG.NetworkRoles {
+			acls, err := logic.ListAclsByNetwork(networkID)
+			if err != nil {
+				continue
+			}
+
+			for _, acl := range acls {
+				var hasGroupSrc bool
+				newAclSrc := make([]models.AclPolicyTag, 0)
+				for _, src := range acl.Src {
+					if src.ID == models.UserGroupAclID && src.Value == userG.ID.String() {
+						hasGroupSrc = true
+					} else {
+						newAclSrc = append(newAclSrc, src)
+					}
+				}
+
+				if hasGroupSrc {
+					if len(newAclSrc) == 0 {
+						// no other src exists, delete acl.
+						_ = logic.DeleteAcl(acl)
+					} else {
+						// other sources exist, update acl.
+						acl.Src = newAclSrc
+						_ = logic.UpsertAcl(acl)
+					}
+					replacePeers = true
+				}
+			}
+		}
+	}()
+
+	go proLogic.UpdatesUserGwAccessOnGrpUpdates(userG.ID, userG.NetworkRoles, make(map[models.NetworkID]map[models.UserRoleID]struct{}))
+	go mq.PublishPeerUpdate(replacePeers)
 	logic.ReturnSuccessResponseWithJson(w, r, nil, "deleted user group")
 	logic.ReturnSuccessResponseWithJson(w, r, nil, "deleted user group")
 }
 }
 
 
@@ -1250,6 +1461,7 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to fetch user %s, error: %v", username, err), "badrequest"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to fetch user %s, error: %v", username, err), "badrequest"))
 		return
 		return
 	}
 	}
+	deviceID := r.URL.Query().Get("device_id")
 	remoteAccessClientID := r.URL.Query().Get("remote_access_clientid")
 	remoteAccessClientID := r.URL.Query().Get("remote_access_clientid")
 	var req models.UserRemoteGwsReq
 	var req models.UserRemoteGwsReq
 	if remoteAccessClientID == "" {
 	if remoteAccessClientID == "" {
@@ -1275,64 +1487,105 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 	userGwNodes := proLogic.GetUserRAGNodes(*user)
 	userGwNodes := proLogic.GetUserRAGNodes(*user)
+
+	userExtClients := make(map[string][]models.ExtClient)
+
+	// group all extclients of the requesting user by ingress
+	// gateway.
 	for _, extClient := range allextClients {
 	for _, extClient := range allextClients {
-		node, ok := userGwNodes[extClient.IngressGatewayID]
+		// filter our extclients that don't belong to this user.
+		if extClient.OwnerID != username {
+			continue
+		}
+
+		_, ok := userExtClients[extClient.IngressGatewayID]
+		if !ok {
+			userExtClients[extClient.IngressGatewayID] = []models.ExtClient{}
+		}
+
+		userExtClients[extClient.IngressGatewayID] = append(userExtClients[extClient.IngressGatewayID], extClient)
+	}
+
+	for ingressGatewayID, extClients := range userExtClients {
+		logic.SortExtClient(extClients)
+
+		node, ok := userGwNodes[ingressGatewayID]
 		if !ok {
 		if !ok {
 			continue
 			continue
 		}
 		}
-		if extClient.RemoteAccessClientID == req.RemoteAccessClientID && extClient.OwnerID == username {
 
 
-			host, err := logic.GetHost(node.HostID.String())
-			if err != nil {
-				continue
-			}
-			network, err := logic.GetNetwork(node.Network)
-			if err != nil {
-				slog.Error("failed to get node network", "error", err)
-				continue
+		var gwClient models.ExtClient
+		var found bool
+		if deviceID != "" {
+			for _, extClient := range extClients {
+				if extClient.DeviceID == deviceID {
+					gwClient = extClient
+					found = true
+					break
+				}
 			}
 			}
-			nodesWithStatus := logic.AddStatusToNodes([]models.Node{node}, false)
-			if len(nodesWithStatus) > 0 {
-				node = nodesWithStatus[0]
+		}
+
+		if !found {
+			// TODO: prevent ip clashes.
+			if len(extClients) > 0 {
+				gwClient = extClients[0]
 			}
 			}
+		}
 
 
-			gws := userGws[node.Network]
+		host, err := logic.GetHost(node.HostID.String())
+		if err != nil {
+			continue
+		}
+		network, err := logic.GetNetwork(node.Network)
+		if err != nil {
+			slog.Error("failed to get node network", "error", err)
+			continue
+		}
+		nodesWithStatus := logic.AddStatusToNodes([]models.Node{node}, false)
+		if len(nodesWithStatus) > 0 {
+			node = nodesWithStatus[0]
+		}
 
 
-			logic.SetDNSOnWgConfig(&node, &extClient)
+		gws := userGws[node.Network]
+		if gwClient.DNS == "" {
+			gwClient.DNS = node.IngressDNS
+		}
 
 
-			extClient.IngressGatewayEndpoint = utils.GetExtClientEndpoint(
-				host.EndpointIP,
-				host.EndpointIPv6,
-				logic.GetPeerListenPort(host),
-			)
-			extClient.AllowedIPs = logic.GetExtclientAllowedIPs(extClient)
-			gw := models.UserRemoteGws{
-				GwID:              node.ID.String(),
-				GWName:            host.Name,
-				Network:           node.Network,
-				GwClient:          extClient,
-				Connected:         true,
-				IsInternetGateway: node.IsInternetGateway,
-				GwPeerPublicKey:   host.PublicKey.String(),
-				GwListenPort:      logic.GetPeerListenPort(host),
-				Metadata:          node.Metadata,
-				AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
-				NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
-				Status:            node.Status,
-				DnsAddress:        node.IngressDNS,
-				Addresses:         utils.NoEmptyStringToCsv(node.Address.String(), node.Address6.String()),
-			}
-			if !node.IsInternetGateway {
-				hNs := logic.GetNameserversForNode(&node)
-				for _, nsI := range hNs {
-					gw.MatchDomains = append(gw.MatchDomains, nsI.MatchDomain)
-				}
+		gwClient.IngressGatewayEndpoint = utils.GetExtClientEndpoint(
+			host.EndpointIP,
+			host.EndpointIPv6,
+			logic.GetPeerListenPort(host),
+		)
+		gwClient.AllowedIPs = logic.GetExtclientAllowedIPs(gwClient)
+		gw := models.UserRemoteGws{
+			GwID:              node.ID.String(),
+			GWName:            host.Name,
+			Network:           node.Network,
+			GwClient:          gwClient,
+			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},
+			Status:            node.Status,
+			DnsAddress:        node.IngressDNS,
+			Addresses:         utils.NoEmptyStringToCsv(node.Address.String(), node.Address6.String()),
+		}
+		if !node.IsInternetGateway {
+			hNs := logic.GetNameserversForNode(&node)
+			for _, nsI := range hNs {
+				gw.MatchDomains = append(gw.MatchDomains, nsI.MatchDomain)
 			}
 			}
-			gws = append(gws, gw)
-			userGws[node.Network] = gws
-			delete(userGwNodes, node.ID.String())
 		}
 		}
+		gw.MatchDomains = append(gw.MatchDomains, logic.GetEgressDomainsByAccess(user, models.NetworkID(node.Network))...)
+		gws = append(gws, gw)
+		userGws[node.Network] = gws
+		delete(userGwNodes, node.ID.String())
 	}
 	}
+
 	// add remaining gw nodes to resp
 	// add remaining gw nodes to resp
 	for gwID := range userGwNodes {
 	for gwID := range userGwNodes {
 		node, err := logic.GetNodeByID(gwID)
 		node, err := logic.GetNodeByID(gwID)
@@ -1358,7 +1611,6 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 			slog.Error("failed to get node network", "error", err)
 			slog.Error("failed to get node network", "error", err)
 		}
 		}
 		gws := userGws[node.Network]
 		gws := userGws[node.Network]
-
 		gw := models.UserRemoteGws{
 		gw := models.UserRemoteGws{
 			GwID:              node.ID.String(),
 			GwID:              node.ID.String(),
 			GWName:            host.Name,
 			GWName:            host.Name,
@@ -1379,6 +1631,7 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 				gw.MatchDomains = append(gw.MatchDomains, nsI.MatchDomain)
 				gw.MatchDomains = append(gw.MatchDomains, nsI.MatchDomain)
 			}
 			}
 		}
 		}
+		gw.MatchDomains = append(gw.MatchDomains, logic.GetEgressDomainsByAccess(user, models.NetworkID(node.Network))...)
 		gws = append(gws, gw)
 		gws = append(gws, gw)
 		userGws[node.Network] = gws
 		userGws[node.Network] = gws
 	}
 	}
@@ -1627,6 +1880,60 @@ func syncIDP(w http.ResponseWriter, r *http.Request) {
 	logic.ReturnSuccessResponse(w, r, "starting sync from idp")
 	logic.ReturnSuccessResponse(w, r, "starting sync from idp")
 }
 }
 
 
+// @Summary     Test IDP Sync Credentials.
+// @Router      /api/idp/sync/test [post]
+// @Tags        IDP
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+func testIDPSync(w http.ResponseWriter, r *http.Request) {
+	var req models.IDPSyncTestRequest
+	err := json.NewDecoder(r.Body).Decode(&req)
+	if err != nil {
+		err = fmt.Errorf("failed to decode request body: %v", err)
+		logger.Log(0, err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	var idpClient idp.Client
+	switch req.AuthProvider {
+	case "google":
+		idpClient, err = google.NewGoogleWorkspaceClient(req.GoogleAdminEmail, req.GoogleSACredsJson)
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+			return
+		}
+	case "azure-ad":
+		idpClient = azure.NewAzureEntraIDClient(req.ClientID, req.ClientSecret, req.AzureTenantID)
+	case "okta":
+		idpClient, err = okta.NewOktaClient(req.OktaOrgURL, req.OktaAPIToken)
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+			return
+		}
+	default:
+		err = fmt.Errorf("invalid auth provider: %s", req.AuthProvider)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	err = idpClient.Verify()
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	logic.ReturnSuccessResponse(w, r, "idp sync test successful")
+}
+
+// @Summary     Gets idp sync status.
+// @Router      /api/idp/sync/status [get]
+// @Tags        IDP
+// @Success     200 {object} models.SuccessResponse
+func getIDPSyncStatus(w http.ResponseWriter, r *http.Request) {
+	logic.ReturnSuccessResponseWithJson(w, r, proAuth.GetIDPSyncStatus(), "idp sync status retrieved")
+}
+
 // @Summary     Remove idp integration.
 // @Summary     Remove idp integration.
 // @Router      /api/idp [delete]
 // @Router      /api/idp [delete]
 // @Tags        IDP
 // @Tags        IDP
@@ -1644,11 +1951,10 @@ func removeIDPIntegration(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 
 
 	if superAdmin.AuthType == models.OAuth {
 	if superAdmin.AuthType == models.OAuth {
-		logic.ReturnErrorResponse(
-			w,
-			r,
-			logic.FormatError(fmt.Errorf("cannot remove idp integration with superadmin oauth user"), "badrequest"),
+		err := fmt.Errorf(
+			"cannot remove IdP integration because an OAuth user has the super-admin role; transfer the super-admin role to another user first",
 		)
 		)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 		return
 	}
 	}
 
 

+ 88 - 10
pro/idp/azure/azure.go

@@ -17,14 +17,80 @@ type Client struct {
 	tenantID     string
 	tenantID     string
 }
 }
 
 
-func NewAzureEntraIDClient() *Client {
+func NewAzureEntraIDClient(clientID, clientSecret, tenantID string) *Client {
+	return &Client{
+		clientID:     clientID,
+		clientSecret: clientSecret,
+		tenantID:     tenantID,
+	}
+}
+
+func NewAzureEntraIDClientFromSettings() *Client {
 	settings := logic.GetServerSettings()
 	settings := logic.GetServerSettings()
 
 
-	return &Client{
-		clientID:     settings.ClientID,
-		clientSecret: settings.ClientSecret,
-		tenantID:     settings.AzureTenant,
+	return NewAzureEntraIDClient(settings.ClientID, settings.ClientSecret, settings.AzureTenant)
+}
+
+func (a *Client) Verify() error {
+	accessToken, err := a.getAccessToken()
+	if err != nil {
+		return err
+	}
+
+	client := &http.Client{}
+	req, err := http.NewRequest("GET", "https://graph.microsoft.com/v1.0/users?$select=id,userPrincipalName,displayName,accountEnabled&$top=1", nil)
+	if err != nil {
+		return err
+	}
+
+	req.Header.Add("Authorization", "Bearer "+accessToken)
+	req.Header.Add("Accept", "application/json")
+
+	resp, err := client.Do(req)
+	if err != nil {
+		return err
 	}
 	}
+	defer func() {
+		_ = resp.Body.Close()
+	}()
+
+	var users getUsersResponse
+	err = json.NewDecoder(resp.Body).Decode(&users)
+	if err != nil {
+		return err
+	}
+
+	if users.Error.Code != "" {
+		return errors.New(users.Error.Message)
+	}
+
+	req, err = http.NewRequest("GET", "https://graph.microsoft.com/v1.0/groups?$select=id,displayName&$expand=members($select=id)&$top=1", nil)
+	if err != nil {
+		return err
+	}
+
+	req.Header.Add("Authorization", "Bearer "+accessToken)
+	req.Header.Add("Accept", "application/json")
+
+	resp, err = client.Do(req)
+	if err != nil {
+		return err
+	}
+	defer func() {
+		_ = resp.Body.Close()
+	}()
+
+	var groups getGroupsResponse
+	err = json.NewDecoder(resp.Body).Decode(&groups)
+	if err != nil {
+		return err
+	}
+
+	if groups.Error.Code != "" {
+		return errors.New(groups.Error.Message)
+	}
+
+	return nil
 }
 }
 
 
 func (a *Client) GetUsers(filters []string) ([]idp.User, error) {
 func (a *Client) GetUsers(filters []string) ([]idp.User, error) {
@@ -140,7 +206,7 @@ func (a *Client) getAccessToken() (string, error) {
 
 
 	resp, err := http.PostForm(tokenURL, data)
 	resp, err := http.PostForm(tokenURL, data)
 	if err != nil {
 	if err != nil {
-		return "", err
+		return "", errors.New("invalid credentials")
 	}
 	}
 	defer func() {
 	defer func() {
 		_ = resp.Body.Close()
 		_ = resp.Body.Close()
@@ -149,14 +215,14 @@ func (a *Client) getAccessToken() (string, error) {
 	var tokenResp map[string]interface{}
 	var tokenResp map[string]interface{}
 	err = json.NewDecoder(resp.Body).Decode(&tokenResp)
 	err = json.NewDecoder(resp.Body).Decode(&tokenResp)
 	if err != nil {
 	if err != nil {
-		return "", err
+		return "", errors.New("invalid credentials")
 	}
 	}
 
 
 	if token, ok := tokenResp["access_token"].(string); ok {
 	if token, ok := tokenResp["access_token"].(string); ok {
 		return token, nil
 		return token, nil
 	}
 	}
 
 
-	return "", errors.New("failed to get access token")
+	return "", errors.New("invalid credentials")
 }
 }
 
 
 func buildPrefixFilter(field string, prefixes []string) string {
 func buildPrefixFilter(field string, prefixes []string) string {
@@ -172,7 +238,8 @@ func buildPrefixFilter(field string, prefixes []string) string {
 }
 }
 
 
 type getUsersResponse struct {
 type getUsersResponse struct {
-	OdataContext string `json:"@odata.context"`
+	Error        errorResponse `json:"error"`
+	OdataContext string        `json:"@odata.context"`
 	Value        []struct {
 	Value        []struct {
 		Id                string `json:"id"`
 		Id                string `json:"id"`
 		UserPrincipalName string `json:"userPrincipalName"`
 		UserPrincipalName string `json:"userPrincipalName"`
@@ -183,7 +250,8 @@ type getUsersResponse struct {
 }
 }
 
 
 type getGroupsResponse struct {
 type getGroupsResponse struct {
-	OdataContext string `json:"@odata.context"`
+	Error        errorResponse `json:"error"`
+	OdataContext string        `json:"@odata.context"`
 	Value        []struct {
 	Value        []struct {
 		Id          string `json:"id"`
 		Id          string `json:"id"`
 		DisplayName string `json:"displayName"`
 		DisplayName string `json:"displayName"`
@@ -194,3 +262,13 @@ type getGroupsResponse struct {
 	} `json:"value"`
 	} `json:"value"`
 	NextLink string `json:"@odata.nextLink"`
 	NextLink string `json:"@odata.nextLink"`
 }
 }
+
+type errorResponse struct {
+	Code       string `json:"code"`
+	Message    string `json:"message"`
+	InnerError struct {
+		Date            string `json:"date"`
+		RequestId       string `json:"request-id"`
+		ClientRequestId string `json:"client-request-id"`
+	} `json:"innerError"`
+}

+ 76 - 6
pro/idp/google/google.go

@@ -4,11 +4,15 @@ import (
 	"context"
 	"context"
 	"encoding/base64"
 	"encoding/base64"
 	"encoding/json"
 	"encoding/json"
+	"errors"
 	"strings"
 	"strings"
 
 
+	"net/url"
+
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/pro/idp"
 	"github.com/gravitl/netmaker/pro/idp"
 	admindir "google.golang.org/api/admin/directory/v1"
 	admindir "google.golang.org/api/admin/directory/v1"
+	"google.golang.org/api/googleapi"
 	"google.golang.org/api/impersonate"
 	"google.golang.org/api/impersonate"
 	"google.golang.org/api/option"
 	"google.golang.org/api/option"
 )
 )
@@ -17,10 +21,8 @@ type Client struct {
 	service *admindir.Service
 	service *admindir.Service
 }
 }
 
 
-func NewGoogleWorkspaceClient() (*Client, error) {
-	settings := logic.GetServerSettings()
-
-	credsJson, err := base64.StdEncoding.DecodeString(settings.GoogleSACredsJson)
+func NewGoogleWorkspaceClient(adminEmail, creds string) (*Client, error) {
+	credsJson, err := base64.StdEncoding.DecodeString(creds)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -31,16 +33,24 @@ func NewGoogleWorkspaceClient() (*Client, error) {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
+	var targetPrincipal string
+	_, ok := credsJsonMap["client_email"]
+	if !ok {
+		return nil, errors.New("invalid service account credentials: missing client_email field")
+	} else {
+		targetPrincipal = credsJsonMap["client_email"].(string)
+	}
+
 	source, err := impersonate.CredentialsTokenSource(
 	source, err := impersonate.CredentialsTokenSource(
 		context.TODO(),
 		context.TODO(),
 		impersonate.CredentialsConfig{
 		impersonate.CredentialsConfig{
-			TargetPrincipal: credsJsonMap["client_email"].(string),
+			TargetPrincipal: targetPrincipal,
 			Scopes: []string{
 			Scopes: []string{
 				admindir.AdminDirectoryUserReadonlyScope,
 				admindir.AdminDirectoryUserReadonlyScope,
 				admindir.AdminDirectoryGroupReadonlyScope,
 				admindir.AdminDirectoryGroupReadonlyScope,
 				admindir.AdminDirectoryGroupMemberReadonlyScope,
 				admindir.AdminDirectoryGroupMemberReadonlyScope,
 			},
 			},
-			Subject: settings.GoogleAdminEmail,
+			Subject: adminEmail,
 		},
 		},
 		option.WithCredentialsJSON(credsJson),
 		option.WithCredentialsJSON(credsJson),
 	)
 	)
@@ -61,6 +71,58 @@ func NewGoogleWorkspaceClient() (*Client, error) {
 	}, nil
 	}, nil
 }
 }
 
 
+func NewGoogleWorkspaceClientFromSettings() (*Client, error) {
+	settings := logic.GetServerSettings()
+
+	return NewGoogleWorkspaceClient(settings.GoogleAdminEmail, settings.GoogleSACredsJson)
+}
+
+func (g *Client) Verify() error {
+	_, err := g.service.Users.List().
+		Customer("my_customer").
+		MaxResults(1).
+		Do()
+	if err != nil {
+		var gerr *googleapi.Error
+		if errors.As(err, &gerr) {
+			return errors.New(gerr.Message)
+		}
+
+		var uerr *url.Error
+		if errors.As(err, &uerr) {
+			errMsg := strings.TrimSpace(uerr.Err.Error())
+			if strings.Contains(errMsg, "{") && strings.HasSuffix(errMsg, "}") {
+				// probably contains response json.
+				_, jsonBody, _ := strings.Cut(errMsg, "{")
+				jsonBody = "{" + jsonBody
+
+				var errResp errorResponse
+				err := json.Unmarshal([]byte(jsonBody), &errResp)
+				if err == nil && errResp.Error.Message != "" {
+					return errors.New(errResp.Error.Message)
+				}
+			}
+		}
+
+		return err
+	}
+
+	_, err = g.service.Groups.List().
+		Customer("my_customer").
+		MaxResults(1).
+		Do()
+	if err != nil {
+		var gerr *googleapi.Error
+		if errors.As(err, &gerr) {
+			return errors.New(gerr.Message)
+		}
+
+		return err
+	}
+
+	return nil
+}
+
 func (g *Client) GetUsers(filters []string) ([]idp.User, error) {
 func (g *Client) GetUsers(filters []string) ([]idp.User, error) {
 	var retval []idp.User
 	var retval []idp.User
 	err := g.service.Users.List().
 	err := g.service.Users.List().
@@ -146,3 +208,11 @@ func (g *Client) GetGroups(filters []string) ([]idp.Group, error) {
 
 
 	return retval, err
 	return retval, err
 }
 }
+
+type errorResponse struct {
+	Error struct {
+		Code    int    `json:"code"`
+		Message string `json:"message"`
+		Status  string `json:"status"`
+	} `json:"error"`
+}

+ 1 - 0
pro/idp/idp.go

@@ -1,6 +1,7 @@
 package idp
 package idp
 
 
 type Client interface {
 type Client interface {
+	Verify() error
 	GetUsers(filters []string) ([]User, error)
 	GetUsers(filters []string) ([]User, error)
 	GetGroups(filters []string) ([]Group, error)
 	GetGroups(filters []string) ([]Group, error)
 }
 }

+ 7 - 6
pro/initialize.go

@@ -35,6 +35,7 @@ func InitPro() {
 		proControllers.RacHandlers,
 		proControllers.RacHandlers,
 		proControllers.EventHandlers,
 		proControllers.EventHandlers,
 		proControllers.TagHandlers,
 		proControllers.TagHandlers,
+		proControllers.NetworkHandlers,
 	)
 	)
 	controller.ListRoles = proControllers.ListRoles
 	controller.ListRoles = proControllers.ListRoles
 	logic.EnterpriseCheckFuncs = append(logic.EnterpriseCheckFuncs, func() {
 	logic.EnterpriseCheckFuncs = append(logic.EnterpriseCheckFuncs, func() {
@@ -131,9 +132,11 @@ func InitPro() {
 	logic.MigrateToUUIDs = proLogic.MigrateToUUIDs
 	logic.MigrateToUUIDs = proLogic.MigrateToUUIDs
 	logic.IntialiseGroups = proLogic.UserGroupsInit
 	logic.IntialiseGroups = proLogic.UserGroupsInit
 	logic.AddGlobalNetRolesToAdmins = proLogic.AddGlobalNetRolesToAdmins
 	logic.AddGlobalNetRolesToAdmins = proLogic.AddGlobalNetRolesToAdmins
+	logic.ListUserGroups = proLogic.ListUserGroups
 	logic.GetUserGroupsInNetwork = proLogic.GetUserGroupsInNetwork
 	logic.GetUserGroupsInNetwork = proLogic.GetUserGroupsInNetwork
 	logic.GetUserGroup = proLogic.GetUserGroup
 	logic.GetUserGroup = proLogic.GetUserGroup
 	logic.GetNodeStatus = proLogic.GetNodeStatus
 	logic.GetNodeStatus = proLogic.GetNodeStatus
+	logic.IsOAuthConfigured = auth.IsOAuthConfigured
 	logic.ResetAuthProvider = auth.ResetAuthProvider
 	logic.ResetAuthProvider = auth.ResetAuthProvider
 	logic.ResetIDPSyncHook = auth.ResetIDPSyncHook
 	logic.ResetIDPSyncHook = auth.ResetIDPSyncHook
 	logic.EmailInit = email.Init
 	logic.EmailInit = email.Init
@@ -142,19 +145,17 @@ func InitPro() {
 	logic.IsUserAllowedToCommunicate = proLogic.IsUserAllowedToCommunicate
 	logic.IsUserAllowedToCommunicate = proLogic.IsUserAllowedToCommunicate
 	logic.DeleteAllNetworkTags = proLogic.DeleteAllNetworkTags
 	logic.DeleteAllNetworkTags = proLogic.DeleteAllNetworkTags
 	logic.CreateDefaultTags = proLogic.CreateDefaultTags
 	logic.CreateDefaultTags = proLogic.CreateDefaultTags
-	logic.GetInetClientsFromAclPolicies = proLogic.GetInetClientsFromAclPolicies
 	logic.IsPeerAllowed = proLogic.IsPeerAllowed
 	logic.IsPeerAllowed = proLogic.IsPeerAllowed
 	logic.IsAclPolicyValid = proLogic.IsAclPolicyValid
 	logic.IsAclPolicyValid = proLogic.IsAclPolicyValid
-	logic.GetEgressRulesForNode = proLogic.GetEgressRulesForNode
-	logic.GetAclRuleForInetGw = proLogic.GetAclRuleForInetGw
-	logic.GetAclRulesForNode = proLogic.GetAclRulesForNode
-	logic.CheckIfAnyActiveEgressPolicy = proLogic.CheckIfAnyActiveEgressPolicy
+	logic.GetEgressUserRulesForNode = proLogic.GetEgressUserRulesForNode
+	logic.GetTagMapWithNodesByNetwork = proLogic.GetTagMapWithNodesByNetwork
+	logic.GetUserAclRulesForNode = proLogic.GetUserAclRulesForNode
 	logic.CheckIfAnyPolicyisUniDirectional = proLogic.CheckIfAnyPolicyisUniDirectional
 	logic.CheckIfAnyPolicyisUniDirectional = proLogic.CheckIfAnyPolicyisUniDirectional
 	logic.MigrateToGws = proLogic.MigrateToGws
 	logic.MigrateToGws = proLogic.MigrateToGws
-	logic.IsNodeAllowedToCommunicate = proLogic.IsNodeAllowedToCommunicate
 	logic.GetFwRulesForNodeAndPeerOnGw = proLogic.GetFwRulesForNodeAndPeerOnGw
 	logic.GetFwRulesForNodeAndPeerOnGw = proLogic.GetFwRulesForNodeAndPeerOnGw
 	logic.GetFwRulesForUserNodesOnGw = proLogic.GetFwRulesForUserNodesOnGw
 	logic.GetFwRulesForUserNodesOnGw = proLogic.GetFwRulesForUserNodesOnGw
 	logic.GetHostLocInfo = proLogic.GetHostLocInfo
 	logic.GetHostLocInfo = proLogic.GetHostLocInfo
+	logic.GetFeatureFlags = proLogic.GetFeatureFlags
 	logic.GetNameserversForHost = proLogic.GetNameserversForHost
 	logic.GetNameserversForHost = proLogic.GetNameserversForHost
 	logic.GetNameserversForNode = proLogic.GetNameserversForNode
 	logic.GetNameserversForNode = proLogic.GetNameserversForNode
 	logic.ValidateNameserverReq = proLogic.ValidateNameserverReq
 	logic.ValidateNameserverReq = proLogic.ValidateNameserverReq

+ 3 - 0
pro/license.go

@@ -135,6 +135,8 @@ func ValidateLicense() (err error) {
 		return err
 		return err
 	}
 	}
 
 
+	proLogic.SetFeatureFlags(licenseResponse.FeatureFlags)
+
 	slog.Info("License validation succeeded!")
 	slog.Info("License validation succeeded!")
 	return nil
 	return nil
 }
 }
@@ -200,6 +202,7 @@ func validateLicenseKey(encryptedData []byte, publicKey *[32]byte) ([]byte, bool
 		LicenseKey:     servercfg.GetLicenseKey(),
 		LicenseKey:     servercfg.GetLicenseKey(),
 		NmServerPubKey: base64encode(publicKeyBytes),
 		NmServerPubKey: base64encode(publicKeyBytes),
 		EncryptedPart:  base64encode(encryptedData),
 		EncryptedPart:  base64encode(encryptedData),
+		NmBaseDomain:   servercfg.GetNmBaseDomain(),
 	}
 	}
 
 
 	requestBody, err := json.Marshal(msg)
 	requestBody, err := json.Marshal(msg)

+ 338 - 796
pro/logic/acls.go

@@ -3,7 +3,6 @@ package logic
 import (
 import (
 	"context"
 	"context"
 	"errors"
 	"errors"
-	"fmt"
 	"maps"
 	"maps"
 	"net"
 	"net"
 
 
@@ -13,10 +12,29 @@ import (
 	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/schema"
 )
 )
 
 
+func getStaticUserNodesByNetwork(network models.NetworkID) (staticNode []models.Node) {
+	extClients, err := logic.GetAllExtClients()
+	if err != nil {
+		return
+	}
+	for _, extI := range extClients {
+		if extI.Network == network.String() {
+			if extI.RemoteAccessClientID != "" {
+				n := extI.ConvertToStaticNode()
+				staticNode = append(staticNode, n)
+			}
+		}
+	}
+	return
+}
+
 func GetFwRulesForUserNodesOnGw(node models.Node, nodes []models.Node) (rules []models.FwRule) {
 func GetFwRulesForUserNodesOnGw(node models.Node, nodes []models.Node) (rules []models.FwRule) {
 	defaultUserPolicy, _ := logic.GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy)
 	defaultUserPolicy, _ := logic.GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy)
-	userNodes := logic.GetStaticUserNodesByNetwork(models.NetworkID(node.Network))
+	userNodes := getStaticUserNodesByNetwork(models.NetworkID(node.Network))
 	for _, userNodeI := range userNodes {
 	for _, userNodeI := range userNodes {
+		if !userNodeI.StaticNode.Enabled {
+			continue
+		}
 		if defaultUserPolicy.Enabled {
 		if defaultUserPolicy.Enabled {
 			if userNodeI.StaticNode.Address != "" {
 			if userNodeI.StaticNode.Address != "" {
 				rules = append(rules, models.FwRule{
 				rules = append(rules, models.FwRule{
@@ -92,28 +110,56 @@ func GetFwRulesForUserNodesOnGw(node models.Node, nodes []models.Node) (rules []
 							if err != nil {
 							if err != nil {
 								continue
 								continue
 							}
 							}
-							dstI.Value = e.Range
-
-							ip, cidr, err := net.ParseCIDR(dstI.Value)
-							if err == nil {
-								if ip.To4() != nil && userNodeI.StaticNode.Address != "" {
-									rules = append(rules, models.FwRule{
-										SrcIP:           userNodeI.StaticNode.AddressIPNet4(),
-										DstIP:           *cidr,
-										AllowedProtocol: policy.Proto,
-										AllowedPorts:    policy.Port,
-										Allow:           true,
-									})
-								} else if ip.To16() != nil && userNodeI.StaticNode.Address6 != "" {
-									rules = append(rules, models.FwRule{
-										SrcIP:           userNodeI.StaticNode.AddressIPNet6(),
-										DstIP:           *cidr,
-										AllowedProtocol: policy.Proto,
-										AllowedPorts:    policy.Port,
-										Allow:           true,
-									})
+							if e.Range != "" {
+								dstI.Value = e.Range
+
+								ip, cidr, err := net.ParseCIDR(dstI.Value)
+								if err == nil {
+									if ip.To4() != nil && userNodeI.StaticNode.Address != "" {
+										rules = append(rules, models.FwRule{
+											SrcIP:           userNodeI.StaticNode.AddressIPNet4(),
+											DstIP:           *cidr,
+											AllowedProtocol: policy.Proto,
+											AllowedPorts:    policy.Port,
+											Allow:           true,
+										})
+									} else if ip.To16() != nil && userNodeI.StaticNode.Address6 != "" {
+										rules = append(rules, models.FwRule{
+											SrcIP:           userNodeI.StaticNode.AddressIPNet6(),
+											DstIP:           *cidr,
+											AllowedProtocol: policy.Proto,
+											AllowedPorts:    policy.Port,
+											Allow:           true,
+										})
+									}
+								}
+							} else if len(e.DomainAns) > 0 {
+								for _, domainAns := range e.DomainAns {
+									dstI.Value = domainAns
+
+									ip, cidr, err := net.ParseCIDR(dstI.Value)
+									if err == nil {
+										if ip.To4() != nil && userNodeI.StaticNode.Address != "" {
+											rules = append(rules, models.FwRule{
+												SrcIP:           userNodeI.StaticNode.AddressIPNet4(),
+												DstIP:           *cidr,
+												AllowedProtocol: policy.Proto,
+												AllowedPorts:    policy.Port,
+												Allow:           true,
+											})
+										} else if ip.To16() != nil && userNodeI.StaticNode.Address6 != "" {
+											rules = append(rules, models.FwRule{
+												SrcIP:           userNodeI.StaticNode.AddressIPNet6(),
+												DstIP:           *cidr,
+												AllowedProtocol: policy.Proto,
+												AllowedPorts:    policy.Port,
+												Allow:           true,
+											})
+										}
+									}
 								}
 								}
 							}
 							}
+
 						}
 						}
 					}
 					}
 
 
@@ -261,39 +307,78 @@ func GetFwRulesForNodeAndPeerOnGw(node, peer models.Node, allowedPolicies []mode
 				if err != nil {
 				if err != nil {
 					continue
 					continue
 				}
 				}
-				dstI.Value = e.Range
+				if e.Range != "" {
+					dstI.Value = e.Range
 
 
-				ip, cidr, err := net.ParseCIDR(dstI.Value)
-				if err == nil {
-					if ip.To4() != nil {
-						if node.Address.IP != nil {
-							rules = append(rules, models.FwRule{
-								SrcIP: net.IPNet{
-									IP:   node.Address.IP,
-									Mask: net.CIDRMask(32, 32),
-								},
-								DstIP:           *cidr,
-								AllowedProtocol: policy.Proto,
-								AllowedPorts:    policy.Port,
-								Allow:           true,
-							})
-						}
-					} else {
-						if node.Address6.IP != nil {
-							rules = append(rules, models.FwRule{
-								SrcIP: net.IPNet{
-									IP:   node.Address6.IP,
-									Mask: net.CIDRMask(128, 128),
-								},
-								DstIP:           *cidr,
-								AllowedProtocol: policy.Proto,
-								AllowedPorts:    policy.Port,
-								Allow:           true,
-							})
+					ip, cidr, err := net.ParseCIDR(dstI.Value)
+					if err == nil {
+						if ip.To4() != nil {
+							if node.Address.IP != nil {
+								rules = append(rules, models.FwRule{
+									SrcIP: net.IPNet{
+										IP:   node.Address.IP,
+										Mask: net.CIDRMask(32, 32),
+									},
+									DstIP:           *cidr,
+									AllowedProtocol: policy.Proto,
+									AllowedPorts:    policy.Port,
+									Allow:           true,
+								})
+							}
+						} else {
+							if node.Address6.IP != nil {
+								rules = append(rules, models.FwRule{
+									SrcIP: net.IPNet{
+										IP:   node.Address6.IP,
+										Mask: net.CIDRMask(128, 128),
+									},
+									DstIP:           *cidr,
+									AllowedProtocol: policy.Proto,
+									AllowedPorts:    policy.Port,
+									Allow:           true,
+								})
+							}
 						}
 						}
+
 					}
 					}
+				} else if len(e.DomainAns) > 0 {
+					for _, domainAnsI := range e.DomainAns {
+						dstI.Value = domainAnsI
+
+						ip, cidr, err := net.ParseCIDR(dstI.Value)
+						if err == nil {
+							if ip.To4() != nil {
+								if node.Address.IP != nil {
+									rules = append(rules, models.FwRule{
+										SrcIP: net.IPNet{
+											IP:   node.Address.IP,
+											Mask: net.CIDRMask(32, 32),
+										},
+										DstIP:           *cidr,
+										AllowedProtocol: policy.Proto,
+										AllowedPorts:    policy.Port,
+										Allow:           true,
+									})
+								}
+							} else {
+								if node.Address6.IP != nil {
+									rules = append(rules, models.FwRule{
+										SrcIP: net.IPNet{
+											IP:   node.Address6.IP,
+											Mask: net.CIDRMask(128, 128),
+										},
+										DstIP:           *cidr,
+										AllowedProtocol: policy.Proto,
+										AllowedPorts:    policy.Port,
+										Allow:           true,
+									})
+								}
+							}
 
 
+						}
+					}
 				}
 				}
+
 			}
 			}
 		}
 		}
 	}
 	}
@@ -690,188 +775,6 @@ func RemoveUserFromAclPolicy(userName string) {
 	}
 	}
 }
 }
 
 
-// IsNodeAllowedToCommunicate - check node is allowed to communicate with the peer // ADD ALLOWED DIRECTION - 0 => node -> peer, 1 => peer-> node,
-func IsNodeAllowedToCommunicate(node, peer models.Node, checkDefaultPolicy bool) (bool, []models.Acl) {
-	var nodeId, peerId string
-	// if peer.IsFailOver && node.FailedOverBy != uuid.Nil && node.FailedOverBy == peer.ID {
-	// 	return true, []models.Acl{}
-	// }
-	// if node.IsFailOver && peer.FailedOverBy != uuid.Nil && peer.FailedOverBy == node.ID {
-	// 	return true, []models.Acl{}
-	// }
-	// if node.IsGw && peer.IsRelayed && peer.RelayedBy == node.ID.String() {
-	// 	return true, []models.Acl{}
-	// }
-	// if peer.IsGw && node.IsRelayed && node.RelayedBy == peer.ID.String() {
-	// 	return true, []models.Acl{}
-	// }
-	if node.IsStatic {
-		nodeId = node.StaticNode.ClientID
-		node = node.StaticNode.ConvertToStaticNode()
-	} else {
-		nodeId = node.ID.String()
-	}
-	if peer.IsStatic {
-		peerId = peer.StaticNode.ClientID
-		peer = peer.StaticNode.ConvertToStaticNode()
-	} else {
-		peerId = peer.ID.String()
-	}
-
-	var nodeTags, peerTags map[models.TagID]struct{}
-	if node.Mutex != nil {
-		node.Mutex.Lock()
-		nodeTags = maps.Clone(node.Tags)
-		node.Mutex.Unlock()
-	} else {
-		nodeTags = node.Tags
-	}
-	if peer.Mutex != nil {
-		peer.Mutex.Lock()
-		peerTags = maps.Clone(peer.Tags)
-		peer.Mutex.Unlock()
-	} else {
-		peerTags = peer.Tags
-	}
-	if nodeTags == nil {
-		nodeTags = make(map[models.TagID]struct{})
-	}
-	if peerTags == nil {
-		peerTags = make(map[models.TagID]struct{})
-	}
-	nodeTags[models.TagID(nodeId)] = struct{}{}
-	peerTags[models.TagID(peerId)] = struct{}{}
-	if checkDefaultPolicy {
-		// check default policy if all allowed return true
-		defaultPolicy, err := logic.GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
-		if err == nil {
-			if defaultPolicy.Enabled {
-				return true, []models.Acl{defaultPolicy}
-			}
-		}
-	}
-	allowedPolicies := []models.Acl{}
-	defer func() {
-		allowedPolicies = logic.UniquePolicies(allowedPolicies)
-	}()
-	// list device policies
-	policies := logic.ListDevicePolicies(models.NetworkID(peer.Network))
-	srcMap := make(map[string]struct{})
-	dstMap := make(map[string]struct{})
-	defer func() {
-		srcMap = nil
-		dstMap = nil
-	}()
-	for _, policy := range policies {
-		if !policy.Enabled {
-			continue
-		}
-		allowed := false
-		srcMap = logic.ConvAclTagToValueMap(policy.Src)
-		dstMap = logic.ConvAclTagToValueMap(policy.Dst)
-		for _, dst := range policy.Dst {
-			if dst.ID == models.EgressID {
-				e := schema.Egress{ID: dst.Value}
-				err := e.Get(db.WithContext(context.TODO()))
-				if err == nil && e.Status {
-					for nodeID := range e.Nodes {
-						dstMap[nodeID] = struct{}{}
-					}
-				}
-			}
-		}
-		_, srcAll := srcMap["*"]
-		_, dstAll := dstMap["*"]
-		if policy.AllowedDirection == models.TrafficDirectionBi {
-			if _, ok := srcMap[nodeId]; ok || srcAll {
-				if _, ok := dstMap[peerId]; ok || dstAll {
-					allowedPolicies = append(allowedPolicies, policy)
-					continue
-				}
-
-			}
-			if _, ok := dstMap[nodeId]; ok || dstAll {
-				if _, ok := srcMap[peerId]; ok || srcAll {
-					allowedPolicies = append(allowedPolicies, policy)
-					continue
-				}
-			}
-		}
-		if _, ok := dstMap[peerId]; ok || dstAll {
-			if _, ok := srcMap[nodeId]; ok || srcAll {
-				allowedPolicies = append(allowedPolicies, policy)
-				continue
-			}
-		}
-		if policy.AllowedDirection == models.TrafficDirectionBi {
-
-			for tagID := range nodeTags {
-
-				if _, ok := dstMap[tagID.String()]; ok || dstAll {
-					if srcAll {
-						allowed = true
-						break
-					}
-					for tagID := range peerTags {
-						if _, ok := srcMap[tagID.String()]; ok {
-							allowed = true
-							break
-						}
-					}
-				}
-				if allowed {
-					allowedPolicies = append(allowedPolicies, policy)
-					break
-				}
-				if _, ok := srcMap[tagID.String()]; ok || srcAll {
-					if dstAll {
-						allowed = true
-						break
-					}
-					for tagID := range peerTags {
-						if _, ok := dstMap[tagID.String()]; ok {
-							allowed = true
-							break
-						}
-					}
-				}
-				if allowed {
-					break
-				}
-			}
-			if allowed {
-				allowedPolicies = append(allowedPolicies, policy)
-				continue
-			}
-		}
-		for tagID := range peerTags {
-			if _, ok := dstMap[tagID.String()]; ok || dstAll {
-				if srcAll {
-					allowed = true
-					break
-				}
-				for tagID := range nodeTags {
-					if _, ok := srcMap[tagID.String()]; ok {
-						allowed = true
-						break
-					}
-				}
-			}
-			if allowed {
-				break
-			}
-		}
-		if allowed {
-			allowedPolicies = append(allowedPolicies, policy)
-		}
-	}
-
-	if len(allowedPolicies) > 0 {
-		return true, allowedPolicies
-	}
-	return false, allowedPolicies
-}
-
 // UpdateDeviceTag - updates device tag on acl policies
 // UpdateDeviceTag - updates device tag on acl policies
 func UpdateDeviceTag(OldID, newID models.TagID, netID models.NetworkID) {
 func UpdateDeviceTag(OldID, newID models.TagID, netID models.NetworkID) {
 	acls := logic.ListDevicePolicies(netID)
 	acls := logic.ListDevicePolicies(netID)
@@ -948,9 +851,9 @@ func RemoveDeviceTagFromAclPolicies(tagID models.TagID, netID models.NetworkID)
 	return nil
 	return nil
 }
 }
 
 
-func getEgressUserRulesForNode(targetnode *models.Node,
+func GetEgressUserRulesForNode(targetnode *models.Node,
 	rules map[string]models.AclRule) map[string]models.AclRule {
 	rules map[string]models.AclRule) map[string]models.AclRule {
-	userNodes := logic.GetStaticUserNodesByNetwork(models.NetworkID(targetnode.Network))
+	userNodes := getStaticUserNodesByNetwork(models.NetworkID(targetnode.Network))
 	userGrpMap := GetUserGrpMap()
 	userGrpMap := GetUserGrpMap()
 	allowedUsers := make(map[string][]models.Acl)
 	allowedUsers := make(map[string][]models.Acl)
 	acls := listUserPolicies(models.NetworkID(targetnode.Network))
 	acls := listUserPolicies(models.NetworkID(targetnode.Network))
@@ -967,7 +870,14 @@ func getEgressUserRulesForNode(targetnode *models.Node,
 			continue
 			continue
 		}
 		}
 		if _, ok := egI.Nodes[targetnode.ID.String()]; ok {
 		if _, ok := egI.Nodes[targetnode.ID.String()]; ok {
-			targetNodeTags[models.TagID(egI.Range)] = struct{}{}
+			if egI.Range != "" {
+				targetNodeTags[models.TagID(egI.Range)] = struct{}{}
+			} else if len(egI.DomainAns) > 0 {
+				for _, domainAnsI := range egI.DomainAns {
+					targetNodeTags[models.TagID(domainAnsI)] = struct{}{}
+				}
+			}
+
 			targetNodeTags[models.TagID(egI.ID)] = struct{}{}
 			targetNodeTags[models.TagID(egI.ID)] = struct{}{}
 		}
 		}
 	}
 	}
@@ -985,7 +895,14 @@ func getEgressUserRulesForNode(targetnode *models.Node,
 						for nodeID := range e.Nodes {
 						for nodeID := range e.Nodes {
 							dstTags[nodeID] = struct{}{}
 							dstTags[nodeID] = struct{}{}
 						}
 						}
-						dstTags[e.Range] = struct{}{}
+						if e.Range != "" {
+							dstTags[e.Range] = struct{}{}
+						} else if len(e.DomainAns) > 0 {
+							for _, domainAnsI := range e.DomainAns {
+								dstTags[domainAnsI] = struct{}{}
+							}
+						}
+
 					}
 					}
 				}
 				}
 			}
 			}
@@ -1079,25 +996,57 @@ func getEgressUserRulesForNode(targetnode *models.Node,
 						if err != nil {
 						if err != nil {
 							continue
 							continue
 						}
 						}
+						if e.Range != "" {
+							ip, cidr, err := net.ParseCIDR(e.Range)
+							if err == nil {
+								if ip.To4() != nil {
+									r.Dst = append(r.Dst, *cidr)
+								} else {
+									r.Dst6 = append(r.Dst6, *cidr)
+								}
 
 
-						ip, cidr, err := net.ParseCIDR(e.Range)
-						if err == nil {
-							if ip.To4() != nil {
-								r.Dst = append(r.Dst, *cidr)
-							} else {
-								r.Dst6 = append(r.Dst6, *cidr)
 							}
 							}
+						} else if len(e.DomainAns) > 0 {
+							for _, domainAnsI := range e.DomainAns {
+								ip, cidr, err := net.ParseCIDR(domainAnsI)
+								if err == nil {
+									if ip.To4() != nil {
+										r.Dst = append(r.Dst, *cidr)
+									} else {
+										r.Dst6 = append(r.Dst6, *cidr)
+									}
 
 
+								}
+							}
 						}
 						}
 
 
 					}
 					}
 
 
 				}
 				}
+				if userNode.StaticNode.Address6 != "" {
+					r.IP6List = append(r.IP6List, userNode.StaticNode.AddressIPNet6())
+				}
 				if aclRule, ok := rules[acl.ID]; ok {
 				if aclRule, ok := rules[acl.ID]; ok {
+
 					aclRule.IPList = append(aclRule.IPList, r.IPList...)
 					aclRule.IPList = append(aclRule.IPList, r.IPList...)
 					aclRule.IP6List = append(aclRule.IP6List, r.IP6List...)
 					aclRule.IP6List = append(aclRule.IP6List, r.IP6List...)
+
+					aclRule.Dst = append(aclRule.Dst, r.Dst...)
+					aclRule.Dst6 = append(aclRule.Dst6, r.Dst6...)
+
+					aclRule.IPList = logic.UniqueIPNetList(aclRule.IPList)
+					aclRule.IP6List = logic.UniqueIPNetList(aclRule.IP6List)
+
+					aclRule.Dst = logic.UniqueIPNetList(aclRule.Dst)
+					aclRule.Dst6 = logic.UniqueIPNetList(aclRule.Dst6)
+
 					rules[acl.ID] = aclRule
 					rules[acl.ID] = aclRule
 				} else {
 				} else {
+					r.IPList = logic.UniqueIPNetList(r.IPList)
+					r.IP6List = logic.UniqueIPNetList(r.IP6List)
+
+					r.Dst = logic.UniqueIPNetList(r.Dst)
+					r.Dst6 = logic.UniqueIPNetList(r.Dst6)
 					rules[acl.ID] = r
 					rules[acl.ID] = r
 				}
 				}
 			}
 			}
@@ -1108,9 +1057,9 @@ func getEgressUserRulesForNode(targetnode *models.Node,
 	return rules
 	return rules
 }
 }
 
 
-func getUserAclRulesForNode(targetnode *models.Node,
+func GetUserAclRulesForNode(targetnode *models.Node,
 	rules map[string]models.AclRule) map[string]models.AclRule {
 	rules map[string]models.AclRule) map[string]models.AclRule {
-	userNodes := logic.GetStaticUserNodesByNetwork(models.NetworkID(targetnode.Network))
+	userNodes := getStaticUserNodesByNetwork(models.NetworkID(targetnode.Network))
 	userGrpMap := GetUserGrpMap()
 	userGrpMap := GetUserGrpMap()
 	allowedUsers := make(map[string][]models.Acl)
 	allowedUsers := make(map[string][]models.Acl)
 	acls := listUserPolicies(models.NetworkID(targetnode.Network))
 	acls := listUserPolicies(models.NetworkID(targetnode.Network))
@@ -1136,6 +1085,17 @@ func getUserAclRulesForNode(targetnode *models.Node,
 			_, all := dstTags["*"]
 			_, all := dstTags["*"]
 			addUsers := false
 			addUsers := false
 			if !all {
 			if !all {
+				for _, dst := range acl.Dst {
+					if dst.ID == models.EgressID {
+						e := schema.Egress{ID: dst.Value}
+						err := e.Get(db.WithContext(context.TODO()))
+						if err == nil && e.Status && len(e.Nodes) > 0 {
+							if _, ok := e.Nodes[targetnode.ID.String()]; ok {
+								dstTags[targetnode.ID.String()] = struct{}{}
+							}
+						}
+					}
+				}
 				for nodeTag := range targetNodeTags {
 				for nodeTag := range targetNodeTags {
 					if _, ok := dstTags[nodeTag.String()]; ok {
 					if _, ok := dstTags[nodeTag.String()]; ok {
 						addUsers = true
 						addUsers = true
@@ -1200,13 +1160,91 @@ func getUserAclRulesForNode(targetnode *models.Node,
 				if !acl.Enabled {
 				if !acl.Enabled {
 					continue
 					continue
 				}
 				}
+				egressRanges4 := []net.IPNet{}
+				egressRanges6 := []net.IPNet{}
+
+				for _, dst := range acl.Dst {
+					if dst.Value == "*" {
+						e := schema.Egress{Network: targetnode.Network}
+						eli, _ := e.ListByNetwork(db.WithContext(context.Background()))
+						for _, eI := range eli {
+							if !eI.Status || len(eI.Nodes) == 0 {
+								continue
+							}
+							if _, ok := eI.Nodes[targetnode.ID.String()]; ok {
+								if eI.Range != "" {
+									_, cidr, err := net.ParseCIDR(eI.Range)
+									if err == nil {
+										if cidr.IP.To4() != nil {
+											egressRanges4 = append(egressRanges4, *cidr)
+										} else {
+											egressRanges6 = append(egressRanges6, *cidr)
+										}
+									}
+								} else if len(eI.DomainAns) > 0 {
+									for _, domainAnsI := range eI.DomainAns {
+										_, cidr, err := net.ParseCIDR(domainAnsI)
+										if err == nil {
+											if cidr.IP.To4() != nil {
+												egressRanges4 = append(egressRanges4, *cidr)
+											} else {
+												egressRanges6 = append(egressRanges6, *cidr)
+											}
+										}
+									}
+								}
+
+							}
+						}
+						break
+					}
+					if dst.ID == models.EgressID {
+						e := schema.Egress{ID: dst.Value}
+						err := e.Get(db.WithContext(context.TODO()))
+						if err == nil && e.Status && len(e.Nodes) > 0 {
+							if _, ok := e.Nodes[targetnode.ID.String()]; ok {
+								if e.Range != "" {
+									_, cidr, err := net.ParseCIDR(e.Range)
+									if err == nil {
+										if cidr.IP.To4() != nil {
+											egressRanges4 = append(egressRanges4, *cidr)
+										} else {
+											egressRanges6 = append(egressRanges6, *cidr)
+										}
+									}
+								} else if len(e.DomainAns) > 0 {
+									for _, domainAnsI := range e.DomainAns {
+										_, cidr, err := net.ParseCIDR(domainAnsI)
+										if err == nil {
+											if cidr.IP.To4() != nil {
+												egressRanges4 = append(egressRanges4, *cidr)
+											} else {
+												egressRanges6 = append(egressRanges6, *cidr)
+											}
+										}
+									}
+								}
+							}
+
+						}
+					}
+
+				}
 				r := models.AclRule{
 				r := models.AclRule{
 					ID:              acl.ID,
 					ID:              acl.ID,
 					AllowedProtocol: acl.Proto,
 					AllowedProtocol: acl.Proto,
 					AllowedPorts:    acl.Port,
 					AllowedPorts:    acl.Port,
 					Direction:       acl.AllowedDirection,
 					Direction:       acl.AllowedDirection,
+					Dst:             []net.IPNet{targetnode.AddressIPNet4()},
+					Dst6:            []net.IPNet{targetnode.AddressIPNet6()},
 					Allowed:         true,
 					Allowed:         true,
 				}
 				}
+				if len(egressRanges4) > 0 {
+					r.Dst = append(r.Dst, egressRanges4...)
+				}
+				if len(egressRanges6) > 0 {
+					r.Dst6 = append(r.Dst6, egressRanges6...)
+				}
 				// Get peers in the tags and add allowed rules
 				// Get peers in the tags and add allowed rules
 				if userNode.StaticNode.Address != "" {
 				if userNode.StaticNode.Address != "" {
 					r.IPList = append(r.IPList, userNode.StaticNode.AddressIPNet4())
 					r.IPList = append(r.IPList, userNode.StaticNode.AddressIPNet4())
@@ -1215,14 +1253,26 @@ func getUserAclRulesForNode(targetnode *models.Node,
 					r.IP6List = append(r.IP6List, userNode.StaticNode.AddressIPNet6())
 					r.IP6List = append(r.IP6List, userNode.StaticNode.AddressIPNet6())
 				}
 				}
 				if aclRule, ok := rules[acl.ID]; ok {
 				if aclRule, ok := rules[acl.ID]; ok {
+
 					aclRule.IPList = append(aclRule.IPList, r.IPList...)
 					aclRule.IPList = append(aclRule.IPList, r.IPList...)
 					aclRule.IP6List = append(aclRule.IP6List, r.IP6List...)
 					aclRule.IP6List = append(aclRule.IP6List, r.IP6List...)
+
+					aclRule.Dst = append(aclRule.Dst, r.Dst...)
+					aclRule.Dst6 = append(aclRule.Dst6, r.Dst6...)
+
 					aclRule.IPList = logic.UniqueIPNetList(aclRule.IPList)
 					aclRule.IPList = logic.UniqueIPNetList(aclRule.IPList)
 					aclRule.IP6List = logic.UniqueIPNetList(aclRule.IP6List)
 					aclRule.IP6List = logic.UniqueIPNetList(aclRule.IP6List)
+
+					aclRule.Dst = logic.UniqueIPNetList(aclRule.Dst)
+					aclRule.Dst6 = logic.UniqueIPNetList(aclRule.Dst6)
+
 					rules[acl.ID] = aclRule
 					rules[acl.ID] = aclRule
 				} else {
 				} else {
 					r.IPList = logic.UniqueIPNetList(r.IPList)
 					r.IPList = logic.UniqueIPNetList(r.IPList)
 					r.IP6List = logic.UniqueIPNetList(r.IP6List)
 					r.IP6List = logic.UniqueIPNetList(r.IP6List)
+
+					r.Dst = logic.UniqueIPNetList(r.Dst)
+					r.Dst6 = logic.UniqueIPNetList(r.Dst6)
 					rules[acl.ID] = r
 					rules[acl.ID] = r
 				}
 				}
 			}
 			}
@@ -1231,48 +1281,6 @@ func getUserAclRulesForNode(targetnode *models.Node,
 	return rules
 	return rules
 }
 }
 
 
-func CheckIfAnyActiveEgressPolicy(targetNode models.Node, acls []models.Acl) bool {
-	if !targetNode.EgressDetails.IsEgressGateway {
-		return false
-	}
-	var targetNodeTags = make(map[models.TagID]struct{})
-	if targetNode.Mutex != nil {
-		targetNode.Mutex.Lock()
-		targetNodeTags = maps.Clone(targetNode.Tags)
-		targetNode.Mutex.Unlock()
-	} else {
-		targetNodeTags = maps.Clone(targetNode.Tags)
-	}
-	if targetNodeTags == nil {
-		targetNodeTags = make(map[models.TagID]struct{})
-	}
-	targetNodeTags[models.TagID(targetNode.ID.String())] = struct{}{}
-	targetNodeTags["*"] = struct{}{}
-	for _, acl := range acls {
-		if !acl.Enabled {
-			continue
-		}
-		srcTags := logic.ConvAclTagToValueMap(acl.Src)
-		for _, dst := range acl.Dst {
-			if dst.ID == models.EgressID {
-				e := schema.Egress{ID: dst.Value}
-				err := e.Get(db.WithContext(context.TODO()))
-				if err == nil && e.Status {
-					for nodeTag := range targetNodeTags {
-						if _, ok := srcTags[nodeTag.String()]; ok {
-							return true
-						}
-						if _, ok := srcTags[targetNode.ID.String()]; ok {
-							return true
-						}
-					}
-				}
-			}
-		}
-	}
-	return false
-}
-
 func CheckIfAnyPolicyisUniDirectional(targetNode models.Node, acls []models.Acl) bool {
 func CheckIfAnyPolicyisUniDirectional(targetNode models.Node, acls []models.Acl) bool {
 	var targetNodeTags = make(map[models.TagID]struct{})
 	var targetNodeTags = make(map[models.TagID]struct{})
 	if targetNode.Mutex != nil {
 	if targetNode.Mutex != nil {
@@ -1320,535 +1328,69 @@ func CheckIfAnyPolicyisUniDirectional(targetNode models.Node, acls []models.Acl)
 	return false
 	return false
 }
 }
 
 
-func GetAclRulesForNode(targetnodeI *models.Node) (rules map[string]models.AclRule) {
-	targetnode := *targetnodeI
-	defer func() {
-		//if !targetnode.IsIngressGateway {
-		rules = getUserAclRulesForNode(&targetnode, rules)
-		//}
-	}()
-	rules = make(map[string]models.AclRule)
-	var taggedNodes map[models.TagID][]models.Node
-	if targetnode.IsIngressGateway {
-		taggedNodes = GetTagMapWithNodesByNetwork(models.NetworkID(targetnode.Network), false)
-	} else {
-		taggedNodes = GetTagMapWithNodesByNetwork(models.NetworkID(targetnode.Network), true)
-	}
-	acls := logic.ListDevicePolicies(models.NetworkID(targetnode.Network))
-	var targetNodeTags = make(map[models.TagID]struct{})
-	if targetnode.Mutex != nil {
-		targetnode.Mutex.Lock()
-		targetNodeTags = maps.Clone(targetnode.Tags)
-		targetnode.Mutex.Unlock()
-	} else {
-		targetNodeTags = maps.Clone(targetnode.Tags)
-	}
-	if targetNodeTags == nil {
-		targetNodeTags = make(map[models.TagID]struct{})
-	}
-	targetNodeTags[models.TagID(targetnode.ID.String())] = struct{}{}
-	targetNodeTags["*"] = struct{}{}
-	for _, acl := range acls {
-		if !acl.Enabled {
-			continue
+func GetTagMapWithNodesByNetwork(netID models.NetworkID, withStaticNodes bool) (tagNodesMap map[models.TagID][]models.Node) {
+	tagNodesMap = make(map[models.TagID][]models.Node)
+	nodes, _ := logic.GetNetworkNodes(netID.String())
+	for _, nodeI := range nodes {
+		tagNodesMap[models.TagID(nodeI.ID.String())] = []models.Node{
+			nodeI,
 		}
 		}
-		srcTags := logic.ConvAclTagToValueMap(acl.Src)
-		dstTags := logic.ConvAclTagToValueMap(acl.Dst)
-		for _, dst := range acl.Dst {
-			if dst.ID == models.EgressID {
-				e := schema.Egress{ID: dst.Value}
-				err := e.Get(db.WithContext(context.TODO()))
-				if err == nil && e.Status {
-					for nodeID := range e.Nodes {
-						dstTags[nodeID] = struct{}{}
-					}
-				}
-			}
+		if nodeI.Tags == nil {
+			continue
 		}
 		}
-		_, srcAll := srcTags["*"]
-		_, dstAll := dstTags["*"]
-		aclRule := models.AclRule{
-			ID:              acl.ID,
-			AllowedProtocol: acl.Proto,
-			AllowedPorts:    acl.Port,
-			Direction:       acl.AllowedDirection,
-			Allowed:         true,
+		if nodeI.Mutex != nil {
+			nodeI.Mutex.Lock()
 		}
 		}
-		for nodeTag := range targetNodeTags {
-			if acl.AllowedDirection == models.TrafficDirectionBi {
-				var existsInSrcTag bool
-				var existsInDstTag bool
-
-				if _, ok := srcTags[nodeTag.String()]; ok || srcAll {
-					existsInSrcTag = true
-				}
-				if _, ok := srcTags[targetnode.ID.String()]; ok || srcAll {
-					existsInSrcTag = true
-				}
-				if _, ok := dstTags[nodeTag.String()]; ok || dstAll {
-					existsInDstTag = true
-				}
-				if _, ok := dstTags[targetnode.ID.String()]; ok || dstAll {
-					existsInDstTag = true
-				}
-
-				if existsInSrcTag /* && !existsInDstTag*/ {
-					// get all dst tags
-					for dst := range dstTags {
-						if dst == nodeTag.String() {
-							continue
-						}
-						// Get peers in the tags and add allowed rules
-						nodes := taggedNodes[models.TagID(dst)]
-						if dst != targetnode.ID.String() {
-							node, err := logic.GetNodeByID(dst)
-							if err == nil {
-								nodes = append(nodes, node)
-							}
-						}
-
-						for _, node := range nodes {
-							if node.ID == targetnode.ID {
-								continue
-							}
-							if node.IsStatic && node.StaticNode.IngressGatewayID == targetnode.ID.String() {
-								continue
-							}
-							if node.Address.IP != nil {
-								aclRule.IPList = append(aclRule.IPList, node.AddressIPNet4())
-							}
-							if node.Address6.IP != nil {
-								aclRule.IP6List = append(aclRule.IP6List, node.AddressIPNet6())
-							}
-							if node.IsStatic && node.StaticNode.Address != "" {
-								aclRule.IPList = append(aclRule.IPList, node.StaticNode.AddressIPNet4())
-							}
-							if node.IsStatic && node.StaticNode.Address6 != "" {
-								aclRule.IP6List = append(aclRule.IP6List, node.StaticNode.AddressIPNet6())
-							}
-						}
-					}
-				}
-				if existsInDstTag /*&& !existsInSrcTag*/ {
-					// get all src tags
-					for src := range srcTags {
-						if src == nodeTag.String() {
-							continue
-						}
-						// Get peers in the tags and add allowed rules
-						nodes := taggedNodes[models.TagID(src)]
-						if src != targetnode.ID.String() {
-							node, err := logic.GetNodeByID(src)
-							if err == nil {
-								nodes = append(nodes, node)
-							}
-						}
-						for _, node := range nodes {
-							if node.ID == targetnode.ID {
-								continue
-							}
-							if node.IsStatic && node.StaticNode.IngressGatewayID == targetnode.ID.String() {
-								continue
-							}
-							if node.Address.IP != nil {
-								aclRule.IPList = append(aclRule.IPList, node.AddressIPNet4())
-							}
-							if node.Address6.IP != nil {
-								aclRule.IP6List = append(aclRule.IP6List, node.AddressIPNet6())
-							}
-							if node.IsStatic && node.StaticNode.Address != "" {
-								aclRule.IPList = append(aclRule.IPList, node.StaticNode.AddressIPNet4())
-							}
-							if node.IsStatic && node.StaticNode.Address6 != "" {
-								aclRule.IP6List = append(aclRule.IP6List, node.StaticNode.AddressIPNet6())
-							}
-						}
-					}
-				}
-				// if existsInDstTag && existsInSrcTag {
-				// 	nodes := taggedNodes[nodeTag]
-				// 	for srcID := range srcTags {
-				// 		if srcID == targetnode.ID.String() {
-				// 			continue
-				// 		}
-				// 		node, err := GetNodeByID(srcID)
-				// 		if err == nil {
-				// 			nodes = append(nodes, node)
-				// 		}
-				// 	}
-				// 	for dstID := range dstTags {
-				// 		if dstID == targetnode.ID.String() {
-				// 			continue
-				// 		}
-				// 		node, err := GetNodeByID(dstID)
-				// 		if err == nil {
-				// 			nodes = append(nodes, node)
-				// 		}
-				// 	}
-				// 	for _, node := range nodes {
-				// 		if node.ID == targetnode.ID {
-				// 			continue
-				// 		}
-				// 		if node.IsStatic && node.StaticNode.IngressGatewayID == targetnode.ID.String() {
-				// 			continue
-				// 		}
-				// 		if node.Address.IP != nil {
-				// 			aclRule.IPList = append(aclRule.IPList, node.AddressIPNet4())
-				// 		}
-				// 		if node.Address6.IP != nil {
-				// 			aclRule.IP6List = append(aclRule.IP6List, node.AddressIPNet6())
-				// 		}
-				// 		if node.IsStatic && node.StaticNode.Address != "" {
-				// 			aclRule.IPList = append(aclRule.IPList, node.StaticNode.AddressIPNet4())
-				// 		}
-				// 		if node.IsStatic && node.StaticNode.Address6 != "" {
-				// 			aclRule.IP6List = append(aclRule.IP6List, node.StaticNode.AddressIPNet6())
-				// 		}
-				// 	}
-				// }
-			} else {
-				_, all := dstTags["*"]
-				if _, ok := dstTags[nodeTag.String()]; ok || all {
-					// get all src tags
-					for src := range srcTags {
-						if src == nodeTag.String() {
-							continue
-						}
-						// Get peers in the tags and add allowed rules
-						nodes := taggedNodes[models.TagID(src)]
-						for _, node := range nodes {
-							if node.ID == targetnode.ID {
-								continue
-							}
-							if node.IsStatic && node.StaticNode.IngressGatewayID == targetnode.ID.String() {
-								continue
-							}
-							if node.Address.IP != nil {
-								aclRule.IPList = append(aclRule.IPList, node.AddressIPNet4())
-							}
-							if node.Address6.IP != nil {
-								aclRule.IP6List = append(aclRule.IP6List, node.AddressIPNet6())
-							}
-							if node.IsStatic && node.StaticNode.Address != "" {
-								aclRule.IPList = append(aclRule.IPList, node.StaticNode.AddressIPNet4())
-							}
-							if node.IsStatic && node.StaticNode.Address6 != "" {
-								aclRule.IP6List = append(aclRule.IP6List, node.StaticNode.AddressIPNet6())
-							}
-						}
-					}
-				}
+		for nodeTagID := range nodeI.Tags {
+			if nodeTagID == models.TagID(nodeI.ID.String()) {
+				continue
 			}
 			}
-
+			tagNodesMap[nodeTagID] = append(tagNodesMap[nodeTagID], nodeI)
 		}
 		}
-
-		if len(aclRule.IPList) > 0 || len(aclRule.IP6List) > 0 {
-			aclRule.IPList = logic.UniqueIPNetList(aclRule.IPList)
-			aclRule.IP6List = logic.UniqueIPNetList(aclRule.IP6List)
-			rules[acl.ID] = aclRule
+		if nodeI.Mutex != nil {
+			nodeI.Mutex.Unlock()
 		}
 		}
 	}
 	}
-	return rules
-}
-
-func GetAclRuleForInetGw(targetnode models.Node) (rules map[string]models.AclRule) {
-	rules = make(map[string]models.AclRule)
-	if targetnode.IsInternetGateway {
-		aclRule := models.AclRule{
-			ID:              fmt.Sprintf("%s-inet-gw-internal-rule", targetnode.ID.String()),
-			AllowedProtocol: models.ALL,
-			AllowedPorts:    []string{},
-			Direction:       models.TrafficDirectionBi,
-			Allowed:         true,
-		}
-		if targetnode.NetworkRange.IP != nil {
-			aclRule.IPList = append(aclRule.IPList, targetnode.NetworkRange)
-			_, allIpv4, _ := net.ParseCIDR(logic.IPv4Network)
-			aclRule.Dst = append(aclRule.Dst, *allIpv4)
-		}
-		if targetnode.NetworkRange6.IP != nil {
-			aclRule.IP6List = append(aclRule.IP6List, targetnode.NetworkRange6)
-			_, allIpv6, _ := net.ParseCIDR(logic.IPv6Network)
-			aclRule.Dst6 = append(aclRule.Dst6, *allIpv6)
-		}
-		rules[aclRule.ID] = aclRule
+	tagNodesMap["*"] = nodes
+	if !withStaticNodes {
+		return
 	}
 	}
-	return
+	return AddTagMapWithStaticNodes(netID, tagNodesMap)
 }
 }
 
 
-func GetEgressRulesForNode(targetnode models.Node) (rules map[string]models.AclRule) {
-	rules = make(map[string]models.AclRule)
-	defer func() {
-		rules = getEgressUserRulesForNode(&targetnode, rules)
-	}()
-	taggedNodes := GetTagMapWithNodesByNetwork(models.NetworkID(targetnode.Network), true)
-
-	acls := logic.ListDevicePolicies(models.NetworkID(targetnode.Network))
-	var targetNodeTags = make(map[models.TagID]struct{})
-	targetNodeTags["*"] = struct{}{}
-
-	/*
-		 if target node is egress gateway
-			if acl policy has egress route and it is present in target node egress ranges
-			fetch all the nodes in that policy and add rules
-	*/
-
-	egs, _ := (&schema.Egress{Network: targetnode.Network}).ListByNetwork(db.WithContext(context.TODO()))
-	if len(egs) == 0 {
-		return
+func AddTagMapWithStaticNodes(netID models.NetworkID,
+	tagNodesMap map[models.TagID][]models.Node) map[models.TagID][]models.Node {
+	extclients, err := logic.GetNetworkExtClients(netID.String())
+	if err != nil {
+		return tagNodesMap
 	}
 	}
-	for _, egI := range egs {
-		if !egI.Status {
+	for _, extclient := range extclients {
+		if extclient.RemoteAccessClientID != "" {
 			continue
 			continue
 		}
 		}
-		if _, ok := egI.Nodes[targetnode.ID.String()]; ok {
-			targetNodeTags[models.TagID(egI.Range)] = struct{}{}
-			targetNodeTags[models.TagID(egI.ID)] = struct{}{}
+		tagNodesMap[models.TagID(extclient.ClientID)] = []models.Node{
+			{
+				IsStatic:   true,
+				StaticNode: extclient,
+			},
 		}
 		}
-	}
-	for _, acl := range acls {
-		if !acl.Enabled {
+		if extclient.Tags == nil {
 			continue
 			continue
 		}
 		}
-		srcTags := logic.ConvAclTagToValueMap(acl.Src)
-		dstTags := logic.ConvAclTagToValueMap(acl.Dst)
-		_, srcAll := srcTags["*"]
-		_, dstAll := dstTags["*"]
-		aclRule := models.AclRule{
-			ID:              acl.ID,
-			AllowedProtocol: acl.Proto,
-			AllowedPorts:    acl.Port,
-			Direction:       acl.AllowedDirection,
-			Allowed:         true,
-		}
-		for nodeTag := range targetNodeTags {
-
-			if nodeTag != "*" {
-				ip, cidr, err := net.ParseCIDR(nodeTag.String())
-				if err == nil {
-					if ip.To4() != nil {
-						aclRule.Dst = append(aclRule.Dst, *cidr)
-					} else {
-						aclRule.Dst6 = append(aclRule.Dst6, *cidr)
-					}
-				}
-			}
-			if acl.AllowedDirection == models.TrafficDirectionBi {
-				var existsInSrcTag bool
-				var existsInDstTag bool
-				if _, ok := srcTags[nodeTag.String()]; ok || srcAll {
-					existsInSrcTag = true
-				}
-				if _, ok := dstTags[nodeTag.String()]; ok || dstAll {
-					existsInDstTag = true
-				}
-				// if srcAll || dstAll {
-				// 	if targetnode.NetworkRange.IP != nil {
-				// 		aclRule.IPList = append(aclRule.IPList, targetnode.NetworkRange)
-				// 	}
-				// 	if targetnode.NetworkRange6.IP != nil {
-				// 		aclRule.IP6List = append(aclRule.IP6List, targetnode.NetworkRange6)
-				// 	}
-				// 	break
-				// }
-				if existsInSrcTag && !existsInDstTag {
-					// get all dst tags
-					for dst := range dstTags {
-						if dst == nodeTag.String() {
-							continue
-						}
-						// Get peers in the tags and add allowed rules
-						nodes := taggedNodes[models.TagID(dst)]
-						if dst != targetnode.ID.String() {
-							node, err := logic.GetNodeByID(dst)
-							if err == nil {
-								nodes = append(nodes, node)
-							}
-							extclient, err := logic.GetExtClient(dst, targetnode.Network)
-							if err == nil {
-								nodes = append(nodes, extclient.ConvertToStaticNode())
-							}
-						}
-
-						for _, node := range nodes {
-							if node.ID == targetnode.ID {
-								continue
-							}
-							if node.Address.IP != nil {
-								aclRule.IPList = append(aclRule.IPList, node.AddressIPNet4())
-							}
-							if node.Address6.IP != nil {
-								aclRule.IP6List = append(aclRule.IP6List, node.AddressIPNet6())
-							}
-							if node.IsStatic && node.StaticNode.Address != "" {
-								aclRule.IPList = append(aclRule.IPList, node.StaticNode.AddressIPNet4())
-							}
-							if node.IsStatic && node.StaticNode.Address6 != "" {
-								aclRule.IP6List = append(aclRule.IP6List, node.StaticNode.AddressIPNet6())
-							}
-						}
-					}
-				}
-				if existsInDstTag && !existsInSrcTag {
-					// get all src tags
-					for src := range srcTags {
-						if src == nodeTag.String() {
-							continue
-						}
-						// Get peers in the tags and add allowed rules
-						nodes := taggedNodes[models.TagID(src)]
-						if src != targetnode.ID.String() {
-							node, err := logic.GetNodeByID(src)
-							if err == nil {
-								nodes = append(nodes, node)
-							}
-							extclient, err := logic.GetExtClient(src, targetnode.Network)
-							if err == nil {
-								nodes = append(nodes, extclient.ConvertToStaticNode())
-							}
-						}
-						for _, node := range nodes {
-							if node.ID == targetnode.ID {
-								continue
-							}
-							if node.Address.IP != nil {
-								aclRule.IPList = append(aclRule.IPList, node.AddressIPNet4())
-							}
-							if node.Address6.IP != nil {
-								aclRule.IP6List = append(aclRule.IP6List, node.AddressIPNet6())
-							}
-							if node.IsStatic && node.StaticNode.Address != "" {
-								aclRule.IPList = append(aclRule.IPList, node.StaticNode.AddressIPNet4())
-							}
-							if node.IsStatic && node.StaticNode.Address6 != "" {
-								aclRule.IP6List = append(aclRule.IP6List, node.StaticNode.AddressIPNet6())
-							}
-						}
-					}
-				}
-				if existsInDstTag && existsInSrcTag {
-					nodes := taggedNodes[nodeTag]
-					for srcID := range srcTags {
-						if srcID == targetnode.ID.String() {
-							continue
-						}
-						node, err := logic.GetNodeByID(srcID)
-						if err == nil {
-							nodes = append(nodes, node)
-						}
-						extclient, err := logic.GetExtClient(srcID, targetnode.Network)
-						if err == nil {
-							nodes = append(nodes, extclient.ConvertToStaticNode())
-						}
-					}
-					for dstID := range dstTags {
-						if dstID == targetnode.ID.String() {
-							continue
-						}
-						node, err := logic.GetNodeByID(dstID)
-						if err == nil {
-							nodes = append(nodes, node)
-						}
-						extclient, err := logic.GetExtClient(dstID, targetnode.Network)
-						if err == nil {
-							nodes = append(nodes, extclient.ConvertToStaticNode())
-						}
-					}
-					for _, node := range nodes {
-						if node.ID == targetnode.ID {
-							continue
-						}
-						if node.Address.IP != nil {
-							aclRule.IPList = append(aclRule.IPList, node.AddressIPNet4())
-						}
-						if node.Address6.IP != nil {
-							aclRule.IP6List = append(aclRule.IP6List, node.AddressIPNet6())
-						}
-						if node.IsStatic && node.StaticNode.Address != "" {
-							aclRule.IPList = append(aclRule.IPList, node.StaticNode.AddressIPNet4())
-						}
-						if node.IsStatic && node.StaticNode.Address6 != "" {
-							aclRule.IP6List = append(aclRule.IP6List, node.StaticNode.AddressIPNet6())
-						}
-					}
-				}
-			} else {
-				if dstAll {
-					if targetnode.NetworkRange.IP != nil {
-						aclRule.IPList = append(aclRule.IPList, targetnode.NetworkRange)
-					}
-					if targetnode.NetworkRange6.IP != nil {
-						aclRule.IP6List = append(aclRule.IP6List, targetnode.NetworkRange6)
-					}
-					break
-				}
-				if _, ok := dstTags[nodeTag.String()]; ok || dstAll {
-					// get all src tags
-					for src := range srcTags {
-						if src == nodeTag.String() {
-							continue
-						}
-						// Get peers in the tags and add allowed rules
-						nodes := taggedNodes[models.TagID(src)]
-						for _, node := range nodes {
-							if node.ID == targetnode.ID {
-								continue
-							}
-							if node.Address.IP != nil {
-								aclRule.IPList = append(aclRule.IPList, node.AddressIPNet4())
-							}
-							if node.Address6.IP != nil {
-								aclRule.IP6List = append(aclRule.IP6List, node.AddressIPNet6())
-							}
-							if node.IsStatic && node.StaticNode.Address != "" {
-								aclRule.IPList = append(aclRule.IPList, node.StaticNode.AddressIPNet4())
-							}
-							if node.IsStatic && node.StaticNode.Address6 != "" {
-								aclRule.IP6List = append(aclRule.IP6List, node.StaticNode.AddressIPNet6())
-							}
-						}
-					}
-				}
-			}
 
 
+		if extclient.Mutex != nil {
+			extclient.Mutex.Lock()
 		}
 		}
-		if len(aclRule.IPList) > 0 || len(aclRule.IP6List) > 0 {
-			aclRule.IPList = logic.UniqueIPNetList(aclRule.IPList)
-			aclRule.IP6List = logic.UniqueIPNetList(aclRule.IP6List)
-			rules[acl.ID] = aclRule
-		}
-
-	}
-
-	return
-}
-
-func GetInetClientsFromAclPolicies(eID string) (inetClientIDs []string) {
-	e := schema.Egress{ID: eID}
-	err := e.Get(db.WithContext(context.TODO()))
-	if err != nil || !e.Status {
-		return
-	}
-	acls, _ := logic.ListAclsByNetwork(models.NetworkID(e.Network))
-	for _, acl := range acls {
-		for _, dstI := range acl.Dst {
-			if dstI.ID == models.EgressID {
-				if dstI.Value != eID {
-					continue
-				}
-				for _, srcI := range acl.Src {
-					if srcI.Value == "*" {
-						continue
-					}
-					if srcI.ID == models.NodeID {
-						inetClientIDs = append(inetClientIDs, srcI.Value)
-					}
-					if srcI.ID == models.NodeTagID {
-						inetClientIDs = append(inetClientIDs, GetNodeIDsWithTag(models.TagID(srcI.Value))...)
-					}
-				}
+		for tagID := range extclient.Tags {
+			if tagID == models.TagID(extclient.ClientID) {
+				continue
 			}
 			}
+			tagNodesMap[tagID] = append(tagNodesMap[tagID], extclient.ConvertToStaticNode())
+			tagNodesMap["*"] = append(tagNodesMap["*"], extclient.ConvertToStaticNode())
+		}
+		if extclient.Mutex != nil {
+			extclient.Mutex.Unlock()
 		}
 		}
 	}
 	}
-	return
+	return tagNodesMap
 }
 }

+ 0 - 67
pro/logic/nodes.go

@@ -41,73 +41,6 @@ func GetTagMapWithNodes() (tagNodesMap map[models.TagID][]models.Node) {
 	return
 	return
 }
 }
 
 
-func GetTagMapWithNodesByNetwork(netID models.NetworkID, withStaticNodes bool) (tagNodesMap map[models.TagID][]models.Node) {
-	tagNodesMap = make(map[models.TagID][]models.Node)
-	nodes, _ := logic.GetNetworkNodes(netID.String())
-	for _, nodeI := range nodes {
-		tagNodesMap[models.TagID(nodeI.ID.String())] = []models.Node{
-			nodeI,
-		}
-		if nodeI.Tags == nil {
-			continue
-		}
-		if nodeI.Mutex != nil {
-			nodeI.Mutex.Lock()
-		}
-		for nodeTagID := range nodeI.Tags {
-			if nodeTagID == models.TagID(nodeI.ID.String()) {
-				continue
-			}
-			tagNodesMap[nodeTagID] = append(tagNodesMap[nodeTagID], nodeI)
-		}
-		if nodeI.Mutex != nil {
-			nodeI.Mutex.Unlock()
-		}
-	}
-	tagNodesMap["*"] = nodes
-	if !withStaticNodes {
-		return
-	}
-	return AddTagMapWithStaticNodes(netID, tagNodesMap)
-}
-
-func AddTagMapWithStaticNodes(netID models.NetworkID,
-	tagNodesMap map[models.TagID][]models.Node) map[models.TagID][]models.Node {
-	extclients, err := logic.GetNetworkExtClients(netID.String())
-	if err != nil {
-		return tagNodesMap
-	}
-	for _, extclient := range extclients {
-		if extclient.RemoteAccessClientID != "" {
-			continue
-		}
-		tagNodesMap[models.TagID(extclient.ClientID)] = []models.Node{
-			{
-				IsStatic:   true,
-				StaticNode: extclient,
-			},
-		}
-		if extclient.Tags == nil {
-			continue
-		}
-
-		if extclient.Mutex != nil {
-			extclient.Mutex.Lock()
-		}
-		for tagID := range extclient.Tags {
-			if tagID == models.TagID(extclient.ClientID) {
-				continue
-			}
-			tagNodesMap[tagID] = append(tagNodesMap[tagID], extclient.ConvertToStaticNode())
-			tagNodesMap["*"] = append(tagNodesMap["*"], extclient.ConvertToStaticNode())
-		}
-		if extclient.Mutex != nil {
-			extclient.Mutex.Unlock()
-		}
-	}
-	return tagNodesMap
-}
-
 func AddTagMapWithStaticNodesWithUsers(netID models.NetworkID,
 func AddTagMapWithStaticNodesWithUsers(netID models.NetworkID,
 	tagNodesMap map[models.TagID][]models.Node) map[models.TagID][]models.Node {
 	tagNodesMap map[models.TagID][]models.Node) map[models.TagID][]models.Node {
 	extclients, err := logic.GetNetworkExtClients(netID.String())
 	extclients, err := logic.GetNetworkExtClients(netID.String())

+ 6 - 12
pro/logic/security.go

@@ -4,6 +4,7 @@ import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
+	"strings"
 
 
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
@@ -115,13 +116,10 @@ func checkNetworkAccessPermissions(netRoleID models.UserRoleID, username, reqSco
 		return nil
 		return nil
 	}
 	}
 	rsrcPermissionScope, ok := networkPermissionScope.NetworkLevelAccess[models.RsrcType(targetRsrc)]
 	rsrcPermissionScope, ok := networkPermissionScope.NetworkLevelAccess[models.RsrcType(targetRsrc)]
-	if targetRsrc == models.HostRsrc.String() && !ok {
-		rsrcPermissionScope, ok = networkPermissionScope.NetworkLevelAccess[models.RemoteAccessGwRsrc]
-	}
 	if !ok {
 	if !ok {
 		return errors.New("access denied")
 		return errors.New("access denied")
 	}
 	}
-	if allRsrcsTypePermissionScope, ok := rsrcPermissionScope[models.RsrcID(fmt.Sprintf("all_%s", targetRsrc))]; ok {
+	if allRsrcsTypePermissionScope, ok := rsrcPermissionScope[logic.GetAllRsrcIDForRsrc(models.RsrcType(targetRsrc))]; ok {
 		// handle extclient apis here
 		// handle extclient apis here
 		if models.RsrcType(targetRsrc) == models.ExtClientsRsrc && allRsrcsTypePermissionScope.SelfOnly && targetRsrcID != "" {
 		if models.RsrcType(targetRsrc) == models.ExtClientsRsrc && allRsrcsTypePermissionScope.SelfOnly && targetRsrcID != "" {
 			extclient, err := logic.GetExtClient(targetRsrcID, netID)
 			extclient, err := logic.GetExtClient(targetRsrcID, netID)
@@ -138,14 +136,6 @@ func checkNetworkAccessPermissions(netRoleID models.UserRoleID, username, reqSco
 		}
 		}
 
 
 	}
 	}
-	if targetRsrc == models.HostRsrc.String() {
-		if allRsrcsTypePermissionScope, ok := rsrcPermissionScope[models.RsrcID(fmt.Sprintf("all_%s", models.RemoteAccessGwRsrc))]; ok {
-			err = checkPermissionScopeWithReqMethod(allRsrcsTypePermissionScope, reqScope)
-			if err == nil {
-				return nil
-			}
-		}
-	}
 	if targetRsrcID == "" {
 	if targetRsrcID == "" {
 		return errors.New("target rsrc id is empty")
 		return errors.New("target rsrc id is empty")
 	}
 	}
@@ -184,6 +174,10 @@ func GlobalPermissionsCheck(username string, r *http.Request) error {
 	if (targetRsrc == models.HostRsrc.String() || targetRsrc == models.NetworkRsrc.String()) && r.Method == http.MethodGet && targetRsrcID == "" {
 	if (targetRsrc == models.HostRsrc.String() || targetRsrc == models.NetworkRsrc.String()) && r.Method == http.MethodGet && targetRsrcID == "" {
 		return nil
 		return nil
 	}
 	}
+	if targetRsrc == models.UserRsrc.String() && user.PlatformRoleID == models.PlatformUser && r.Method == http.MethodPut &&
+		strings.Contains(r.URL.Path, "/api/v1/users/add_network_user") || strings.Contains(r.URL.Path, "/api/v1/users/remove_network_user") {
+		return nil
+	}
 	if targetRsrc == models.UserRsrc.String() && username == targetRsrcID && (r.Method != http.MethodDelete) {
 	if targetRsrc == models.UserRsrc.String() && username == targetRsrcID && (r.Method != http.MethodDelete) {
 		return nil
 		return nil
 	}
 	}

+ 13 - 0
pro/logic/server.go

@@ -0,0 +1,13 @@
+package logic
+
+import "github.com/gravitl/netmaker/models"
+
+var featureFlagsCache models.FeatureFlags
+
+func SetFeatureFlags(featureFlags models.FeatureFlags) {
+	featureFlagsCache = featureFlags
+}
+
+func GetFeatureFlags() models.FeatureFlags {
+	return featureFlagsCache
+}

+ 148 - 34
pro/logic/user_mgmt.go

@@ -34,6 +34,13 @@ var PlatformUserUserPermissionTemplate = models.UserRolePermissionTemplate{
 	ID:         models.PlatformUser,
 	ID:         models.PlatformUser,
 	Default:    true,
 	Default:    true,
 	FullAccess: false,
 	FullAccess: false,
+	GlobalLevelAccess: map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope{
+		models.UserRsrc: {
+			models.AllUserRsrcID: models.RsrcPermissionScope{
+				Read: true,
+			},
+		},
+	},
 }
 }
 
 
 var NetworkAdminAllPermissionTemplate = models.UserRolePermissionTemplate{
 var NetworkAdminAllPermissionTemplate = models.UserRolePermissionTemplate{
@@ -53,6 +60,11 @@ var NetworkUserAllPermissionTemplate = models.UserRolePermissionTemplate{
 	FullAccess: false,
 	FullAccess: false,
 	NetworkID:  models.AllNetworks,
 	NetworkID:  models.AllNetworks,
 	NetworkLevelAccess: map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope{
 	NetworkLevelAccess: map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope{
+		models.HostRsrc: {
+			models.AllHostRsrcID: models.RsrcPermissionScope{
+				Read: true,
+			},
+		},
 		models.RemoteAccessGwRsrc: {
 		models.RemoteAccessGwRsrc: {
 			models.AllRemoteAccessGwRsrcID: models.RsrcPermissionScope{
 			models.AllRemoteAccessGwRsrcID: models.RsrcPermissionScope{
 				Read:      true,
 				Read:      true,
@@ -114,7 +126,6 @@ func UserRolesInit() {
 	database.Insert(NetworkAdminAllPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
 	database.Insert(NetworkAdminAllPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
 	d, _ = json.Marshal(NetworkUserAllPermissionTemplate)
 	d, _ = json.Marshal(NetworkUserAllPermissionTemplate)
 	database.Insert(NetworkUserAllPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
 	database.Insert(NetworkUserAllPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
-
 }
 }
 
 
 func UserGroupsInit() {
 func UserGroupsInit() {
@@ -170,6 +181,11 @@ func CreateDefaultNetworkRolesAndGroups(netID models.NetworkID) {
 		NetworkID:           netID,
 		NetworkID:           netID,
 		DenyDashboardAccess: false,
 		DenyDashboardAccess: false,
 		NetworkLevelAccess: map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope{
 		NetworkLevelAccess: map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope{
+			models.HostRsrc: {
+				models.AllHostRsrcID: models.RsrcPermissionScope{
+					Read: true,
+				},
+			},
 			models.RemoteAccessGwRsrc: {
 			models.RemoteAccessGwRsrc: {
 				models.AllRemoteAccessGwRsrcID: models.RsrcPermissionScope{
 				models.AllRemoteAccessGwRsrcID: models.RsrcPermissionScope{
 					Read:      true,
 					Read:      true,
@@ -581,7 +597,13 @@ func CreateUserGroup(g *models.UserGroup) error {
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	return database.Insert(g.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME)
+	err = database.Insert(g.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME)
+	if err != nil {
+		return err
+	}
+	// create default network gateway policies
+	CreateDefaultUserGroupNetworkPolicies(*g)
+	return nil
 }
 }
 
 
 // GetUserGroup - fetches user group
 // GetUserGroup - fetches user group
@@ -646,11 +668,16 @@ func UpdateUserGroup(g models.UserGroup) error {
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
+
 	return database.Insert(g.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME)
 	return database.Insert(g.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME)
 }
 }
 
 
 // DeleteUserGroup - deletes user group
 // DeleteUserGroup - deletes user group
 func DeleteUserGroup(gid models.UserGroupID) error {
 func DeleteUserGroup(gid models.UserGroupID) error {
+	g, err := GetUserGroup(gid)
+	if err != nil {
+		return err
+	}
 	users, err := logic.GetUsersDB()
 	users, err := logic.GetUsersDB()
 	if err != nil && !database.IsEmptyRecord(err) {
 	if err != nil && !database.IsEmptyRecord(err) {
 		return err
 		return err
@@ -659,6 +686,8 @@ func DeleteUserGroup(gid models.UserGroupID) error {
 		delete(user.UserGroups, gid)
 		delete(user.UserGroups, gid)
 		logic.UpsertUser(user)
 		logic.UpsertUser(user)
 	}
 	}
+	// create default network gateway policies
+	DeleteDefaultUserGroupNetworkPolicies(g)
 	return database.DeleteRecord(database.USER_GROUPS_TABLE_NAME, gid.String())
 	return database.DeleteRecord(database.USER_GROUPS_TABLE_NAME, gid.String())
 }
 }
 
 
@@ -1073,27 +1102,14 @@ func UpdatesUserGwAccessOnRoleUpdates(currNetworkAccess,
 	}
 	}
 }
 }
 
 
-func UpdatesUserGwAccessOnGrpUpdates(currNetworkRoles, changeNetworkRoles map[models.NetworkID]map[models.UserRoleID]struct{}) {
-	networkChangeMap := make(map[models.NetworkID]map[models.UserRoleID]struct{})
-	for netID, networkUserRoles := range currNetworkRoles {
-		if _, ok := changeNetworkRoles[netID]; !ok {
-			for netRoleID := range networkUserRoles {
-				if _, ok := networkChangeMap[netID]; !ok {
-					networkChangeMap[netID] = make(map[models.UserRoleID]struct{})
-				}
-				networkChangeMap[netID][netRoleID] = struct{}{}
-			}
-		} else {
-			for netRoleID := range networkUserRoles {
-				if _, ok := changeNetworkRoles[netID][netRoleID]; !ok {
-					if _, ok := networkChangeMap[netID]; !ok {
-						networkChangeMap[netID] = make(map[models.UserRoleID]struct{})
-					}
-					networkChangeMap[netID][netRoleID] = struct{}{}
-				}
-			}
+func UpdatesUserGwAccessOnGrpUpdates(groupID models.UserGroupID, oldNetworkRoles, newNetworkRoles map[models.NetworkID]map[models.UserRoleID]struct{}) {
+	networkRemovedMap := make(map[models.NetworkID]struct{})
+	for netID := range oldNetworkRoles {
+		if _, ok := newNetworkRoles[netID]; !ok {
+			networkRemovedMap[netID] = struct{}{}
 		}
 		}
 	}
 	}
+
 	extclients, err := logic.GetAllExtClients()
 	extclients, err := logic.GetAllExtClients()
 	if err != nil {
 	if err != nil {
 		slog.Error("failed to fetch extclients", "error", err)
 		slog.Error("failed to fetch extclients", "error", err)
@@ -1103,27 +1119,56 @@ func UpdatesUserGwAccessOnGrpUpdates(currNetworkRoles, changeNetworkRoles map[mo
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
-	for _, extclient := range extclients {
 
 
-		if _, ok := networkChangeMap[models.NetworkID(extclient.Network)]; ok {
-			if user, ok := userMap[extclient.OwnerID]; ok {
-				if user.PlatformRoleID != models.ServiceUser {
-					continue
-				}
-				err = logic.DeleteExtClientAndCleanup(extclient)
-				if err != nil {
-					slog.Error("failed to delete extclient",
-						"id", extclient.ClientID, "owner", user.UserName, "error", err)
+	for _, extclient := range extclients {
+		var shouldDelete bool
+		user, ok := userMap[extclient.OwnerID]
+		if !ok {
+			// user does not exist, delete extclient.
+			shouldDelete = true
+		} else {
+			if user.PlatformRoleID == models.SuperAdminRole || user.PlatformRoleID == models.AdminRole {
+				// Super-admin and Admin's access is not determined by group membership
+				// or network roles. Even if a network is removed from the group, they
+				// continue to have access to the network.
+				// So, no need to delete the extclient.
+				shouldDelete = false
+			} else {
+				_, hasAccess := user.NetworkRoles[models.NetworkID(extclient.Network)]
+				if hasAccess {
+					// The user has access to the network by themselves and not by
+					// virtue of being a member of the group.
+					// So, no need to delete the extclient.
+					shouldDelete = false
 				} else {
 				} else {
-					if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil {
-						slog.Error("error setting ext peers: " + err.Error())
+					_, userInGroup := user.UserGroups[groupID]
+					_, networkRemoved := networkRemovedMap[models.NetworkID(extclient.Network)]
+					if userInGroup && networkRemoved {
+						// This group no longer provides it's members access to the
+						// network.
+						// This user is a member of the group and has no direct
+						// access to the network (either by its platform role or by
+						// network roles).
+						// So, delete the extclient.
+						shouldDelete = true
 					}
 					}
 				}
 				}
 			}
 			}
-
 		}
 		}
 
 
+		if shouldDelete {
+			err = logic.DeleteExtClientAndCleanup(extclient)
+			if err != nil {
+				slog.Error("failed to delete extclient",
+					"id", extclient.ClientID, "owner", user.UserName, "error", err)
+			} else {
+				if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil {
+					slog.Error("error setting ext peers: " + err.Error())
+				}
+			}
+		}
 	}
 	}
+
 	if servercfg.IsDNSMode() {
 	if servercfg.IsDNSMode() {
 		logic.SetDNS()
 		logic.SetDNS()
 	}
 	}
@@ -1203,6 +1248,75 @@ func UpdateUserGwAccess(currentUser, changeUser models.User) {
 
 
 }
 }
 
 
+func CreateDefaultUserGroupNetworkPolicies(g models.UserGroup) {
+	for networkID := range g.NetworkRoles {
+		network, err := logic.GetNetwork(networkID.String())
+		if err != nil {
+			continue
+		}
+
+		acl := models.Acl{
+			ID:          uuid.New().String(),
+			Name:        fmt.Sprintf("%s group", g.Name),
+			MetaData:    "This Policy allows user group to communicate with all gateways",
+			Default:     true,
+			ServiceType: models.Any,
+			NetworkID:   models.NetworkID(network.NetID),
+			Proto:       models.ALL,
+			RuleType:    models.UserPolicy,
+			Src: []models.AclPolicyTag{
+				{
+					ID:    models.UserGroupAclID,
+					Value: g.ID.String(),
+				},
+			},
+			Dst: []models.AclPolicyTag{
+				{
+					ID:    models.NodeTagID,
+					Value: fmt.Sprintf("%s.%s", models.NetworkID(network.NetID), models.GwTagName),
+				}},
+			AllowedDirection: models.TrafficDirectionUni,
+			Enabled:          true,
+			CreatedBy:        "auto",
+			CreatedAt:        time.Now().UTC(),
+		}
+		logic.InsertAcl(acl)
+
+	}
+}
+
+func DeleteDefaultUserGroupNetworkPolicies(g models.UserGroup) {
+	for networkID := range g.NetworkRoles {
+		acls, err := logic.ListAclsByNetwork(networkID)
+		if err != nil {
+			continue
+		}
+
+		for _, acl := range acls {
+			var hasGroupSrc bool
+			newAclSrc := make([]models.AclPolicyTag, 0)
+			for _, src := range acl.Src {
+				if src.ID == models.UserGroupAclID && src.Value == g.ID.String() {
+					hasGroupSrc = true
+				} else {
+					newAclSrc = append(newAclSrc, src)
+				}
+			}
+
+			if hasGroupSrc {
+				if len(newAclSrc) == 0 {
+					// no other src exists, delete acl.
+					_ = logic.DeleteAcl(acl)
+				} else {
+					// other sources exist, update acl.
+					acl.Src = newAclSrc
+					_ = logic.UpsertAcl(acl)
+				}
+			}
+		}
+	}
+}
+
 func CreateDefaultUserPolicies(netID models.NetworkID) {
 func CreateDefaultUserPolicies(netID models.NetworkID) {
 	if netID.String() == "" {
 	if netID.String() == "" {
 		return
 		return

+ 5 - 2
pro/types.go

@@ -5,6 +5,7 @@ package pro
 
 
 import (
 import (
 	"errors"
 	"errors"
+	"github.com/gravitl/netmaker/models"
 )
 )
 
 
 const (
 const (
@@ -32,8 +33,9 @@ type LicenseKey struct {
 
 
 // ValidatedLicense - the validated license struct
 // ValidatedLicense - the validated license struct
 type ValidatedLicense struct {
 type ValidatedLicense struct {
-	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
+	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
+	FeatureFlags     models.FeatureFlags `json:"feature_flags" binding:"required"`
 }
 }
 
 
 // LicenseSecret - the encrypted struct for sending user-id
 // LicenseSecret - the encrypted struct for sending user-id
@@ -74,6 +76,7 @@ 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)
 	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"`
+	NmBaseDomain   string `json:"nm_base_domain"`
 }
 }
 
 
 type licenseResponseCache struct {
 type licenseResponseCache struct {

+ 4 - 6
pro/util.go

@@ -4,10 +4,11 @@
 package pro
 package pro
 
 
 import (
 import (
+	"context"
 	"encoding/base64"
 	"encoding/base64"
-
+	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
-
+	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 )
 )
 
 
@@ -49,10 +50,7 @@ func getCurrentServerUsage() (limits Usage) {
 	if err == nil {
 	if err == nil {
 		limits.Ingresses = len(ingresses)
 		limits.Ingresses = len(ingresses)
 	}
 	}
-	egresses, err := logic.GetAllEgresses()
-	if err == nil {
-		limits.Egresses = len(egresses)
-	}
+	limits.Egresses, _ = (&schema.Egress{}).Count(db.WithContext(context.TODO()))
 	relays, err := logic.GetRelays()
 	relays, err := logic.GetRelays()
 	if err == nil {
 	if err == nil {
 		limits.Relays = len(relays)
 		limits.Relays = len(relays)

+ 16 - 12
release.md

@@ -1,26 +1,30 @@
-# Netmaker v1.0.0
+## Netmaker v1.1.0 Release Notes 🚀 
 
 
-## Whats New ✨
+## Whats New ✨ 
 
 
-- Multi-Factor Authentication (MFA) for user logins – added an extra layer of security to your accounts.
+- Okta IDP Integration – Seamless authentication and user provisioning with Okta.
 
 
-- Gateways Unified: Internet Gateways are now merged into the general Gateway feature and available in Community Edition.
+- Egress Domain-Based Routing – Route traffic based on domain names, not just network CIDRs.
 
 
-- Improved OAuth & IDP Sync: Simplified and more reliable configuration for identity provider integrations.
+- DNS Nameservers with Match Domain Functionality – Fine-grained DNS resolution control per domain.
 
 
-- Global Map View: Visualize all your endpoints and users across the globe in a unified interface.
+- Service User Management – Platform Network Admins can now add service users directly to networks.
 
 
-- Network Graph Control: Directly control and manage endpoints via the interactive network graph.
+- Device Approval Workflow – Require admin approval before devices can join a network.
 
 
-- Site-to-Site over IPv6: IPv4 site-to-site communication over IPv6 Netmaker overlay tunnels.
+- Auto-Created User Group Policies – Automatically generate network access policies for new user groups.
 
 
-## 🛠 Improvements & Fixes
+- User Session Expiry Controls – Set session timeouts for both Dashboard and Client Apps.
 
 
-- Auto-Sync DNS Configs: Multi-network DNS configurations now sync automatically between server and clients.
+## Improvements & Fixes 🛠 
 
 
-- Stability Fixes: Improved connection reliability for nodes using Internet Gateways.
+- Access Control Lists (ACLs): Enhanced functionality and flexibility.
 
 
-- LAN/Private Routing Enhancements: Smarter detection and handling of local/private routes, improving peer-to-peer communication in complex network environments.
+- User Management UX: Streamlined workflows for easier administration.
+
+- IDP User/Group Filtering: Improved filtering capabilities for large organizations.
+
+- Stability Enhancements: More reliable connections for nodes using Internet Gateways.
 
 
 ## Known Issues 🐞
 ## Known Issues 🐞
 
 

+ 16 - 8
schema/egress.go

@@ -11,14 +11,16 @@ import (
 const egressTable = "egresses"
 const egressTable = "egresses"
 
 
 type Egress struct {
 type Egress struct {
-	ID          string            `gorm:"primaryKey" json:"id"`
-	Name        string            `gorm:"name" json:"name"`
-	Network     string            `gorm:"network" json:"network"`
-	Description string            `gorm:"description" json:"description"`
-	Nodes       datatypes.JSONMap `gorm:"nodes" json:"nodes"`
-	Tags        datatypes.JSONMap `gorm:"tags" json:"tags"`
-	Range       string            `gorm:"range" json:"range"`
-	Nat         bool              `gorm:"nat" json:"nat"`
+	ID          string                      `gorm:"primaryKey" json:"id"`
+	Name        string                      `gorm:"name" json:"name"`
+	Network     string                      `gorm:"network" json:"network"`
+	Description string                      `gorm:"description" json:"description"`
+	Nodes       datatypes.JSONMap           `gorm:"nodes" json:"nodes"`
+	Tags        datatypes.JSONMap           `gorm:"tags" json:"tags"`
+	Range       string                      `gorm:"range" json:"range"`
+	DomainAns   datatypes.JSONSlice[string] `gorm:"domain_ans" json:"domain_ans"`
+	Domain      string                      `gorm:"domain" json:"domain"`
+	Nat         bool                        `gorm:"nat" json:"nat"`
 	//IsInetGw    bool              `gorm:"is_inet_gw" json:"is_internet_gateway"`
 	//IsInetGw    bool              `gorm:"is_inet_gw" json:"is_internet_gateway"`
 	Status    bool      `gorm:"status" json:"status"`
 	Status    bool      `gorm:"status" json:"status"`
 	CreatedBy string    `gorm:"created_by" json:"created_by"`
 	CreatedBy string    `gorm:"created_by" json:"created_by"`
@@ -63,6 +65,12 @@ func (e *Egress) ListByNetwork(ctx context.Context) (egs []Egress, err error) {
 	return
 	return
 }
 }
 
 
+func (e *Egress) Count(ctx context.Context) (int, error) {
+	var count int64
+	err := db.FromContext(ctx).Model(&Egress{}).Count(&count).Error
+	return int(count), err
+}
+
 func (e *Egress) Delete(ctx context.Context) error {
 func (e *Egress) Delete(ctx context.Context) error {
 	return db.FromContext(ctx).Table(e.Table()).Where("id = ?", e.ID).Delete(&e).Error
 	return db.FromContext(ctx).Table(e.Table()).Where("id = ?", e.ID).Delete(&e).Error
 }
 }

+ 1 - 0
schema/models.go

@@ -7,6 +7,7 @@ func ListModels() []interface{} {
 		&Egress{},
 		&Egress{},
 		&UserAccessToken{},
 		&UserAccessToken{},
 		&Event{},
 		&Event{},
+		&PendingHost{},
 		&Nameserver{},
 		&Nameserver{},
 	}
 	}
 }
 }

+ 46 - 0
schema/pending_hosts.go

@@ -0,0 +1,46 @@
+package schema
+
+import (
+	"context"
+	"time"
+
+	"github.com/gravitl/netmaker/db"
+	"gorm.io/datatypes"
+)
+
+type PendingHost struct {
+	ID            string         `gorm:"id" json:"id"`
+	HostID        string         `gorm:"host_id" json:"host_id"`
+	Hostname      string         `gorm:"host_name" json:"host_name"`
+	Network       string         `gorm:"network" json:"network"`
+	PublicKey     string         `gorm:"public_key" json:"public_key"`
+	EnrollmentKey datatypes.JSON `gorm:"enrollment_key_id" json:"enrollment_key_id"`
+	OS            string         `gorm:"os" json:"os"`
+	Version       string         `gorm:"version" json:"version"`
+	Location      string         `gorm:"location" json:"location"` // Format: "lat,lon"
+	RequestedAt   time.Time      `gorm:"requested_at" json:"requested_at"`
+}
+
+func (p *PendingHost) Get(ctx context.Context) error {
+	return db.FromContext(ctx).Model(&PendingHost{}).First(&p).Where("id = ?", p.ID).Error
+}
+
+func (p *PendingHost) Create(ctx context.Context) error {
+	return db.FromContext(ctx).Model(&PendingHost{}).Create(&p).Error
+}
+
+func (p *PendingHost) List(ctx context.Context) (pendingHosts []PendingHost, err error) {
+	err = db.FromContext(ctx).Model(&PendingHost{}).Find(&pendingHosts).Error
+	return
+}
+
+func (p *PendingHost) Delete(ctx context.Context) error {
+	return db.FromContext(ctx).Model(&PendingHost{}).Where("id = ?", p.ID).Delete(&p).Error
+}
+func (p *PendingHost) CheckIfPendingHostExists(ctx context.Context) error {
+	return db.FromContext(ctx).Model(&PendingHost{}).Where("host_id = ? AND network = ?", p.HostID, p.Network).First(&p).Error
+}
+
+func (p *PendingHost) DeleteAllPendingHosts(ctx context.Context) error {
+	return db.FromContext(ctx).Model(&PendingHost{}).Where("host_id = ?", p.HostID).Delete(&p).Error
+}

+ 1 - 1
scripts/netmaker.default.env

@@ -34,7 +34,7 @@ EXPORTER_API_PORT=8085
 CORS_ALLOWED_ORIGIN=*
 CORS_ALLOWED_ORIGIN=*
 # Show keys permanently in UI (until deleted) as opposed to 1-time display.
 # Show keys permanently in UI (until deleted) as opposed to 1-time display.
 DISPLAY_KEYS=on
 DISPLAY_KEYS=on
-# Database to use - sqlite, postgres, or rqlite
+# Database to use - sqlite, postgres
 DATABASE=sqlite
 DATABASE=sqlite
 # The address of the mq server. If running from docker compose it will be "mq". Otherwise, need to input address.
 # The address of the mq server. If running from docker compose it will be "mq". Otherwise, need to input address.
 # If using "host networking", it will find and detect the IP of the mq container.
 # If using "host networking", it will find and detect the IP of the mq container.

+ 1 - 76
servercfg/serverconf.go

@@ -38,82 +38,7 @@ func SetHost() error {
 	return nil
 	return nil
 }
 }
 
 
-// GetServerConfig - gets the server config into memory from file or env
-func GetServerConfig() config.ServerConfig {
-	var cfg config.ServerConfig
-	cfg.APIConnString = GetAPIConnString()
-	cfg.CoreDNSAddr = GetCoreDNSAddr()
-	cfg.APIHost = GetAPIHost()
-	cfg.APIPort = GetAPIPort()
-	cfg.MasterKey = "(hidden)"
-	cfg.DNSKey = "(hidden)"
-	cfg.AllowedOrigin = GetAllowedOrigin()
-	cfg.RestBackend = "off"
-	cfg.NodeID = GetNodeID()
-	cfg.BrokerType = GetBrokerType()
-	cfg.EmqxRestEndpoint = GetEmqxRestEndpoint()
-	if AutoUpdateEnabled() {
-		cfg.NetclientAutoUpdate = "enabled"
-	} else {
-		cfg.NetclientAutoUpdate = "disabled"
-	}
-	if IsRestBackend() {
-		cfg.RestBackend = "on"
-	}
-	cfg.DNSMode = "off"
-	if IsDNSMode() {
-		cfg.DNSMode = "on"
-	}
-	cfg.DisplayKeys = "off"
-	if IsDisplayKeys() {
-		cfg.DisplayKeys = "on"
-	}
-	cfg.DisableRemoteIPCheck = "off"
-	if DisableRemoteIPCheck() {
-		cfg.DisableRemoteIPCheck = "on"
-	}
-	cfg.Database = GetDB()
-	cfg.Platform = GetPlatform()
-	cfg.Version = GetVersion()
-	cfg.PublicIp = GetServerHostIP()
-
-	// == auth config ==
-	var authInfo = GetAuthProviderInfo()
-	cfg.AuthProvider = authInfo[0]
-	cfg.ClientID = authInfo[1]
-	cfg.ClientSecret = authInfo[2]
-	cfg.FrontendURL = GetFrontendURL()
-	cfg.Telemetry = Telemetry()
-	cfg.Server = GetServer()
-	cfg.Verbosity = GetVerbosity()
-	cfg.IsPro = "no"
-	if IsPro {
-		cfg.IsPro = "yes"
-	}
-	cfg.JwtValidityDuration = GetJwtValidityDuration()
-	cfg.RacRestrictToSingleNetwork = GetRacRestrictToSingleNetwork()
-	cfg.MetricInterval = GetMetricInterval()
-	cfg.ManageDNS = GetManageDNS()
-	cfg.Stun = IsStunEnabled()
-	cfg.StunServers = GetStunServers()
-	cfg.DefaultDomain = GetDefaultDomain()
-	return cfg
-}
-
-// GetJwtValidityDuration - returns the JWT validity duration in seconds
-func GetJwtValidityDuration() time.Duration {
-	var defaultDuration = time.Duration(24) * time.Hour
-	if os.Getenv("JWT_VALIDITY_DURATION") != "" {
-		t, err := strconv.Atoi(os.Getenv("JWT_VALIDITY_DURATION"))
-		if err != nil {
-			return defaultDuration
-		}
-		return time.Duration(t) * time.Second
-	}
-	return defaultDuration
-}
-
-// GetJwtValidityDuration - returns the JWT validity duration in seconds
+// GetJwtValidityDurationFromEnv - returns the JWT validity duration in seconds
 func GetJwtValidityDurationFromEnv() int {
 func GetJwtValidityDurationFromEnv() int {
 	var defaultDuration = 43200
 	var defaultDuration = 43200
 	if os.Getenv("JWT_VALIDITY_DURATION") != "" {
 	if os.Getenv("JWT_VALIDITY_DURATION") != "" {

+ 1 - 1
swagger.yaml

@@ -1511,7 +1511,7 @@ info:
   contact: {}
   contact: {}
   description: NetMaker API Docs
   description: NetMaker API Docs
   title: NetMaker
   title: NetMaker
-  version: 1.0.0
+  version: 1.1.0
 paths:
 paths:
   /api/dns:
   /api/dns:
     get:
     get:

+ 56 - 2
utils/utils.go

@@ -5,8 +5,11 @@ import (
 	"log/slog"
 	"log/slog"
 	"net"
 	"net"
 	"runtime"
 	"runtime"
+	"sort"
 	"strings"
 	"strings"
 	"time"
 	"time"
+
+	"github.com/gravitl/netmaker/models"
 )
 )
 
 
 // RetryStrategy specifies a strategy to retry an operation after waiting a while,
 // RetryStrategy specifies a strategy to retry an operation after waiting a while,
@@ -59,8 +62,8 @@ func TraceCaller() {
 	funcName := runtime.FuncForPC(pc).Name()
 	funcName := runtime.FuncForPC(pc).Name()
 
 
 	// Print trace details
 	// Print trace details
-	slog.Debug("Called from function: %s\n", "func-name", funcName)
-	slog.Debug("File: %s, Line: %d\n", "file", file, "line-no", line)
+	slog.Debug("Called from function: %s\n", "func", funcName)
+	slog.Debug("File: %s, Line: %d\n", "file", file, "line", line)
 }
 }
 
 
 // NoEmptyStringToCsv takes a bunch of strings, filters out empty ones and returns a csv version of the string
 // NoEmptyStringToCsv takes a bunch of strings, filters out empty ones and returns a csv version of the string
@@ -86,3 +89,54 @@ func GetExtClientEndpoint(hostIpv4Endpoint, hostIpv6Endpoint net.IP, hostListenP
 		return fmt.Sprintf("%s:%d", hostIpv4Endpoint.String(), hostListenPort)
 		return fmt.Sprintf("%s:%d", hostIpv4Endpoint.String(), hostListenPort)
 	}
 	}
 }
 }
+
+// SortIfacesByName sorts a slice of Iface by name in ascending order
+func SortIfacesByName(ifaces []models.Iface) {
+	sort.Slice(ifaces, func(i, j int) bool {
+		return ifaces[i].Name < ifaces[j].Name
+	})
+}
+
+// CompareIfaces compares two slices of Iface and returns true if they are equal
+// Two slices are considered equal if they have the same length and all corresponding
+// elements have the same Name, AddressString, and IP address
+func CompareIfaces(ifaces1, ifaces2 []models.Iface) bool {
+	// Check if lengths are different
+	if len(ifaces1) != len(ifaces2) {
+		return false
+	}
+
+	// Compare each element
+	for i := range ifaces1 {
+		if !CompareIface(ifaces1[i], ifaces2[i]) {
+			return false
+		}
+	}
+
+	return true
+}
+
+// CompareIface compares two individual Iface structs and returns true if they are equal
+func CompareIface(iface1, iface2 models.Iface) bool {
+	// Compare Name
+	if iface1.Name != iface2.Name {
+		return false
+	}
+
+	// Compare AddressString
+	if iface1.AddressString != iface2.AddressString {
+		return false
+	}
+
+	// Compare IP addresses
+	if !iface1.Address.IP.Equal(iface2.Address.IP) {
+		return false
+	}
+
+	// Compare network masks
+	if iface1.Address.Mask.String() != iface2.Address.Mask.String() {
+		return false
+	}
+
+	return true
+}