Kaynağa Gözat

Merge branch 'develop' into NET-1991

# Conflicts:
#	go.mod
#	go.sum
#	pro/controllers/users.go
Vishal Dalwadi 3 ay önce
ebeveyn
işleme
16a9dca774
67 değiştirilmiş dosya ile 3053 ekleme ve 492 silme
  1. 1 1
      auth/host_session.go
  2. 43 0
      cli/cmd/access_token/create.go
  3. 23 0
      cli/cmd/access_token/delete.go
  4. 20 0
      cli/cmd/access_token/get.go
  5. 28 0
      cli/cmd/access_token/root.go
  6. 7 4
      cli/cmd/context/set.go
  7. 4 1
      cli/cmd/root.go
  8. 58 0
      cli/functions/access_tokens.go
  9. 1 1
      cli/functions/http_client.go
  10. 58 5
      controllers/acls.go
  11. 1 0
      controllers/controller.go
  12. 310 0
      controllers/egress.go
  13. 76 3
      controllers/enrollmentkeys.go
  14. 26 4
      controllers/ext_client.go
  15. 40 2
      controllers/gateway.go
  16. 82 4
      controllers/hosts.go
  17. 1 1
      controllers/migrate.go
  18. 33 3
      controllers/network.go
  19. 22 10
      controllers/node.go
  20. 0 94
      controllers/node_test.go
  21. 19 0
      controllers/server.go
  22. 54 2
      controllers/tags.go
  23. 170 8
      controllers/user.go
  24. 12 0
      db/db.go
  25. 14 9
      go.mod
  26. 36 16
      go.sum
  27. 349 137
      logic/acls.go
  28. 1 1
      logic/auth.go
  29. 366 0
      logic/egress.go
  30. 16 6
      logic/errors.go
  31. 49 5
      logic/extpeers.go
  32. 28 14
      logic/gateway.go
  33. 19 2
      logic/hosts.go
  34. 2 2
      logic/jwts.go
  35. 0 2
      logic/networks.go
  36. 39 13
      logic/nodes.go
  37. 50 44
      logic/peers.go
  38. 6 4
      logic/relay.go
  39. 1 1
      logic/tags.go
  40. 2 0
      logic/telemetry.go
  41. 0 14
      logic/wireguard.go
  42. 217 0
      migrate/migrate.go
  43. 60 0
      models/accessToken.go
  44. 1 0
      models/acl.go
  45. 6 15
      models/api_node.go
  46. 14 0
      models/egress.go
  47. 74 0
      models/events.go
  48. 1 1
      models/mqtt.go
  49. 14 12
      models/node.go
  50. 1 0
      models/structs.go
  51. 16 1
      pro/auth/azure-ad.go
  52. 16 1
      pro/auth/github.go
  53. 18 0
      pro/auth/google.go
  54. 16 1
      pro/auth/oidc.go
  55. 114 0
      pro/controllers/events.go
  56. 30 4
      pro/controllers/failover.go
  57. 2 2
      pro/controllers/inet_gws.go
  58. 178 5
      pro/controllers/users.go
  59. 4 0
      pro/initialize.go
  60. 47 0
      pro/logic/events.go
  61. 3 2
      pro/logic/failover.go
  62. 21 15
      pro/logic/nodes.go
  63. 2 20
      pro/logic/user_mgmt.go
  64. 4 0
      schema/activity.go
  65. 70 0
      schema/egress.go
  66. 55 0
      schema/event.go
  67. 2 0
      schema/models.go

+ 1 - 1
auth/host_session.go

@@ -165,7 +165,7 @@ func SessionHandler(conn *websocket.Conn) {
 					return
 				}
 			}
-			logic.CheckHostPorts(&result.Host)
+			_ = logic.CheckHostPorts(&result.Host)
 			if err := logic.CreateHost(&result.Host); err != nil {
 				handleHostRegErr(conn, err)
 				return

+ 43 - 0
cli/cmd/access_token/create.go

@@ -0,0 +1,43 @@
+package access_token
+
+import (
+	"time"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/schema"
+	"github.com/spf13/cobra"
+)
+
+var accessTokenCreateCmd = &cobra.Command{
+	Use:   "create [token-name]",
+	Short: "Create an access token",
+	Long:  `Create an access token for a user`,
+	Args:  cobra.ExactArgs(1),
+	Run: func(cmd *cobra.Command, args []string) {
+		userName, _ := cmd.Flags().GetString("user")
+		expiresAt, _ := cmd.Flags().GetString("expires")
+
+		accessToken := &schema.UserAccessToken{}
+		accessToken.Name = args[0]
+		accessToken.UserName = userName
+
+		expTime := time.Now().Add(time.Hour * 24 * 365) // default to 1 year
+		if expiresAt != "" {
+			var err error
+			expTime, err = time.Parse(time.RFC3339, expiresAt)
+			if err != nil {
+				cmd.PrintErrf("Invalid expiration time format. Please use RFC3339 format (e.g. 2024-01-01T00:00:00Z). Using default 1 year.\n")
+			}
+		}
+		accessToken.ExpiresAt = expTime
+
+		functions.PrettyPrint(functions.CreateAccessToken(accessToken))
+	},
+}
+
+func init() {
+	accessTokenCreateCmd.Flags().String("user", "", "Username to create token for")
+	accessTokenCreateCmd.Flags().String("expires", "", "Expiration time for the token in RFC3339 format (e.g. 2024-01-01T00:00:00Z). Defaults to 1 year from now.")
+	accessTokenCreateCmd.MarkFlagRequired("user")
+	rootCmd.AddCommand(accessTokenCreateCmd)
+}

+ 23 - 0
cli/cmd/access_token/delete.go

@@ -0,0 +1,23 @@
+package access_token
+
+import (
+	"fmt"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var accessTokenDeleteCmd = &cobra.Command{
+	Use:   "delete [ACCESS TOKEN ID]",
+	Short: "Delete an access token",
+	Long:  `Delete an access token by ID`,
+	Args:  cobra.ExactArgs(1),
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.DeleteAccessToken(args[0])
+		fmt.Println("Access token deleted successfully")
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(accessTokenDeleteCmd)
+}

+ 20 - 0
cli/cmd/access_token/get.go

@@ -0,0 +1,20 @@
+package access_token
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var accessTokenGetCmd = &cobra.Command{
+	Use:   "get [USERNAME]",
+	Short: "Get a user's access token",
+	Long:  `Get a user's access token`,
+	Args:  cobra.ExactArgs(1),
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.GetAccessToken(args[0]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(accessTokenGetCmd)
+}

+ 28 - 0
cli/cmd/access_token/root.go

@@ -0,0 +1,28 @@
+package access_token
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "access_token",
+	Short: "Manage Netmaker user access tokens",
+	Long:  `Manage a Netmaker user's access tokens. This command allows you to create, delete, and list access tokens for a user.`,
+}
+
+// GetRoot returns the root subcommand
+func GetRoot() *cobra.Command {
+	return rootCmd
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}

+ 7 - 4
cli/cmd/context/set.go

@@ -17,6 +17,7 @@ var (
 	sso       bool
 	tenantId  string
 	saas      bool
+	authToken string
 )
 
 var contextSetCmd = &cobra.Command{
@@ -30,13 +31,14 @@ var contextSetCmd = &cobra.Command{
 			Username:  username,
 			Password:  password,
 			MasterKey: masterKey,
+			AuthToken: authToken,
 			SSO:       sso,
 			TenantId:  tenantId,
 			Saas:      saas,
 		}
 		if !ctx.Saas {
-			if ctx.Username == "" && ctx.MasterKey == "" && !ctx.SSO {
-				log.Fatal("Either username/password or master key is required")
+			if ctx.Username == "" && ctx.MasterKey == "" && !ctx.SSO && ctx.AuthToken == "" {
+				log.Fatal("Either username/password or master key or auth token is required")
 				cmd.Usage()
 			}
 			if ctx.Endpoint == "" {
@@ -49,8 +51,8 @@ var contextSetCmd = &cobra.Command{
 				cmd.Usage()
 			}
 			ctx.Endpoint = fmt.Sprintf(functions.TenantUrlTemplate, tenantId)
-			if ctx.Username == "" && ctx.Password == "" && !ctx.SSO {
-				log.Fatal("Username/password is required for non-SSO SaaS contexts")
+			if ctx.Username == "" && ctx.Password == "" && ctx.AuthToken == "" && !ctx.SSO {
+				log.Fatal("Username/password or authtoken is required for non-SSO SaaS contexts")
 				cmd.Usage()
 			}
 		}
@@ -62,6 +64,7 @@ func init() {
 	contextSetCmd.Flags().StringVar(&endpoint, "endpoint", "", "Endpoint of the API Server")
 	contextSetCmd.Flags().StringVar(&username, "username", "", "Username")
 	contextSetCmd.Flags().StringVar(&password, "password", "", "Password")
+	contextSetCmd.Flags().StringVar(&authToken, "auth_token", "", "Auth Token")
 	contextSetCmd.MarkFlagsRequiredTogether("username", "password")
 	contextSetCmd.Flags().BoolVar(&sso, "sso", false, "Login via Single Sign On (SSO)?")
 	contextSetCmd.Flags().StringVar(&masterKey, "master_key", "", "Master Key")

+ 4 - 1
cli/cmd/root.go

@@ -1,9 +1,9 @@
 package cmd
 
 import (
-	"github.com/gravitl/netmaker/cli/cmd/gateway"
 	"os"
 
+	"github.com/gravitl/netmaker/cli/cmd/access_token"
 	"github.com/gravitl/netmaker/cli/cmd/acl"
 	"github.com/gravitl/netmaker/cli/cmd/commons"
 	"github.com/gravitl/netmaker/cli/cmd/context"
@@ -11,12 +11,14 @@ import (
 	"github.com/gravitl/netmaker/cli/cmd/enrollment_key"
 	"github.com/gravitl/netmaker/cli/cmd/ext_client"
 	"github.com/gravitl/netmaker/cli/cmd/failover"
+	"github.com/gravitl/netmaker/cli/cmd/gateway"
 	"github.com/gravitl/netmaker/cli/cmd/host"
 	"github.com/gravitl/netmaker/cli/cmd/metrics"
 	"github.com/gravitl/netmaker/cli/cmd/network"
 	"github.com/gravitl/netmaker/cli/cmd/node"
 	"github.com/gravitl/netmaker/cli/cmd/server"
 	"github.com/gravitl/netmaker/cli/cmd/user"
+
 	"github.com/spf13/cobra"
 )
 
@@ -57,4 +59,5 @@ func init() {
 	rootCmd.AddCommand(enrollment_key.GetRoot())
 	rootCmd.AddCommand(failover.GetRoot())
 	rootCmd.AddCommand(gateway.GetRoot())
+	rootCmd.AddCommand(access_token.GetRoot())
 }

+ 58 - 0
cli/functions/access_tokens.go

@@ -0,0 +1,58 @@
+package functions
+
+import (
+	"encoding/json"
+	"log"
+	"net/http"
+
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/schema"
+)
+
+// CreateAccessToken - creates an access token for a user
+func CreateAccessToken(payload *schema.UserAccessToken) *models.SuccessfulUserLoginResponse {
+	res := request[models.SuccessResponse](http.MethodPost, "/api/v1/users/access_token", payload)
+	if res.Code != http.StatusOK {
+		log.Fatalf("Error creating access token: %s", res.Message)
+	}
+
+	var token models.SuccessfulUserLoginResponse
+	responseBytes, err := json.Marshal(res.Response)
+	if err != nil {
+		log.Fatalf("Error marshaling response: %v", err)
+	}
+
+	if err := json.Unmarshal(responseBytes, &token); err != nil {
+		log.Fatalf("Error unmarshaling token: %v", err)
+	}
+
+	return &token
+}
+
+// GetAccessToken - fetch all access tokens per user
+func GetAccessToken(userName string) []schema.UserAccessToken {
+	res := request[models.SuccessResponse](http.MethodGet, "/api/v1/users/access_token?username="+userName, nil)
+	if res.Code != http.StatusOK {
+		log.Fatalf("Error getting access token: %s", res.Message)
+	}
+
+	var tokens []schema.UserAccessToken
+	responseBytes, err := json.Marshal(res.Response)
+	if err != nil {
+		log.Fatalf("Error marshaling response: %v", err)
+	}
+
+	if err := json.Unmarshal(responseBytes, &tokens); err != nil {
+		log.Fatalf("Error unmarshaling tokens: %v", err)
+	}
+
+	return tokens
+}
+
+// DeleteAccessToken - delete an access token
+func DeleteAccessToken(id string) {
+	res := request[models.SuccessResponse](http.MethodDelete, "/api/v1/users/access_token?id="+id, nil)
+	if res.Code != http.StatusOK {
+		log.Fatalf("Error deleting access token: %s", res.Message)
+	}
+}

+ 1 - 1
cli/functions/http_client.go

@@ -192,7 +192,7 @@ retry:
 	body := new(T)
 	if len(resBodyBytes) > 0 {
 		if err := json.Unmarshal(resBodyBytes, body); err != nil {
-			log.Fatalf("Error unmarshalling JSON: %s", err)
+			log.Fatalf("Error unmarshalling JSON: %s %s", err, string(resBodyBytes))
 		}
 	}
 	return body

+ 58 - 5
controllers/acls.go

@@ -51,7 +51,7 @@ func aclPolicyTypes(w http.ResponseWriter, r *http.Request) {
 		DstGroupTypes: []models.AclGroupType{
 			models.NodeTagID,
 			models.NodeID,
-			models.EgressRange,
+			models.EgressID,
 			// models.NetmakerIPAclID,
 			// models.NetmakerSubNetRangeAClID,
 		},
@@ -171,6 +171,7 @@ func aclDebug(w http.ResponseWriter, r *http.Request) {
 		IsPeerAllowed bool
 		Policies      []models.Acl
 		IngressRules  []models.FwRule
+		NodeAllPolicy bool
 	}
 
 	allowed, ps := logic.IsNodeAllowedToCommunicateV1(node, peer, true)
@@ -253,8 +254,8 @@ func createAcl(w http.ResponseWriter, r *http.Request) {
 		acl.Proto = models.ALL
 	}
 	// validate create acl policy
-	if !logic.IsAclPolicyValid(acl) {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("invalid policy"), "badrequest"))
+	if err := logic.IsAclPolicyValid(acl); err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
 	err = logic.InsertAcl(acl)
@@ -267,6 +268,22 @@ func createAcl(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Create,
+		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:   acl.ID,
+			Name: acl.Name,
+			Type: models.AclSub,
+		},
+		NetworkID: acl.NetworkID,
+		Origin:    models.Dashboard,
+	})
 	go mq.PublishPeerUpdate(true)
 	logic.ReturnSuccessResponseWithJson(w, r, acl, "created acl successfully")
 }
@@ -292,8 +309,8 @@ func updateAcl(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
-	if !logic.IsAclPolicyValid(updateAcl.Acl) {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("invalid policy"), "badrequest"))
+	if err := logic.IsAclPolicyValid(updateAcl.Acl); err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
 	if updateAcl.Acl.NetworkID != acl.NetworkID {
@@ -309,6 +326,26 @@ func updateAcl(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
+	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:   acl.ID,
+			Name: acl.Name,
+			Type: models.AclSub,
+		},
+		Diff: models.Diff{
+			Old: acl,
+			New: updateAcl.Acl,
+		},
+		NetworkID: acl.NetworkID,
+		Origin:    models.Dashboard,
+	})
 	go mq.PublishPeerUpdate(true)
 	logic.ReturnSuccessResponse(w, r, "updated acl "+acl.Name)
 }
@@ -340,6 +377,22 @@ func deleteAcl(w http.ResponseWriter, r *http.Request) {
 			logic.FormatError(errors.New("cannot delete default policy"), "internal"))
 		return
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		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:   acl.ID,
+			Name: acl.Name,
+			Type: models.AclSub,
+		},
+		NetworkID: acl.NetworkID,
+		Origin:    models.Dashboard,
+	})
 	go mq.PublishPeerUpdate(true)
 	logic.ReturnSuccessResponse(w, r, "deleted acl "+acl.Name)
 }

+ 1 - 0
controllers/controller.go

@@ -39,6 +39,7 @@ var HttpHandlers = []interface{}{
 	enrollmentKeyHandlers,
 	tagHandlers,
 	aclHandlers,
+	egressHandlers,
 	legacyHandlers,
 }
 

+ 310 - 0
controllers/egress.go

@@ -0,0 +1,310 @@
+package controller
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"net/http"
+	"time"
+
+	"github.com/google/uuid"
+	"github.com/gorilla/mux"
+	"github.com/gravitl/netmaker/db"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/mq"
+	"github.com/gravitl/netmaker/schema"
+	"gorm.io/datatypes"
+)
+
+func egressHandlers(r *mux.Router) {
+	r.HandleFunc("/api/v1/egress", logic.SecurityCheck(true, http.HandlerFunc(createEgress))).Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/egress", logic.SecurityCheck(true, http.HandlerFunc(listEgress))).Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/egress", logic.SecurityCheck(true, http.HandlerFunc(updateEgress))).Methods(http.MethodPut)
+	r.HandleFunc("/api/v1/egress", logic.SecurityCheck(true, http.HandlerFunc(deleteEgress))).Methods(http.MethodDelete)
+}
+
+// @Summary     Create Egress Resource
+// @Router      /api/v1/egress [post]
+// @Tags        Auth
+// @Accept      json
+// @Param       body body models.Egress
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     401 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func createEgress(w http.ResponseWriter, r *http.Request) {
+
+	var req models.EgressReq
+	err := json.NewDecoder(r.Body).Decode(&req)
+	if err != nil {
+		logger.Log(0, "error decoding request body: ",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	var egressRange string
+	if !req.IsInetGw {
+		egressRange, err = logic.NormalizeCIDR(req.Range)
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+			return
+		}
+	} else {
+		egressRange = "*"
+	}
+
+	e := schema.Egress{
+		ID:          uuid.New().String(),
+		Name:        req.Name,
+		Network:     req.Network,
+		Description: req.Description,
+		Range:       egressRange,
+		Nat:         req.Nat,
+		IsInetGw:    req.IsInetGw,
+		Nodes:       make(datatypes.JSONMap),
+		Tags:        make(datatypes.JSONMap),
+		Status:      true,
+		CreatedBy:   r.Header.Get("user"),
+		CreatedAt:   time.Now().UTC(),
+	}
+	for nodeID, metric := range req.Nodes {
+		e.Nodes[nodeID] = metric
+	}
+	if err := logic.ValidateEgressReq(&e); err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	err = e.Create(db.WithContext(r.Context()))
+	if err != nil {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("error creating egress resource"+err.Error()), "internal"),
+		)
+		return
+	}
+	logic.LogEvent(&models.Event{
+		Action: models.Create,
+		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:   e.ID,
+			Name: e.Name,
+			Type: models.EgressSub,
+		},
+		NetworkID: models.NetworkID(e.Network),
+		Origin:    models.Dashboard,
+	})
+	// for nodeID := range e.Nodes {
+	// 	node, err := logic.GetNodeByID(nodeID)
+	// 	if err != nil {
+	// 		logic.AddEgressInfoToNode(&node, e)
+	// 		logic.UpsertNode(&node)
+	// 	}
+
+	// }
+	go mq.PublishPeerUpdate(false)
+	logic.ReturnSuccessResponseWithJson(w, r, e, "created egress resource")
+}
+
+// @Summary     List Egress Resource
+// @Router      /api/v1/egress [get]
+// @Tags        Auth
+// @Accept      json
+// @Param       query network string
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     401 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func listEgress(w http.ResponseWriter, r *http.Request) {
+
+	network := r.URL.Query().Get("network")
+	if network == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("network is required"), "badrequest"))
+		return
+	}
+	e := schema.Egress{Network: network}
+	list, err := e.ListByNetwork(db.WithContext(r.Context()))
+	if err != nil {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("error listing egress resource"+err.Error()), "internal"),
+		)
+		return
+	}
+	logic.ReturnSuccessResponseWithJson(w, r, list, "fetched egress resource list")
+}
+
+// @Summary     Update Egress Resource
+// @Router      /api/v1/egress [put]
+// @Tags        Auth
+// @Accept      json
+// @Param       body body models.Egress
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     401 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func updateEgress(w http.ResponseWriter, r *http.Request) {
+
+	var req models.EgressReq
+	err := json.NewDecoder(r.Body).Decode(&req)
+	if err != nil {
+		logger.Log(0, "error decoding request body: ",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	var egressRange string
+	if !req.IsInetGw {
+		egressRange, err = logic.NormalizeCIDR(req.Range)
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+			return
+		}
+	} else {
+		egressRange = "*"
+	}
+
+	e := schema.Egress{ID: req.ID}
+	err = e.Get(db.WithContext(r.Context()))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	var updateNat bool
+	var updateInetGw bool
+	var updateStatus bool
+	if req.Nat != e.Nat {
+		updateNat = true
+	}
+	if req.IsInetGw != e.IsInetGw {
+		updateInetGw = true
+	}
+	if req.Status != e.Status {
+		updateStatus = true
+	}
+	event := &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:   e.ID,
+			Name: e.Name,
+			Type: models.EgressSub,
+		},
+		Diff: models.Diff{
+			Old: e,
+		},
+		NetworkID: models.NetworkID(e.Network),
+		Origin:    models.Dashboard,
+	}
+	e.Nodes = make(datatypes.JSONMap)
+	e.Tags = make(datatypes.JSONMap)
+	for nodeID, metric := range req.Nodes {
+		e.Nodes[nodeID] = metric
+	}
+	e.Range = egressRange
+	e.Description = req.Description
+	e.Name = req.Name
+	e.Nat = req.Nat
+	e.Status = req.Status
+	e.IsInetGw = req.IsInetGw
+	e.UpdatedAt = time.Now().UTC()
+	if err := logic.ValidateEgressReq(&e); err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	err = e.Update(db.WithContext(context.TODO()))
+	if err != nil {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("error creating egress resource"+err.Error()), "internal"),
+		)
+		return
+	}
+	if updateNat {
+		e.Nat = req.Nat
+		e.UpdateNatStatus(db.WithContext(context.TODO()))
+	}
+	if updateInetGw {
+		e.IsInetGw = req.IsInetGw
+		e.UpdateINetGwStatus(db.WithContext(context.TODO()))
+	}
+	if updateStatus {
+		e.Status = req.Status
+		e.UpdateEgressStatus(db.WithContext(context.TODO()))
+	}
+	event.Diff.New = e
+	logic.LogEvent(event)
+	go mq.PublishPeerUpdate(false)
+	logic.ReturnSuccessResponseWithJson(w, r, e, "updated egress resource")
+}
+
+// @Summary     Delete Egress Resource
+// @Router      /api/v1/egress [delete]
+// @Tags        Auth
+// @Accept      json
+// @Param       body body models.Egress
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     401 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func deleteEgress(w http.ResponseWriter, r *http.Request) {
+
+	id := r.URL.Query().Get("id")
+	if id == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("id is required"), "badrequest"))
+		return
+	}
+	e := schema.Egress{ID: id}
+	err := e.Delete(db.WithContext(r.Context()))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		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:   e.ID,
+			Name: e.Name,
+			Type: models.EgressSub,
+		},
+		NetworkID: models.NetworkID(e.Network),
+		Origin:    models.Dashboard,
+	})
+	// delete related acl policies
+	acls := logic.ListAcls()
+	for _, acl := range acls {
+
+		for i := len(acl.Dst) - 1; i >= 0; i-- {
+			if acl.Dst[i].ID == models.EgressID && acl.Dst[i].Value == id {
+				acl.Dst = append(acl.Dst[:i], acl.Dst[i+1:]...)
+			}
+		}
+		if len(acl.Dst) == 0 {
+			logic.DeleteAcl(acl)
+		} else {
+			logic.UpsertAcl(acl)
+		}
+	}
+	go mq.PublishPeerUpdate(false)
+	logic.ReturnSuccessResponseWithJson(w, r, nil, "deleted egress resource")
+}

+ 76 - 3
controllers/enrollmentkeys.go

@@ -72,12 +72,32 @@ func getEnrollmentKeys(w http.ResponseWriter, r *http.Request) {
 func deleteEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 	params := mux.Vars(r)
 	keyID := params["keyID"]
-	err := logic.DeleteEnrollmentKey(keyID, false)
+	key, err := logic.GetEnrollmentKey(keyID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	err = logic.DeleteEnrollmentKey(keyID, false)
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "failed to remove enrollment key: ", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		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:   keyID,
+			Name: key.Tags[0],
+			Type: models.EnrollmentKeySub,
+		},
+		Origin: models.Dashboard,
+	})
 	logger.Log(2, r.Header.Get("user"), "deleted enrollment key", keyID)
 	w.WriteHeader(http.StatusOK)
 }
@@ -173,6 +193,21 @@ func createEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Create,
+		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:   newEnrollmentKey.Value,
+			Name: newEnrollmentKey.Tags[0],
+			Type: models.EnrollmentKeySub,
+		},
+		Origin: models.Dashboard,
+	})
 	logger.Log(2, r.Header.Get("user"), "created enrollment key")
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(newEnrollmentKey)
@@ -208,6 +243,7 @@ func updateEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 	}
+	currKey, _ := logic.GetEnrollmentKey(keyId)
 
 	newEnrollmentKey, err := logic.UpdateEnrollmentKey(keyId, relayId, enrollmentKeyBody.Groups)
 	if err != nil {
@@ -221,7 +257,25 @@ func updateEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-
+	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:   newEnrollmentKey.Value,
+			Name: newEnrollmentKey.Tags[0],
+			Type: models.EnrollmentKeySub,
+		},
+		Diff: models.Diff{
+			Old: currKey,
+			New: newEnrollmentKey,
+		},
+		Origin: models.Dashboard,
+	})
 	slog.Info("updated enrollment key", "id", keyId)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(newEnrollmentKey)
@@ -302,7 +356,7 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 	if !hostExists {
 		newHost.PersistentKeepalive = models.DefaultPersistentKeepAlive
 		// register host
-		logic.CheckHostPorts(&newHost)
+		_ = logic.CheckHostPorts(&newHost)
 		// create EMQX credentials and ACLs for host
 		if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
 			if err := mq.GetEmqxHandler().CreateEmqxUser(newHost.ID.String(), newHost.HostPass); err != nil {
@@ -355,6 +409,25 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 		ServerConf:    server,
 		RequestedHost: newHost,
 	}
+	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, newHost.Name, newHost.ID.String(), "registered with Netmaker")
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(&response)

+ 26 - 4
controllers/ext_client.go

@@ -174,6 +174,7 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+	logic.GetNodeEgressInfo(&gwnode)
 	host, err := logic.GetHost(gwnode.HostID.String())
 	if err != nil {
 		logger.Log(
@@ -261,7 +262,7 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) {
 	}
 
 	var newAllowedIPs string
-	if logic.IsInternetGw(gwnode) || gwnode.InternetGwID != "" {
+	if logic.IsInternetGw(gwnode) || gwnode.EgressDetails.InternetGwID != "" {
 		egressrange := "0.0.0.0/0"
 		if gwnode.Address6.IP != nil && client.Address6 != "" {
 			egressrange += "," + "::/0"
@@ -540,7 +541,7 @@ func getExtClientHAConf(w http.ResponseWriter, r *http.Request) {
 		keepalive = "PersistentKeepalive = " + strconv.Itoa(int(gwnode.IngressPersistentKeepalive))
 	}
 	var newAllowedIPs string
-	if logic.IsInternetGw(gwnode) || gwnode.InternetGwID != "" {
+	if logic.IsInternetGw(gwnode) || gwnode.EgressDetails.InternetGwID != "" {
 		egressrange := "0.0.0.0/0"
 		if gwnode.Address6.IP != nil && client.Address6 != "" {
 			egressrange += "," + "::/0"
@@ -688,7 +689,7 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 	var gateway models.EgressGatewayRequest
 	gateway.NetID = params["network"]
 	gateway.Ranges = customExtClient.ExtraAllowedIPs
-	err := logic.ValidateEgressRange(gateway)
+	err := logic.ValidateEgressRange(gateway.NetID, gateway.Ranges)
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "error validating egress range: ", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
@@ -798,6 +799,27 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 		"clientid",
 		extclient.ClientID,
 	)
+	if extclient.RemoteAccessClientID != "" {
+		// if created by user from client app, log event
+		logic.LogEvent(&models.Event{
+			Action: models.Connect,
+			Source: models.Subject{
+				ID:   userName,
+				Name: userName,
+				Type: models.UserSub,
+			},
+			TriggeredBy: userName,
+			Target: models.Subject{
+				ID:   extclient.Network,
+				Name: extclient.Network,
+				Type: models.NetworkSub,
+				Info: extclient,
+			},
+			NetworkID: models.NetworkID(extclient.Network),
+			Origin:    models.ClientApp,
+		})
+	}
+
 	w.WriteHeader(http.StatusOK)
 	go func() {
 		if err := logic.SetClientDefaultACLs(&extclient); err != nil {
@@ -876,7 +898,7 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 	var gateway models.EgressGatewayRequest
 	gateway.NetID = params["network"]
 	gateway.Ranges = update.ExtraAllowedIPs
-	err = logic.ValidateEgressRange(gateway)
+	err = logic.ValidateEgressRange(gateway.NetID, gateway.Ranges)
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "error validating egress range: ", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))

+ 40 - 2
controllers/gateway.go

@@ -39,6 +39,11 @@ func createGateway(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
+	host, err := logic.GetHost(node.HostID.String())
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
 	var req models.CreateGwReq
 	err = json.NewDecoder(r.Body).Decode(&req)
 	if err != nil {
@@ -89,7 +94,21 @@ func createGateway(w http.ResponseWriter, r *http.Request) {
 	)
 	logic.GetNodeStatus(&relayNode, false)
 	apiNode := relayNode.ConvertToAPINode()
-
+	logic.LogEvent(&models.Event{
+		Action: models.Create,
+		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:   node.ID.String(),
+			Name: host.Name,
+			Type: models.GatewaySub,
+		},
+		Origin: models.Dashboard,
+	})
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(apiNode)
 	go func() {
@@ -138,6 +157,11 @@ func deleteGateway(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+	host, err := logic.GetHost(node.HostID.String())
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
 	node.IsGw = false
 	logic.UpsertNode(&node)
 	logger.Log(1, r.Header.Get("user"), "deleted gw", nodeid, "on network", netid)
@@ -200,7 +224,21 @@ func deleteGateway(w http.ResponseWriter, r *http.Request) {
 		}
 
 	}()
-
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		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:   node.ID.String(),
+			Name: host.Name,
+			Type: models.GatewaySub,
+		},
+		Origin: models.Dashboard,
+	})
 	logic.GetNodeStatus(&node, false)
 	apiNode := node.ConvertToAPINode()
 	logger.Log(1, r.Header.Get("user"), "deleted ingress gateway", nodeid)

+ 82 - 4
controllers/hosts.go

@@ -216,7 +216,7 @@ func pull(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(keyErr, "internal"))
 		return
 	}
-
+	_ = logic.CheckHostPorts(host)
 	serverConf.TrafficKey = key
 	response := models.HostPull{
 		Host:              *host,
@@ -294,7 +294,25 @@ func updateHost(w http.ResponseWriter, r *http.Request) {
 			}
 		}
 	}()
-
+	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:   currHost.ID.String(),
+			Name: newHost.Name,
+			Type: models.DeviceSub,
+		},
+		Diff: models.Diff{
+			Old: currHost,
+			New: newHost,
+		},
+		Origin: models.Dashboard,
+	})
 	apiHostData := newHost.ConvertNMHostToAPI()
 	logger.Log(2, r.Header.Get("user"), "updated host", newHost.ID.String())
 	w.WriteHeader(http.StatusOK)
@@ -420,7 +438,21 @@ func deleteHost(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		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:   currHost.ID.String(),
+			Name: currHost.Name,
+			Type: models.DeviceSub,
+		},
+		Origin: models.Dashboard,
+	})
 	apiHostData := currHost.ConvertNMHostToAPI()
 	logger.Log(2, r.Header.Get("user"), "removed host", currHost.Name)
 	w.WriteHeader(http.StatusOK)
@@ -492,6 +524,22 @@ func addHostToNetwork(w http.ResponseWriter, r *http.Request) {
 		r.Header.Get("user"),
 		fmt.Sprintf("added host %s to network %s", currHost.Name, network),
 	)
+	logic.LogEvent(&models.Event{
+		Action: models.JoinHostToNet,
+		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:   currHost.ID.String(),
+			Name: currHost.Name,
+			Type: models.DeviceSub,
+		},
+		NetworkID: models.NetworkID(network),
+		Origin:    models.Dashboard,
+	})
 	w.WriteHeader(http.StatusOK)
 }
 
@@ -623,6 +671,22 @@ func deleteHostFromNetwork(w http.ResponseWriter, r *http.Request) {
 			logic.SetDNS()
 		}
 	}()
+	logic.LogEvent(&models.Event{
+		Action: models.RemoveHostFromNet,
+		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:   currHost.ID.String(),
+			Name: currHost.Name,
+			Type: models.DeviceSub,
+		},
+		NetworkID: models.NetworkID(network),
+		Origin:    models.Dashboard,
+	})
 	logger.Log(
 		2,
 		r.Header.Get("user"),
@@ -937,7 +1001,21 @@ func syncHost(w http.ResponseWriter, r *http.Request) {
 			slog.Error("failed to send host pull request", "host", host.ID.String(), "error", err)
 		}
 	}()
-
+	logic.LogEvent(&models.Event{
+		Action: models.Sync,
+		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:   host.ID.String(),
+			Name: host.Name,
+			Type: models.DeviceSub,
+		},
+		Origin: models.Dashboard,
+	})
 	slog.Info("requested host pull", "user", r.Header.Get("user"), "host", host.ID.String())
 	w.WriteHeader(http.StatusOK)
 }

+ 1 - 1
controllers/migrate.go

@@ -208,7 +208,7 @@ func convertLegacyNode(legacy models.LegacyNode, hostID uuid.UUID) models.Node {
 	node.IsRelay = false
 	node.RelayedNodes = []string{}
 	node.DNSOn = models.ParseBool(legacy.DNSOn)
-	node.LastModified = time.Now()
+	node.LastModified = time.Now().UTC()
 	node.ExpirationDateTime = time.Unix(legacy.ExpirationDateTime, 0)
 	node.EgressGatewayNatEnabled = models.ParseBool(legacy.EgressGatewayNatEnabled)
 	node.EgressGatewayRequest = legacy.EgressGatewayRequest

+ 33 - 3
controllers/network.go

@@ -483,9 +483,9 @@ func deleteNetwork(w http.ResponseWriter, r *http.Request) {
 	}
 	err = logic.DeleteNetwork(network, force, doneCh)
 	if err != nil {
-		errtype := "badrequest"
+		errtype := logic.BadReq
 		if strings.Contains(err.Error(), "Node check failed") {
-			errtype = "forbidden"
+			errtype = logic.Forbidden
 		}
 		logger.Log(0, r.Header.Get("user"),
 			fmt.Sprintf("failed to delete network [%s]: %v", network, err))
@@ -514,6 +514,21 @@ func deleteNetwork(w http.ResponseWriter, r *http.Request) {
 			logic.SetDNS()
 		}
 	}()
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		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:   network,
+			Name: network,
+			Type: models.NetworkSub,
+		},
+		Origin: models.Dashboard,
+	})
 	logger.Log(1, r.Header.Get("user"), "deleted network", network)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode("success")
@@ -636,7 +651,22 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
 			logger.Log(1, "failed to publish peer update for default hosts after network is added")
 		}
 	}()
-
+	logic.LogEvent(&models.Event{
+		Action: models.Create,
+		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:   network.NetID,
+			Name: network.NetID,
+			Type: models.NetworkSub,
+			Info: network,
+		},
+		Origin: models.Dashboard,
+	})
 	logger.Log(1, r.Header.Get("user"), "created network", network.NetID)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(network)

+ 22 - 10
controllers/node.go

@@ -178,7 +178,7 @@ func Authorize(
 			// check if host instead of user
 			if hostAllowed {
 				// TODO --- should ensure that node is only operating on itself
-				if hostID, _, _, err := logic.VerifyHostToken(authToken); err == nil {
+				if hostID, macAddr, _, err := logic.VerifyHostToken(authToken); err == nil && macAddr != "" {
 					r.Header.Set(hostIDHeader, hostID)
 					// this indicates request is from a node
 					// used for failover - if a getNode comes from node, this will trigger a metrics wipe
@@ -516,7 +516,7 @@ func createEgressGateway(w http.ResponseWriter, r *http.Request) {
 	}
 	gateway.NetID = params["network"]
 	gateway.NodeID = params["nodeid"]
-	err = logic.ValidateEgressRange(gateway)
+	err = logic.ValidateEgressRange(gateway.NetID, gateway.Ranges)
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "error validating egress range: ", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
@@ -638,13 +638,6 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 		)
 		return
 	}
-	if newNode.IsInternetGateway != currentNode.IsInternetGateway {
-		if newNode.IsInternetGateway {
-			logic.SetInternetGw(newNode, models.InetNodeReq{})
-		} else {
-			logic.UnsetInternetGw(newNode)
-		}
-	}
 	relayUpdate := logic.RelayUpdates(&currentNode, newNode)
 	if relayUpdate && newNode.IsRelay {
 		err = logic.ValidateRelay(models.RelayRequest{
@@ -657,7 +650,7 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 	}
-	_, err = logic.GetHost(newNode.HostID.String())
+	host, err := logic.GetHost(newNode.HostID.String())
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"),
 			fmt.Sprintf("failed to get host for node  [ %s ] info: %v", nodeid, err))
@@ -689,6 +682,25 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 		"on network",
 		currentNode.Network,
 	)
+	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:   newNode.ID.String(),
+			Name: host.Name,
+			Type: models.NodeSub,
+		},
+		Diff: models.Diff{
+			Old: currentNode,
+			New: newNode,
+		},
+		Origin: models.Dashboard,
+	})
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(apiNode)
 	go func(aclUpdate, relayupdate bool, newNode *models.Node) {

+ 0 - 94
controllers/node_test.go

@@ -18,100 +18,6 @@ import (
 var nonLinuxHost models.Host
 var linuxHost models.Host
 
-func TestCreateEgressGateway(t *testing.T) {
-	var gateway models.EgressGatewayRequest
-	gateway.Ranges = []string{"10.100.100.0/24"}
-	gateway.RangesWithMetric = append(gateway.RangesWithMetric, models.EgressRangeMetric{
-		Network:     "10.100.100.0/24",
-		RouteMetric: 256,
-	})
-	gateway.NetID = "skynet"
-	deleteAllNetworks()
-	createNet()
-	t.Run("NoNodes", func(t *testing.T) {
-		node, err := logic.CreateEgressGateway(gateway)
-		assert.Equal(t, models.Node{}, node)
-		assert.EqualError(t, err, "could not find any records")
-	})
-	t.Run("Non-linux node", func(t *testing.T) {
-		createnode := createNodeWithParams("", "")
-		createNodeHosts()
-		createnode.HostID = nonLinuxHost.ID
-		err := logic.AssociateNodeToHost(createnode, &nonLinuxHost)
-		assert.Nil(t, err)
-		gateway.NodeID = createnode.ID.String()
-		node, err := logic.CreateEgressGateway(gateway)
-		assert.Equal(t, models.Node{}, node)
-		assert.EqualError(t, err, "windows is unsupported for egress gateways")
-	})
-	t.Run("Success-Nat-Enabled", func(t *testing.T) {
-		deleteAllNodes()
-		testnode := createTestNode()
-		gateway.NodeID = testnode.ID.String()
-		gateway.NatEnabled = "yes"
-
-		node, err := logic.CreateEgressGateway(gateway)
-		t.Log(node.EgressGatewayNatEnabled)
-		assert.Nil(t, err)
-	})
-	t.Run("Success-Nat-Disabled", func(t *testing.T) {
-		deleteAllNodes()
-		testnode := createTestNode()
-		gateway.NodeID = testnode.ID.String()
-		gateway.NatEnabled = "no"
-
-		node, err := logic.CreateEgressGateway(gateway)
-		t.Log(node.EgressGatewayNatEnabled)
-		assert.Nil(t, err)
-	})
-	t.Run("Success", func(t *testing.T) {
-		var gateway models.EgressGatewayRequest
-		gateway.Ranges = []string{"10.100.100.0/24"}
-		gateway.NetID = "skynet"
-		deleteAllNodes()
-		testnode := createTestNode()
-		gateway.NodeID = testnode.ID.String()
-
-		node, err := logic.CreateEgressGateway(gateway)
-		t.Log(node)
-		assert.Nil(t, err)
-		assert.Equal(t, true, node.IsEgressGateway)
-		assert.Equal(t, gateway.Ranges, node.EgressGatewayRanges)
-	})
-
-}
-func TestDeleteEgressGateway(t *testing.T) {
-	var gateway models.EgressGatewayRequest
-	deleteAllNetworks()
-	createNet()
-	testnode := createTestNode()
-	gateway.Ranges = []string{"10.100.100.0/24"}
-	gateway.NetID = "skynet"
-	gateway.NodeID = testnode.ID.String()
-	t.Run("Success", func(t *testing.T) {
-		node, err := logic.CreateEgressGateway(gateway)
-		assert.Nil(t, err)
-		assert.Equal(t, true, node.IsEgressGateway)
-		assert.Equal(t, []string{"10.100.100.0/24"}, node.EgressGatewayRanges)
-		node, err = logic.DeleteEgressGateway(gateway.NetID, gateway.NodeID)
-		assert.Nil(t, err)
-		assert.Equal(t, false, node.IsEgressGateway)
-		assert.Equal(t, []string([]string{}), node.EgressGatewayRanges)
-	})
-	t.Run("NotGateway", func(t *testing.T) {
-		node, err := logic.DeleteEgressGateway(gateway.NetID, gateway.NodeID)
-		assert.Nil(t, err)
-		assert.Equal(t, false, node.IsEgressGateway)
-		assert.Equal(t, []string([]string{}), node.EgressGatewayRanges)
-	})
-	t.Run("BadNode", func(t *testing.T) {
-		node, err := logic.DeleteEgressGateway(gateway.NetID, "01:02:03")
-		assert.EqualError(t, err, "no result found")
-		assert.Equal(t, models.Node{}, node)
-		deleteAllNodes()
-	})
-}
-
 func TestGetNetworkNodes(t *testing.T) {
 	deleteAllNetworks()
 	createNet()

+ 19 - 0
controllers/server.go

@@ -271,6 +271,25 @@ func updateSettings(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to udpate server settings "+err.Error()), "internal"))
 		return
 	}
+	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:   models.SettingSub.String(),
+			Name: models.SettingSub.String(),
+			Type: models.SettingSub,
+		},
+		Diff: models.Diff{
+			Old: currSettings,
+			New: req,
+		},
+		Origin: models.Dashboard,
+	})
 	go reInit(currSettings, req, force == "true")
 	logic.ReturnSuccessResponseWithJson(w, r, req, "updated server settings successfully")
 }

+ 54 - 2
controllers/tags.go

@@ -89,7 +89,7 @@ func createTag(w http.ResponseWriter, r *http.Request) {
 		Network:   req.Network,
 		CreatedBy: user.UserName,
 		ColorCode: req.ColorCode,
-		CreatedAt: time.Now(),
+		CreatedAt: time.Now().UTC(),
 	}
 	_, err = logic.GetTag(tag.ID)
 	if err == nil {
@@ -131,6 +131,22 @@ func createTag(w http.ResponseWriter, r *http.Request) {
 			logic.UpsertNode(&node)
 		}
 	}()
+	logic.LogEvent(&models.Event{
+		Action: models.Create,
+		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:   tag.ID.String(),
+			Name: tag.TagName,
+			Type: models.TagSub,
+		},
+		NetworkID: tag.Network,
+		Origin:    models.Dashboard,
+	})
 	go mq.PublishPeerUpdate(false)
 
 	var res models.TagListRespNodes = models.TagListRespNodes{
@@ -163,6 +179,25 @@ func updateTag(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
+	e := &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:   tag.ID.String(),
+			Name: tag.TagName,
+			Type: models.TagSub,
+		},
+		Diff: models.Diff{
+			Old: tag,
+		},
+		NetworkID: tag.Network,
+		Origin:    models.Dashboard,
+	}
 	updateTag.NewName = strings.TrimSpace(updateTag.NewName)
 	var newID models.TagID
 	if updateTag.NewName != "" {
@@ -198,7 +233,8 @@ func updateTag(w http.ResponseWriter, r *http.Request) {
 		}
 		mq.PublishPeerUpdate(false)
 	}()
-
+	e.Diff.New = updateTag
+	logic.LogEvent(e)
 	var res models.TagListRespNodes = models.TagListRespNodes{
 		Tag:         tag,
 		UsedByCnt:   len(updateTag.TaggedNodes),
@@ -241,5 +277,21 @@ func deleteTag(w http.ResponseWriter, r *http.Request) {
 		logic.RemoveTagFromEnrollmentKeys(tag.ID)
 		mq.PublishPeerUpdate(false)
 	}()
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		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:   tag.ID.String(),
+			Name: tag.TagName,
+			Type: models.TagSub,
+		},
+		NetworkID: tag.Network,
+		Origin:    models.Dashboard,
+	})
 	logic.ReturnSuccessResponse(w, r, "deleted tag "+tagID)
 }

+ 170 - 8
controllers/user.go

@@ -45,6 +45,7 @@ func userHandlers(r *mux.Router) {
 	r.HandleFunc("/api/v1/users/access_token", logic.SecurityCheck(true, http.HandlerFunc(createUserAccessToken))).Methods(http.MethodPost)
 	r.HandleFunc("/api/v1/users/access_token", logic.SecurityCheck(true, http.HandlerFunc(getUserAccessTokens))).Methods(http.MethodGet)
 	r.HandleFunc("/api/v1/users/access_token", logic.SecurityCheck(true, http.HandlerFunc(deleteUserAccessTokens))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/v1/users/logout", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(logout)))).Methods(http.MethodPost)
 }
 
 // @Summary     Authenticate a user to retrieve an authorization token
@@ -66,25 +67,25 @@ func createUserAccessToken(w http.ResponseWriter, r *http.Request) {
 	if err != nil {
 		logger.Log(0, "error decoding request body: ",
 			err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
 		return
 	}
 	if req.Name == "" {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("name is required"), "badrequest"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("name is required"), logic.BadReq))
 		return
 	}
 	if req.UserName == "" {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("username is required"), "badrequest"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("username is required"), logic.BadReq))
 		return
 	}
 	caller, err := logic.GetUser(r.Header.Get("user"))
 	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.UnAuthorized))
 		return
 	}
 	user, err := logic.GetUser(req.UserName)
 	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.UnAuthorized))
 		return
 	}
 	if caller.UserName != user.UserName && caller.PlatformRoleID != models.SuperAdminRole {
@@ -108,7 +109,7 @@ func createUserAccessToken(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(
 			w,
 			r,
-			logic.FormatError(errors.New("error creating access token "+err.Error()), "internal"),
+			logic.FormatError(errors.New("error creating access token "+err.Error()), logic.Internal),
 		)
 		return
 	}
@@ -117,10 +118,26 @@ func createUserAccessToken(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(
 			w,
 			r,
-			logic.FormatError(errors.New("error creating access token "+err.Error()), "internal"),
+			logic.FormatError(errors.New("error creating access token "+err.Error()), logic.Internal),
 		)
 		return
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Create,
+		Source: models.Subject{
+			ID:   caller.UserName,
+			Name: caller.UserName,
+			Type: models.UserSub,
+		},
+		TriggeredBy: caller.UserName,
+		Target: models.Subject{
+			ID:   req.ID,
+			Name: req.Name,
+			Type: models.UserAccessTokenSub,
+			Info: req,
+		},
+		Origin: models.Dashboard,
+	})
 	logic.ReturnSuccessResponseWithJson(w, r, models.SuccessfulUserLoginResponse{
 		AuthToken: jwt,
 		UserName:  req.UserName,
@@ -165,7 +182,7 @@ func deleteUserAccessTokens(w http.ResponseWriter, r *http.Request) {
 	}
 	err := a.Get(r.Context())
 	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("id is required"), "badrequest"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("token does not exist"), "badrequest"))
 		return
 	}
 	caller, err := logic.GetUser(r.Header.Get("user"))
@@ -199,6 +216,22 @@ func deleteUserAccessTokens(w http.ResponseWriter, r *http.Request) {
 		)
 		return
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		Source: models.Subject{
+			ID:   caller.UserName,
+			Name: caller.UserName,
+			Type: models.UserSub,
+		},
+		TriggeredBy: caller.UserName,
+		Target: models.Subject{
+			ID:   a.ID,
+			Name: a.Name,
+			Type: models.UserAccessTokenSub,
+			Info: a,
+		},
+		Origin: models.Dashboard,
+	})
 	logic.ReturnSuccessResponseWithJson(w, r, nil, "revoked access token")
 }
 
@@ -267,6 +300,38 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 			logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("access denied to dashboard"), "unauthorized"))
 			return
 		}
+		// log user activity
+		logic.LogEvent(&models.Event{
+			Action: models.Login,
+			Source: models.Subject{
+				ID:   user.UserName,
+				Name: user.UserName,
+				Type: models.UserSub,
+			},
+			TriggeredBy: user.UserName,
+			Target: models.Subject{
+				ID:   models.DashboardSub.String(),
+				Name: models.DashboardSub.String(),
+				Type: models.DashboardSub,
+			},
+			Origin: models.Dashboard,
+		})
+	} else {
+		logic.LogEvent(&models.Event{
+			Action: models.Login,
+			Source: models.Subject{
+				ID:   user.UserName,
+				Name: user.UserName,
+				Type: models.UserSub,
+			},
+			TriggeredBy: user.UserName,
+			Target: models.Subject{
+				ID:   models.ClientAppSub.String(),
+				Name: models.ClientAppSub.String(),
+				Type: models.ClientAppSub,
+			},
+			Origin: models.ClientApp,
+		})
 	}
 
 	username := authRequest.UserName
@@ -682,6 +747,21 @@ func createUser(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Create,
+		Source: models.Subject{
+			ID:   caller.UserName,
+			Name: caller.UserName,
+			Type: models.UserSub,
+		},
+		TriggeredBy: caller.UserName,
+		Target: models.Subject{
+			ID:   user.UserName,
+			Name: user.UserName,
+			Type: models.UserSub,
+		},
+		Origin: models.Dashboard,
+	})
 	logic.DeleteUserInvite(user.UserName)
 	logic.DeletePendingUser(user.UserName)
 	go mq.PublishPeerUpdate(false)
@@ -820,6 +900,25 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
 	if userchange.PlatformRoleID != user.PlatformRoleID || !logic.CompareMaps(user.UserGroups, userchange.UserGroups) {
 		(&schema.UserAccessToken{UserName: user.UserName}).DeleteAllUserTokens(r.Context())
 	}
+	e := models.Event{
+		Action: models.Update,
+		Source: models.Subject{
+			ID:   caller.UserName,
+			Name: caller.UserName,
+			Type: models.UserSub,
+		},
+		TriggeredBy: caller.UserName,
+		Target: models.Subject{
+			ID:   user.UserName,
+			Name: user.UserName,
+			Type: models.UserSub,
+		},
+		Diff: models.Diff{
+			Old: user,
+			New: userchange,
+		},
+		Origin: models.Dashboard,
+	}
 	user, err = logic.UpdateUser(&userchange, user)
 	if err != nil {
 		logger.Log(0, username,
@@ -827,6 +926,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
+	logic.LogEvent(&e)
 	go mq.PublishPeerUpdate(false)
 	logger.Log(1, username, "was updated")
 	json.NewEncoder(w).Encode(logic.ToReturnUser(*user))
@@ -905,6 +1005,21 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		Source: models.Subject{
+			ID:   caller.UserName,
+			Name: caller.UserName,
+			Type: models.UserSub,
+		},
+		TriggeredBy: caller.UserName,
+		Target: models.Subject{
+			ID:   user.UserName,
+			Name: user.UserName,
+			Type: models.UserSub,
+		},
+		Origin: models.Dashboard,
+	})
 	// check and delete extclient with this ownerID
 	go func() {
 		extclients, err := logic.GetAllExtClients()
@@ -970,3 +1085,50 @@ func listRoles(w http.ResponseWriter, r *http.Request) {
 
 	logic.ReturnSuccessResponseWithJson(w, r, roles, "successfully fetched user roles permission templates")
 }
+
+// swagger:route POST /api/v1/user/logout user logout
+//
+// LogOut user.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
+func logout(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+	userName := r.URL.Query().Get("username")
+	user, err := logic.GetUser(userName)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
+		return
+	}
+	var target models.SubjectType
+	if val := r.Header.Get("From-Ui"); val == "true" {
+		target = models.DashboardSub
+	} else {
+		target = models.ClientAppSub
+	}
+	if target != "" {
+		logic.LogEvent(&models.Event{
+			Action: models.LogOut,
+			Source: models.Subject{
+				ID:   user.UserName,
+				Name: user.UserName,
+				Type: models.UserSub,
+			},
+			TriggeredBy: user.UserName,
+			Target: models.Subject{
+				ID:   target.String(),
+				Name: target.String(),
+				Type: target,
+			},
+			Origin: models.Origin(target),
+		})
+	}
+
+	logic.ReturnSuccessResponse(w, r, "user logged out")
+}

+ 12 - 0
db/db.go

@@ -83,6 +83,18 @@ func FromContext(ctx context.Context) *gorm.DB {
 	return db
 }
 
+func SetPagination(ctx context.Context, page, pageSize int) context.Context {
+	if page < 1 {
+		page = 1
+	}
+	if pageSize < 1 || pageSize > 100 {
+		pageSize = 10
+	}
+	db := FromContext(ctx)
+	offset := (page - 1) * pageSize
+	return context.WithValue(ctx, dbCtxKey, db.Offset(offset).Limit(pageSize))
+}
+
 // BeginTx returns a context with a new transaction.
 // If the context already has a db connection instance,
 // it uses that instance. Otherwise, it uses the

+ 14 - 9
go.mod

@@ -8,23 +8,23 @@ require (
 	github.com/blang/semver v3.5.1+incompatible
 	github.com/eclipse/paho.mqtt.golang v1.5.0
 	github.com/go-playground/validator/v10 v10.26.0
-	github.com/golang-jwt/jwt/v4 v4.5.1
+	github.com/golang-jwt/jwt/v4 v4.5.2
 	github.com/google/uuid v1.6.0
 	github.com/gorilla/handlers v1.5.2
 	github.com/gorilla/mux v1.8.1
 	github.com/lib/pq v1.10.9
-	github.com/mattn/go-sqlite3 v1.14.24
+	github.com/mattn/go-sqlite3 v1.14.28
 	github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa
 	github.com/seancfoley/ipaddress-go v1.7.0
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/stretchr/testify v1.10.0
 	github.com/txn2/txeh v1.5.5
 	go.uber.org/automaxprocs v1.6.0
-	golang.org/x/crypto v0.37.0
+	golang.org/x/crypto v0.38.0
 	golang.org/x/net v0.39.0 // indirect
 	golang.org/x/oauth2 v0.29.0
-	golang.org/x/sys v0.32.0 // indirect
-	golang.org/x/text v0.24.0 // indirect
+	golang.org/x/sys v0.33.0 // indirect
+	golang.org/x/text v0.25.0 // indirect
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb
 	gopkg.in/yaml.v3 v3.0.1
 )
@@ -32,11 +32,11 @@ require (
 require (
 	filippo.io/edwards25519 v1.1.0
 	github.com/c-robinson/iplib v1.0.8
-	github.com/posthog/posthog-go v1.2.24
+	github.com/posthog/posthog-go v1.5.5
 )
 
 require (
-	github.com/coreos/go-oidc/v3 v3.9.0
+	github.com/coreos/go-oidc/v3 v3.14.1
 	github.com/gorilla/websocket v1.5.3
 	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
 )
@@ -49,9 +49,10 @@ require (
 	github.com/spf13/cobra v1.9.1
 	google.golang.org/api v0.229.0
 	gopkg.in/mail.v2 v2.3.1
+	gorm.io/datatypes v1.2.5
 	gorm.io/driver/postgres v1.5.11
 	gorm.io/driver/sqlite v1.5.7
-	gorm.io/gorm v1.25.12
+	gorm.io/gorm v1.26.1
 )
 
 require (
@@ -65,6 +66,9 @@ require (
 	github.com/google/s2a-go v0.1.9 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
 	github.com/googleapis/gax-go/v2 v2.14.1 // indirect
+	github.com/go-sql-driver/mysql v1.8.1 // indirect
+	github.com/google/go-cmp v0.7.0 // indirect
+	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jackc/pgpassfile v1.0.0 // indirect
 	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
@@ -85,6 +89,7 @@ require (
 	google.golang.org/grpc v1.71.1 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
+	gorm.io/driver/mysql v1.5.6 // indirect
 )
 
 require (
@@ -96,5 +101,5 @@ require (
 	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/mattn/go-runewidth v0.0.13 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	golang.org/x/sync v0.13.0 // indirect
+	golang.org/x/sync v0.14.0 // indirect
 )

+ 36 - 16
go.sum

@@ -37,8 +37,15 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
 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/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
-github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+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/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
+github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
+github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
+github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
+github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
+github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -46,6 +53,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
 github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 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/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
@@ -64,6 +73,8 @@ github.com/guumaster/tablewriter v0.0.10 h1:A0HD94yMdt4usgxBjoEceNeE0XMJ027euoHA
 github.com/guumaster/tablewriter v0.0.10/go.mod h1:p4FRFhyfo0UD9ZLmMRbbJooTUsxo6b80qZTERVDWrH8=
 github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
 github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
@@ -92,14 +103,16 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
 github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
-github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+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/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
+github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/posthog/posthog-go v1.2.24 h1:A+iG4saBJemo++VDlcWovbYf8KFFNUfrCoJtsc40RPA=
-github.com/posthog/posthog-go v1.2.24/go.mod h1:uYC2l1Yktc8E+9FAHJ9QZG4vQf/NHJPD800Hsm7DzoM=
+github.com/posthog/posthog-go v1.5.5 h1:2o3j7IrHbTIfxRtj4MPaXKeimuTYg49onNzNBZbwksM=
+github.com/posthog/posthog-go v1.5.5/go.mod h1:3RqUmSnPuwmeVj/GYrS75wNGqcAKdpODiwc83xZWgdE=
 github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
 github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -148,8 +161,8 @@ go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
 go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
-golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
+golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
+golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
 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/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -157,15 +170,15 @@ golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
 golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
 golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
 golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
-golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
-golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
+golang.org/x/sync v0.14.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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
-golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
-golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
+golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
+golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
 golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
 golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -191,9 +204,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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 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/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
+gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
 gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
 gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
 gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
 gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
-gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
-gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
+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/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+gorm.io/gorm v1.26.1 h1:ghB2gUI9FkS46luZtn6DLZ0f6ooBJ5IbVej2ENFDjRw=
+gorm.io/gorm v1.26.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=

+ 349 - 137
logic/acls.go

@@ -1,6 +1,7 @@
 package logic
 
 import (
+	"context"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -12,10 +13,23 @@ import (
 
 	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/servercfg"
 )
 
+/*
+TODO: EGRESS
+1. allow only selection of egress ranges in a policy
+ranges should be replaced by egress identifier
+
+2. check logic required for MAC exit node
+
+3.
+
+*/
+
 var (
 	aclCacheMutex = &sync.RWMutex{}
 	aclCacheMap   = make(map[string]models.Acl)
@@ -236,10 +250,10 @@ func GetEgressRanges(netID models.NetworkID) (map[string][]string, map[string]st
 		if currentNode.Network != netID.String() {
 			continue
 		}
-		if currentNode.IsEgressGateway { // add the egress gateway range(s) to the result
-			if len(currentNode.EgressGatewayRanges) > 0 {
-				nodeEgressMap[currentNode.ID.String()] = currentNode.EgressGatewayRanges
-				for _, egressRangeI := range currentNode.EgressGatewayRanges {
+		if currentNode.EgressDetails.IsEgressGateway { // add the egress gateway range(s) to the result
+			if len(currentNode.EgressDetails.EgressGatewayRanges) > 0 {
+				nodeEgressMap[currentNode.ID.String()] = currentNode.EgressDetails.EgressGatewayRanges
+				for _, egressRangeI := range currentNode.EgressDetails.EgressGatewayRanges {
 					resultMap[egressRangeI] = struct{}{}
 				}
 			}
@@ -257,78 +271,102 @@ func GetEgressRanges(netID models.NetworkID) (map[string][]string, map[string]st
 	return nodeEgressMap, resultMap, nil
 }
 
-func checkIfAclTagisValid(t models.AclPolicyTag, netID models.NetworkID, policyType models.AclPolicyType, isSrc bool) bool {
+func checkIfAclTagisValid(a models.Acl, t models.AclPolicyTag, isSrc bool) (err error) {
 	switch t.ID {
 	case models.NodeTagID:
-		if policyType == models.UserPolicy && isSrc {
-			return false
+		if a.RuleType == models.UserPolicy && isSrc {
+			return errors.New("user policy source mismatch")
 		}
 		// check if tag is valid
 		_, err := GetTag(models.TagID(t.Value))
 		if err != nil {
-			return false
+			return errors.New("invalid tag " + t.Value)
 		}
 	case models.NodeID:
-		if policyType == models.UserPolicy && isSrc {
-			return false
+		if a.RuleType == models.UserPolicy && isSrc {
+			return errors.New("user policy source mismatch")
 		}
 		_, nodeErr := GetNodeByID(t.Value)
 		if nodeErr != nil {
-			_, staticNodeErr := GetExtClient(t.Value, netID.String())
+			_, staticNodeErr := GetExtClient(t.Value, a.NetworkID.String())
 			if staticNodeErr != nil {
-				return false
+				return errors.New("invalid node " + t.Value)
 			}
 		}
-	case models.EgressRange:
-		if isSrc {
-			return false
+	case models.EgressID, models.EgressRange:
+		e := schema.Egress{
+			ID: t.Value,
 		}
-		// _, rangesMap, err := GetEgressRanges(netID)
-		// if err != nil {
-		// 	return false
-		// }
-		// if _, ok := rangesMap[t.Value]; !ok {
-		// 	return false
-		// }
+		err := e.Get(db.WithContext(context.TODO()))
+		if err != nil {
+			return errors.New("invalid egress")
+		}
+		if e.IsInetGw {
+			req := models.InetNodeReq{}
+			for _, srcI := range a.Src {
+				if srcI.ID == models.NodeTagID {
+					nodesMap := GetNodesWithTag(models.TagID(srcI.Value))
+					for _, node := range nodesMap {
+						req.InetNodeClientIDs = append(req.InetNodeClientIDs, node.ID.String())
+					}
+				} else if srcI.ID == models.NodeID {
+					req.InetNodeClientIDs = append(req.InetNodeClientIDs, srcI.Value)
+				}
+			}
+			if len(e.Nodes) > 0 {
+				for k := range e.Nodes {
+					inetNode, err := GetNodeByID(k)
+					if err != nil {
+						return errors.New("invalid node " + t.Value)
+					}
+					if err = ValidateInetGwReq(inetNode, req, false); err != nil {
+						return err
+					}
+				}
+
+			}
+
+		}
+
 	case models.UserAclID:
-		if policyType == models.DevicePolicy {
-			return false
+		if a.RuleType == models.DevicePolicy {
+			return errors.New("device policy source mismatch")
 		}
 		if !isSrc {
-			return false
+			return errors.New("user cannot be added to destination")
 		}
 		_, err := GetUser(t.Value)
 		if err != nil {
-			return false
+			return errors.New("invalid user " + t.Value)
 		}
 	case models.UserGroupAclID:
-		if policyType == models.DevicePolicy {
-			return false
+		if a.RuleType == models.DevicePolicy {
+			return errors.New("device policy source mismatch")
 		}
 		if !isSrc {
-			return false
+			return errors.New("user cannot be added to destination")
 		}
 		err := IsGroupValid(models.UserGroupID(t.Value))
 		if err != nil {
-			return false
+			return errors.New("invalid user group " + t.Value)
 		}
 		// check if group belongs to this network
-		netGrps := GetUserGroupsInNetwork(netID)
+		netGrps := GetUserGroupsInNetwork(a.NetworkID)
 		if _, ok := netGrps[models.UserGroupID(t.Value)]; !ok {
-			return false
+			return errors.New("invalid user group " + t.Value)
 		}
 	default:
-		return false
+		return errors.New("invalid policy")
 	}
-	return true
+	return nil
 }
 
 // IsAclPolicyValid - validates if acl policy is valid
-func IsAclPolicyValid(acl models.Acl) bool {
+func IsAclPolicyValid(acl models.Acl) (err error) {
 	//check if src and dst are valid
 	if acl.AllowedDirection != models.TrafficDirectionBi &&
 		acl.AllowedDirection != models.TrafficDirectionUni {
-		return false
+		return errors.New("invalid traffic direction")
 	}
 	switch acl.RuleType {
 	case models.UserPolicy:
@@ -339,8 +377,8 @@ func IsAclPolicyValid(acl models.Acl) bool {
 				continue
 			}
 			// check if user group is valid
-			if !checkIfAclTagisValid(srcI, acl.NetworkID, acl.RuleType, true) {
-				return false
+			if err = checkIfAclTagisValid(acl, srcI, true); err != nil {
+				return
 			}
 		}
 		for _, dstI := range acl.Dst {
@@ -350,8 +388,8 @@ func IsAclPolicyValid(acl models.Acl) bool {
 			}
 
 			// check if user group is valid
-			if !checkIfAclTagisValid(dstI, acl.NetworkID, acl.RuleType, false) {
-				return false
+			if err = checkIfAclTagisValid(acl, dstI, false); err != nil {
+				return
 			}
 		}
 	case models.DevicePolicy:
@@ -360,8 +398,8 @@ func IsAclPolicyValid(acl models.Acl) bool {
 				continue
 			}
 			// check if user group is valid
-			if !checkIfAclTagisValid(srcI, acl.NetworkID, acl.RuleType, true) {
-				return false
+			if err = checkIfAclTagisValid(acl, srcI, true); err != nil {
+				return err
 			}
 		}
 		for _, dstI := range acl.Dst {
@@ -370,12 +408,26 @@ func IsAclPolicyValid(acl models.Acl) bool {
 				continue
 			}
 			// check if user group is valid
-			if !checkIfAclTagisValid(dstI, acl.NetworkID, acl.RuleType, false) {
-				return false
+			if err = checkIfAclTagisValid(acl, dstI, false); err != nil {
+				return
 			}
 		}
 	}
-	return true
+	return nil
+}
+
+func UniqueAclPolicyTags(tags []models.AclPolicyTag) []models.AclPolicyTag {
+	seen := make(map[string]bool)
+	var result []models.AclPolicyTag
+
+	for _, tag := range tags {
+		key := fmt.Sprintf("%v-%s", tag.ID, tag.Value)
+		if !seen[key] {
+			seen[key] = true
+			result = append(result, tag)
+		}
+	}
+	return result
 }
 
 // UpdateAcl - updates allowed fields on acls and commits to DB
@@ -623,6 +675,17 @@ func IsUserAllowedToCommunicate(userName string, peer models.Node) (bool, []mode
 			continue
 		}
 		dstMap := 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{}{}
+					}
+				}
+			}
+		}
 		if _, ok := dstMap["*"]; ok {
 			allowedPolicies = append(allowedPolicies, policy)
 			continue
@@ -712,8 +775,20 @@ func IsPeerAllowed(node, peer models.Node, checkDefaultPolicy bool) bool {
 		if !policy.Enabled {
 			continue
 		}
+
 		srcMap = convAclTagToValueMap(policy.Src)
 		dstMap = 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{}{}
+					}
+				}
+			}
+		}
 		if checkTagGroupPolicy(srcMap, dstMap, node, peer, nodeTags, peerTags) {
 			return true
 		}
@@ -975,6 +1050,17 @@ func IsNodeAllowedToCommunicateV1(node, peer models.Node, checkDefaultPolicy boo
 		allowed := false
 		srcMap = convAclTagToValueMap(policy.Src)
 		dstMap = 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 {
@@ -1158,7 +1244,7 @@ func getEgressUserRulesForNode(targetnode *models.Node,
 	acls := listUserPolicies(models.NetworkID(targetnode.Network))
 	var targetNodeTags = make(map[models.TagID]struct{})
 	targetNodeTags["*"] = struct{}{}
-	for _, rangeI := range targetnode.EgressGatewayRanges {
+	for _, rangeI := range targetnode.EgressDetails.EgressGatewayRanges {
 		targetNodeTags[models.TagID(rangeI)] = struct{}{}
 	}
 	for _, acl := range acls {
@@ -1166,6 +1252,18 @@ func getEgressUserRulesForNode(targetnode *models.Node,
 			continue
 		}
 		dstTags := 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{}{}
+					}
+					dstTags[e.Range] = struct{}{}
+				}
+			}
+		}
 		_, all := dstTags["*"]
 		addUsers := false
 		if !all {
@@ -1225,16 +1323,34 @@ func getEgressUserRulesForNode(targetnode *models.Node,
 				r.IP6List = append(r.IP6List, userNode.StaticNode.AddressIPNet6())
 			}
 			for _, dstI := range acl.Dst {
-				if dstI.ID == models.EgressRange {
-					ip, cidr, err := net.ParseCIDR(dstI.Value)
-					if err == nil {
-						if ip.To4() != nil {
-							r.Dst = append(r.Dst, *cidr)
-						} else {
-							r.Dst6 = append(r.Dst6, *cidr)
-						}
+				if dstI.ID == models.EgressID {
+					e := schema.Egress{ID: dstI.Value}
+					err := e.Get(db.WithContext(context.TODO()))
+					if err != nil {
+						continue
+					}
+					if e.IsInetGw {
+						r.Dst = append(r.Dst, net.IPNet{
+							IP:   net.IPv4zero,
+							Mask: net.CIDRMask(0, 32),
+						})
+						r.Dst6 = append(r.Dst6, net.IPNet{
+							IP:   net.IPv6zero,
+							Mask: net.CIDRMask(0, 128),
+						})
+
+					} else {
+						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)
+							}
 
+						}
 					}
+
 				}
 
 			}
@@ -1348,7 +1464,7 @@ func getUserAclRulesForNode(targetnode *models.Node,
 }
 
 func checkIfAnyActiveEgressPolicy(targetNode models.Node) bool {
-	if !targetNode.IsEgressGateway {
+	if !targetNode.EgressDetails.IsEgressGateway {
 		return false
 	}
 	var targetNodeTags = make(map[models.TagID]struct{})
@@ -1371,8 +1487,20 @@ func checkIfAnyActiveEgressPolicy(targetNode models.Node) bool {
 		}
 		srcTags := convAclTagToValueMap(acl.Src)
 		dstTags := 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{}{}
+					}
+					dstTags[e.Range] = struct{}{}
+				}
+			}
+		}
 		for nodeTag := range targetNodeTags {
-			if acl.RuleType == models.DevicePolicy {
+			if acl.RuleType == models.DevicePolicy && acl.AllowedDirection == models.TrafficDirectionBi {
 				if _, ok := srcTags[nodeTag.String()]; ok {
 					return true
 				}
@@ -1411,7 +1539,7 @@ func checkIfAnyPolicyisUniDirectional(targetNode models.Node) bool {
 		if !acl.Enabled {
 			continue
 		}
-		if acl.AllowedDirection == models.TrafficDirectionBi {
+		if acl.AllowedDirection == models.TrafficDirectionBi && acl.Proto == models.ALL && acl.ServiceType == models.Any {
 			continue
 		}
 		if acl.Proto != models.ALL || acl.ServiceType != models.Any {
@@ -1440,6 +1568,60 @@ func checkIfAnyPolicyisUniDirectional(targetNode models.Node) bool {
 	return false
 }
 
+func checkIfNodeHasAccessToAllResources(targetnode *models.Node) bool {
+	acls := 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
+		}
+		srcTags := convAclTagToValueMap(acl.Src)
+		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
+}
+
 func GetAclRulesForNode(targetnodeI *models.Node) (rules map[string]models.AclRule) {
 	targetnode := *targetnodeI
 	defer func() {
@@ -1454,7 +1636,7 @@ func GetAclRulesForNode(targetnodeI *models.Node) (rules map[string]models.AclRu
 	} else {
 		taggedNodes = GetTagMapWithNodesByNetwork(models.NetworkID(targetnode.Network), true)
 	}
-
+	fmt.Printf("TAGGED NODES: %+v\n", taggedNodes)
 	acls := listDevicePolicies(models.NetworkID(targetnode.Network))
 	var targetNodeTags = make(map[models.TagID]struct{})
 	if targetnode.Mutex != nil {
@@ -1475,6 +1657,17 @@ func GetAclRulesForNode(targetnodeI *models.Node) (rules map[string]models.AclRu
 		}
 		srcTags := convAclTagToValueMap(acl.Src)
 		dstTags := 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{}{}
+					}
+				}
+			}
+		}
 		_, srcAll := srcTags["*"]
 		_, dstAll := dstTags["*"]
 		aclRule := models.AclRule{
@@ -1502,7 +1695,7 @@ func GetAclRulesForNode(targetnodeI *models.Node) (rules map[string]models.AclRu
 					existsInDstTag = true
 				}
 
-				if existsInSrcTag && !existsInDstTag {
+				if existsInSrcTag /* && !existsInDstTag*/ {
 					// get all dst tags
 					for dst := range dstTags {
 						if dst == nodeTag.String() {
@@ -1539,7 +1732,7 @@ func GetAclRulesForNode(targetnodeI *models.Node) (rules map[string]models.AclRu
 						}
 					}
 				}
-				if existsInDstTag && !existsInSrcTag {
+				if existsInDstTag /*&& !existsInSrcTag*/ {
 					// get all src tags
 					for src := range srcTags {
 						if src == nodeTag.String() {
@@ -1575,47 +1768,47 @@ func GetAclRulesForNode(targetnodeI *models.Node) (rules map[string]models.AclRu
 						}
 					}
 				}
-				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())
-						}
-					}
-				}
+				// 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 {
@@ -1677,9 +1870,23 @@ func GetEgressRulesForNode(targetnode models.Node) (rules map[string]models.AclR
 			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
 	*/
-
-	for _, rangeI := range targetnode.EgressGatewayRanges {
-		targetNodeTags[models.TagID(rangeI)] = struct{}{}
+	egs, _ := (&schema.Egress{Network: targetnode.Network}).ListByNetwork(db.WithContext(context.TODO()))
+	if len(egs) == 0 {
+		return
+	}
+	for _, egI := range egs {
+		if !egI.Status {
+			continue
+		}
+		if _, ok := egI.Nodes[targetnode.ID.String()]; ok {
+			if egI.Range == "*" {
+				targetNodeTags[models.TagID("0.0.0.0/0")] = struct{}{}
+				targetNodeTags[models.TagID("::/0")] = struct{}{}
+			} else {
+				targetNodeTags[models.TagID(egI.Range)] = struct{}{}
+			}
+			targetNodeTags[models.TagID(egI.ID)] = struct{}{}
+		}
 	}
 	for _, acl := range acls {
 		if !acl.Enabled {
@@ -1689,46 +1896,43 @@ func GetEgressRulesForNode(targetnode models.Node) (rules map[string]models.AclR
 		dstTags := 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 {
-			aclRule := models.AclRule{
-				ID:              acl.ID,
-				AllowedProtocol: acl.Proto,
-				AllowedPorts:    acl.Port,
-				Direction:       acl.AllowedDirection,
-				Allowed:         true,
-			}
+
 			if nodeTag != "*" {
 				ip, cidr, err := net.ParseCIDR(nodeTag.String())
-				if err != nil {
-					continue
-				}
-				if ip.To4() != nil {
-					aclRule.Dst = append(aclRule.Dst, *cidr)
-				} else {
-					aclRule.Dst6 = append(aclRule.Dst6, *cidr)
+				if err == nil {
+					if ip.To4() != nil {
+						aclRule.Dst = append(aclRule.Dst, *cidr)
+					} else {
+						aclRule.Dst6 = append(aclRule.Dst6, *cidr)
+					}
 				}
-
-			} else {
-				aclRule.Dst = append(aclRule.Dst, net.IPNet{
-					IP:   net.IPv4zero,        // 0.0.0.0
-					Mask: net.CIDRMask(0, 32), // /0 means match all IPv4
-				})
-				aclRule.Dst6 = append(aclRule.Dst6, net.IPNet{
-					IP:   net.IPv6zero,         // ::
-					Mask: net.CIDRMask(0, 128), // /0 means match all IPv6
-				})
 			}
 			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 {
@@ -1835,8 +2039,16 @@ func GetEgressRulesForNode(targetnode models.Node) (rules map[string]models.AclR
 					}
 				}
 			} else {
-				_, all := dstTags["*"]
-				if _, ok := dstTags[nodeTag.String()]; ok || all {
+				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() {
@@ -1864,13 +2076,13 @@ func GetEgressRulesForNode(targetnode models.Node) (rules map[string]models.AclR
 					}
 				}
 			}
-			if len(aclRule.IPList) > 0 || len(aclRule.IP6List) > 0 {
-				aclRule.IPList = UniqueIPNetList(aclRule.IPList)
-				aclRule.IP6List = UniqueIPNetList(aclRule.IP6List)
-				rules[acl.ID] = aclRule
-			}
 
 		}
+		if len(aclRule.IPList) > 0 || len(aclRule.IP6List) > 0 {
+			aclRule.IPList = UniqueIPNetList(aclRule.IPList)
+			aclRule.IP6List = UniqueIPNetList(aclRule.IP6List)
+			rules[acl.ID] = aclRule
+		}
 
 	}
 	return

+ 1 - 1
logic/auth.go

@@ -242,7 +242,7 @@ func VerifyAuthRequest(authRequest models.UserAuthParams) (string, error) {
 	}
 
 	// update last login time
-	result.LastLoginTime = time.Now()
+	result.LastLoginTime = time.Now().UTC()
 	err = UpsertUser(result)
 	if err != nil {
 		slog.Error("error upserting user", "error", err)

+ 366 - 0
logic/egress.go

@@ -0,0 +1,366 @@
+package logic
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"maps"
+	"net"
+
+	"github.com/gravitl/netmaker/db"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/schema"
+)
+
+func ValidateEgressReq(e *schema.Egress) error {
+	if e.Network == "" {
+		return errors.New("network id is empty")
+	}
+	_, err := GetNetwork(e.Network)
+	if err != nil {
+		return errors.New("failed to get network " + err.Error())
+	}
+	if !e.IsInetGw {
+		if e.Range == "" {
+			return errors.New("egress range is empty")
+		}
+		_, _, err = net.ParseCIDR(e.Range)
+		if err != nil {
+			return errors.New("invalid egress range " + err.Error())
+		}
+		err = ValidateEgressRange(e.Network, []string{e.Range})
+		if err != nil {
+			return errors.New("invalid egress range " + err.Error())
+		}
+	} else {
+		if len(e.Nodes) > 1 {
+			return errors.New("can only set one internet routing node")
+		}
+		req := models.InetNodeReq{}
+
+		for k := range e.Nodes {
+			inetNode, err := GetNodeByID(k)
+			if err != nil {
+				return errors.New("invalid routing node " + err.Error())
+			}
+			// check if node is acting as egress gw already
+			GetNodeEgressInfo(&inetNode)
+			if err := ValidateInetGwReq(inetNode, req, false); err != nil {
+				return err
+			}
+
+		}
+
+	}
+	if len(e.Nodes) != 0 {
+		for k := range e.Nodes {
+			_, err := GetNodeByID(k)
+			if err != nil {
+				return errors.New("invalid routing node " + err.Error())
+			}
+		}
+	}
+	return nil
+}
+
+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, _ := 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))...)
+					}
+				}
+			}
+		}
+	}
+	return
+}
+
+func isNodeUsingInternetGw(node *models.Node) {
+	host, err := GetHost(node.HostID.String())
+	if err != nil {
+		return
+	}
+	if host.IsDefault || node.IsFailOver {
+		return
+	}
+	nodeTags := maps.Clone(node.Tags)
+	nodeTags[models.TagID(node.ID.String())] = struct{}{}
+	acls, _ := ListAclsByNetwork(models.NetworkID(node.Network))
+	var isUsing bool
+	for _, acl := range acls {
+		if !acl.Enabled {
+			continue
+		}
+		srcVal := convAclTagToValueMap(acl.Src)
+		for _, dstI := range acl.Dst {
+			if dstI.ID == models.EgressID {
+				e := schema.Egress{ID: dstI.Value}
+				err := e.Get(db.WithContext(context.TODO()))
+				if err != nil || !e.Status {
+					continue
+				}
+
+				if e.IsInetGw {
+					if _, ok := srcVal[node.ID.String()]; ok {
+						for nodeID := range e.Nodes {
+							if nodeID == node.ID.String() {
+								continue
+							}
+							node.EgressDetails.InternetGwID = nodeID
+							isUsing = true
+							return
+						}
+					}
+					for tagID := range nodeTags {
+						if _, ok := srcVal[tagID.String()]; ok {
+							for nodeID := range e.Nodes {
+								if nodeID == node.ID.String() {
+									continue
+								}
+								node.EgressDetails.InternetGwID = nodeID
+								isUsing = true
+								return
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+	if !isUsing {
+		node.EgressDetails.InternetGwID = ""
+	}
+}
+
+func DoesNodeHaveAccessToEgress(node *models.Node, e *schema.Egress) bool {
+	nodeTags := maps.Clone(node.Tags)
+	nodeTags[models.TagID(node.ID.String())] = struct{}{}
+	if !e.IsInetGw {
+		nodeTags[models.TagID("*")] = struct{}{}
+	}
+	acls, _ := ListAclsByNetwork(models.NetworkID(node.Network))
+	if !e.IsInetGw {
+		defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
+		if defaultDevicePolicy.Enabled {
+			return true
+		}
+	}
+	for _, acl := range acls {
+		if !acl.Enabled {
+			continue
+		}
+		srcVal := convAclTagToValueMap(acl.Src)
+		if !e.IsInetGw && acl.AllowedDirection == models.TrafficDirectionBi {
+			if _, ok := srcVal["*"]; ok {
+				return true
+			}
+		}
+		for _, dstI := range acl.Dst {
+
+			if !e.IsInetGw && dstI.ID == models.NodeTagID && dstI.Value == "*" {
+				return true
+			}
+			if dstI.ID == models.EgressID && dstI.Value == e.ID {
+				e := schema.Egress{ID: dstI.Value}
+				err := e.Get(db.WithContext(context.TODO()))
+				if err != nil {
+					continue
+				}
+				if node.IsStatic {
+					if _, ok := srcVal[node.StaticNode.ClientID]; ok {
+						return true
+					}
+				} else {
+					if _, ok := srcVal[node.ID.String()]; ok {
+						return true
+					}
+				}
+
+				for tagID := range nodeTags {
+					if _, ok := srcVal[tagID.String()]; ok {
+						return true
+					}
+				}
+
+			}
+		}
+	}
+	return false
+}
+
+func AddEgressInfoToPeerByAccess(node, targetNode *models.Node) {
+	eli, _ := (&schema.Egress{Network: targetNode.Network}).ListByNetwork(db.WithContext(context.TODO()))
+	req := models.EgressGatewayRequest{
+		NodeID: targetNode.ID.String(),
+		NetID:  targetNode.Network,
+	}
+	defer func() {
+		if targetNode.Mutex != nil {
+			targetNode.Mutex.Lock()
+		}
+		isNodeUsingInternetGw(targetNode)
+		if targetNode.Mutex != nil {
+			targetNode.Mutex.Unlock()
+		}
+	}()
+	for _, e := range eli {
+		if !e.Status || e.Network != targetNode.Network {
+			continue
+		}
+		if !DoesNodeHaveAccessToEgress(node, &e) {
+			if node.IsRelayed && node.RelayedBy == targetNode.ID.String() {
+				if !DoesNodeHaveAccessToEgress(targetNode, &e) {
+					continue
+				}
+			} else {
+				continue
+			}
+
+		}
+		if metric, ok := e.Nodes[targetNode.ID.String()]; ok {
+			if e.IsInetGw {
+				targetNode.EgressDetails.IsInternetGateway = true
+				targetNode.EgressDetails.InetNodeReq = models.InetNodeReq{
+					InetNodeClientIDs: GetInetClientsFromAclPolicies(e.ID),
+				}
+				req.Ranges = append(req.Ranges, "0.0.0.0/0")
+				req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
+					Network:     "0.0.0.0/0",
+					Nat:         true,
+					RouteMetric: 256,
+				})
+				req.Ranges = append(req.Ranges, "::/0")
+				req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
+					Network:     "::/0",
+					Nat:         true,
+					RouteMetric: 256,
+				})
+			} else {
+				m64, err := metric.(json.Number).Int64()
+				if err != nil {
+					m64 = 256
+				}
+				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 targetNode.Mutex != nil {
+		targetNode.Mutex.Lock()
+	}
+	if len(req.Ranges) > 0 {
+
+		targetNode.EgressDetails.IsEgressGateway = true
+		targetNode.EgressDetails.EgressGatewayRanges = req.Ranges
+		targetNode.EgressDetails.EgressGatewayRequest = req
+
+	} else {
+		targetNode.EgressDetails = models.EgressDetails{}
+	}
+	if targetNode.Mutex != nil {
+		targetNode.Mutex.Unlock()
+	}
+}
+
+func GetNodeEgressInfo(targetNode *models.Node) {
+	eli, _ := (&schema.Egress{Network: targetNode.Network}).ListByNetwork(db.WithContext(context.TODO()))
+	req := models.EgressGatewayRequest{
+		NodeID: targetNode.ID.String(),
+		NetID:  targetNode.Network,
+	}
+	defer func() {
+		if targetNode.Mutex != nil {
+			targetNode.Mutex.Lock()
+		}
+		isNodeUsingInternetGw(targetNode)
+		if targetNode.Mutex != nil {
+			targetNode.Mutex.Unlock()
+		}
+	}()
+	for _, e := range eli {
+		if !e.Status || e.Network != targetNode.Network {
+			continue
+		}
+		if metric, ok := e.Nodes[targetNode.ID.String()]; ok {
+			if e.IsInetGw {
+				targetNode.EgressDetails.IsInternetGateway = true
+				targetNode.EgressDetails.InetNodeReq = models.InetNodeReq{
+					InetNodeClientIDs: GetInetClientsFromAclPolicies(e.ID),
+				}
+				req.Ranges = append(req.Ranges, "0.0.0.0/0")
+				req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
+					Network:     "0.0.0.0/0",
+					Nat:         true,
+					RouteMetric: 256,
+				})
+				req.Ranges = append(req.Ranges, "::/0")
+				req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
+					Network:     "::/0",
+					Nat:         true,
+					RouteMetric: 256,
+				})
+			} else {
+				m64, err := metric.(json.Number).Int64()
+				if err != nil {
+					m64 = 256
+				}
+				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 targetNode.Mutex != nil {
+		targetNode.Mutex.Lock()
+	}
+	if len(req.Ranges) > 0 {
+		targetNode.EgressDetails.IsEgressGateway = true
+		targetNode.EgressDetails.EgressGatewayRanges = req.Ranges
+		targetNode.EgressDetails.EgressGatewayRequest = req
+	} else {
+		targetNode.EgressDetails = models.EgressDetails{}
+	}
+	if targetNode.Mutex != nil {
+		targetNode.Mutex.Unlock()
+	}
+}
+
+func RemoveNodeFromEgress(node models.Node) {
+	egs, _ := (&schema.Egress{}).ListByNetwork(db.WithContext(context.TODO()))
+	for _, egI := range egs {
+		if _, ok := egI.Nodes[node.ID.String()]; ok {
+			delete(egI.Nodes, node.ID.String())
+			egI.Update(db.WithContext(context.TODO()))
+		}
+	}
+
+}

+ 16 - 6
logic/errors.go

@@ -8,20 +8,30 @@ import (
 	"golang.org/x/exp/slog"
 )
 
+type ApiErrorType string
+
+const (
+	Internal     ApiErrorType = "internal"
+	BadReq       ApiErrorType = "badrequest"
+	NotFound     ApiErrorType = "notfound"
+	UnAuthorized ApiErrorType = "unauthorized"
+	Forbidden    ApiErrorType = "forbidden"
+)
+
 // FormatError - takes ErrorResponse and uses correct code
-func FormatError(err error, errType string) models.ErrorResponse {
+func FormatError(err error, errType ApiErrorType) models.ErrorResponse {
 
 	var status = http.StatusInternalServerError
 	switch errType {
-	case "internal":
+	case Internal:
 		status = http.StatusInternalServerError
-	case "badrequest":
+	case BadReq:
 		status = http.StatusBadRequest
-	case "notfound":
+	case NotFound:
 		status = http.StatusNotFound
-	case "unauthorized":
+	case UnAuthorized:
 		status = http.StatusUnauthorized
-	case "forbidden":
+	case Forbidden:
 		status = http.StatusForbidden
 	default:
 		status = http.StatusInternalServerError

+ 49 - 5
logic/extpeers.go

@@ -1,6 +1,7 @@
 package logic
 
 import (
+	"context"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -13,9 +14,11 @@ import (
 
 	"github.com/goombaio/namegenerator"
 	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/exp/slog"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
@@ -71,13 +74,19 @@ func GetEgressRangesOnNetwork(client *models.ExtClient) ([]string, error) {
 	if err != nil {
 		return []string{}, err
 	}
+	// clientNode := client.ConvertToStaticNode()
 	for _, currentNode := range networkNodes {
 		if currentNode.Network != client.Network {
 			continue
 		}
-		if currentNode.IsEgressGateway { // add the egress gateway range(s) to the result
-			if len(currentNode.EgressGatewayRanges) > 0 {
-				result = append(result, currentNode.EgressGatewayRanges...)
+		GetNodeEgressInfo(&currentNode)
+		if currentNode.EgressDetails.IsInternetGateway && client.IngressGatewayID != currentNode.ID.String() {
+			continue
+		}
+		if currentNode.EgressDetails.IsEgressGateway { // add the egress gateway range(s) to the result
+			fmt.Println("EGRESSS EXTCLEINT: ", currentNode.EgressDetails)
+			if len(currentNode.EgressDetails.EgressGatewayRanges) > 0 {
+				result = append(result, currentNode.EgressDetails.EgressGatewayRanges...)
 			}
 		}
 	}
@@ -116,6 +125,25 @@ func DeleteExtClient(network string, clientid string) error {
 		}
 		deleteExtClientFromCache(key)
 	}
+	if extClient.RemoteAccessClientID != "" {
+		LogEvent(&models.Event{
+			Action: models.Disconnect,
+			Source: models.Subject{
+				ID:   extClient.OwnerID,
+				Name: extClient.OwnerID,
+				Type: models.UserSub,
+			},
+			TriggeredBy: extClient.OwnerID,
+			Target: models.Subject{
+				ID:   extClient.Network,
+				Name: extClient.Network,
+				Type: models.NetworkSub,
+				Info: extClient,
+			},
+			NetworkID: models.NetworkID(extClient.Network),
+			Origin:    models.ClientApp,
+		})
+	}
 	go RemoveNodeFromAclPolicy(extClient.ConvertToStaticNode())
 	return nil
 }
@@ -627,7 +655,15 @@ func getFwRulesForNodeAndPeerOnGw(node, peer models.Node, allowedPolicies []mode
 
 		// add egress range rules
 		for _, dstI := range policy.Dst {
-			if dstI.ID == models.EgressRange {
+			if dstI.ID == models.EgressID {
+
+				e := schema.Egress{ID: dstI.Value}
+				err := e.Get(db.WithContext(context.TODO()))
+				if err != nil {
+					continue
+				}
+				dstI.Value = e.Range
+
 				ip, cidr, err := net.ParseCIDR(dstI.Value)
 				if err == nil {
 					if ip.To4() != nil {
@@ -708,7 +744,15 @@ func getFwRulesForUserNodesOnGw(node models.Node, nodes []models.Node) (rules []
 
 						// add egress ranges
 						for _, dstI := range policy.Dst {
-							if dstI.ID == models.EgressRange {
+							if dstI.ID == models.EgressID {
+
+								e := schema.Egress{ID: dstI.Value}
+								err := e.Get(db.WithContext(context.TODO()))
+								if err != nil {
+									continue
+								}
+								dstI.Value = e.Range
+
 								ip, cidr, err := net.ParseCIDR(dstI.Value)
 								if err == nil {
 									if ip.To4() != nil && userNodeI.StaticNode.Address != "" {

+ 28 - 14
logic/gateway.go

@@ -1,6 +1,7 @@
 package logic
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"slices"
@@ -8,14 +9,27 @@ import (
 	"time"
 
 	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/servercfg"
 )
 
 // IsInternetGw - checks if node is acting as internet gw
 func IsInternetGw(node models.Node) bool {
-	return node.IsInternetGateway
+	e := schema.Egress{
+		Network: node.Network,
+	}
+	egList, _ := e.ListByNetwork(db.WithContext(context.TODO()))
+	for _, egI := range egList {
+		if egI.IsInetGw {
+			if _, ok := egI.Nodes[node.ID.String()]; ok {
+				return true
+			}
+		}
+	}
+	return false
 }
 
 // GetInternetGateways - gets all the nodes that are internet gateways
@@ -26,7 +40,7 @@ func GetInternetGateways() ([]models.Node, error) {
 	}
 	igs := make([]models.Node, 0)
 	for _, node := range nodes {
-		if node.IsInternetGateway {
+		if node.EgressDetails.IsInternetGateway {
 			igs = append(igs, node)
 		}
 	}
@@ -56,7 +70,7 @@ func GetAllEgresses() ([]models.Node, error) {
 	}
 	egresses := make([]models.Node, 0)
 	for _, node := range nodes {
-		if node.IsEgressGateway {
+		if node.EgressDetails.IsEgressGateway {
 			egresses = append(egresses, node)
 		}
 	}
@@ -133,11 +147,11 @@ func CreateEgressGateway(gateway models.EgressGatewayRequest) (models.Node, erro
 	if gateway.Ranges == nil {
 		gateway.Ranges = make([]string, 0)
 	}
-	node.IsEgressGateway = true
-	node.EgressGatewayRanges = gateway.Ranges
-	node.EgressGatewayNatEnabled = models.ParseBool(gateway.NatEnabled)
+	node.EgressDetails.IsEgressGateway = true
+	node.EgressDetails.EgressGatewayRanges = gateway.Ranges
+	node.EgressDetails.EgressGatewayNatEnabled = models.ParseBool(gateway.NatEnabled)
 
-	node.EgressGatewayRequest = gateway // store entire request for use when preserving the egress gateway
+	node.EgressDetails.EgressGatewayRequest = gateway // store entire request for use when preserving the egress gateway
 	node.SetLastModified()
 	if err = UpsertNode(&node); err != nil {
 		return models.Node{}, err
@@ -156,9 +170,9 @@ func DeleteEgressGateway(network, nodeid string) (models.Node, error) {
 	if err != nil {
 		return models.Node{}, err
 	}
-	node.IsEgressGateway = false
-	node.EgressGatewayRanges = []string{}
-	node.EgressGatewayRequest = models.EgressGatewayRequest{} // remove preserved request as the egress gateway is gone
+	node.EgressDetails.IsEgressGateway = false
+	node.EgressDetails.EgressGatewayRanges = []string{}
+	node.EgressDetails.EgressGatewayRequest = models.EgressGatewayRequest{} // remove preserved request as the egress gateway is gone
 	node.SetLastModified()
 	if err = UpsertNode(&node); err != nil {
 		return models.Node{}, err
@@ -191,12 +205,12 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq
 	node.IsIngressGateway = true
 	node.IsGw = true
 	if !servercfg.IsPro {
-		node.IsInternetGateway = ingress.IsInternetGateway
+		node.EgressDetails.IsInternetGateway = ingress.IsInternetGateway
 	}
 	node.IngressGatewayRange = network.AddressRange
 	node.IngressGatewayRange6 = network.AddressRange6
 	node.IngressDNS = ingress.ExtclientDNS
-	if node.IsInternetGateway && node.IngressDNS == "" {
+	if node.EgressDetails.IsInternetGateway && node.IngressDNS == "" {
 		node.IngressDNS = "1.1.1.1"
 	}
 	node.IngressPersistentKeepalive = 20
@@ -267,10 +281,10 @@ func DeleteIngressGateway(nodeid string) (models.Node, []models.ExtClient, error
 		return models.Node{}, removedClients, err
 	}
 	logger.Log(3, "deleting ingress gateway")
-	node.LastModified = time.Now()
+	node.LastModified = time.Now().UTC()
 	node.IsIngressGateway = false
 	if !servercfg.IsPro {
-		node.IsInternetGateway = false
+		node.EgressDetails.IsInternetGateway = false
 	}
 	delete(node.Tags, models.TagID(fmt.Sprintf("%s.%s", node.Network, models.GwTagName)))
 	node.IngressGatewayRange = ""

+ 19 - 2
logic/hosts.go

@@ -548,17 +548,29 @@ func GetRelatedHosts(hostID string) []models.Host {
 // CheckHostPort checks host endpoints to ensures that hosts on the same server
 // with the same endpoint have different listen ports
 // in the case of 64535 hosts or more with same endpoint, ports will not be changed
-func CheckHostPorts(h *models.Host) {
+func CheckHostPorts(h *models.Host) (changed bool) {
 	portsInUse := make(map[int]bool, 0)
 	hosts, err := GetAllHosts()
 	if err != nil {
 		return
 	}
+	originalPort := h.ListenPort
+	defer func() {
+		if originalPort != h.ListenPort {
+			changed = true
+		}
+	}()
+	if h.EndpointIP == nil {
+		return
+	}
 	for _, host := range hosts {
 		if host.ID.String() == h.ID.String() {
 			// skip self
 			continue
 		}
+		if host.EndpointIP == nil {
+			continue
+		}
 		if !host.EndpointIP.Equal(h.EndpointIP) {
 			continue
 		}
@@ -566,11 +578,16 @@ func CheckHostPorts(h *models.Host) {
 	}
 	// iterate until port is not found or max iteration is reached
 	for i := 0; portsInUse[h.ListenPort] && i < maxPort-minPort+1; i++ {
-		h.ListenPort++
+		if h.ListenPort == 443 {
+			h.ListenPort = 51821
+		} else {
+			h.ListenPort++
+		}
 		if h.ListenPort > maxPort {
 			h.ListenPort = minPort
 		}
 	}
+	return
 }
 
 // HostExists - checks if given host already exists

+ 2 - 2
logic/jwts.go

@@ -135,7 +135,7 @@ func GetUserNameFromToken(authtoken string) (username string, err error) {
 				err = errors.New("token revoked")
 				return "", err
 			}
-			a.LastUsed = time.Now()
+			a.LastUsed = time.Now().UTC()
 			a.Update(db.WithContext(context.TODO()))
 		}
 	}
@@ -181,7 +181,7 @@ func VerifyUserToken(tokenString string) (username string, issuperadmin, isadmin
 				err = errors.New("token revoked")
 				return "", false, false, err
 			}
-			a.LastUsed = time.Now()
+			a.LastUsed = time.Now().UTC()
 			a.Update(db.WithContext(context.TODO()))
 		}
 	}

+ 0 - 2
logic/networks.go

@@ -522,7 +522,6 @@ func UniqueAddress6DB(networkName string, reverse bool) (net.IP, error) {
 	var network models.Network
 	network, err := GetParentNetwork(networkName)
 	if err != nil {
-		fmt.Println("Network Not Found")
 		return add, err
 	}
 	if network.IsIPv6 == "no" {
@@ -567,7 +566,6 @@ func UniqueAddress6Cache(networkName string, reverse bool) (net.IP, error) {
 	var network models.Network
 	network, err := GetParentNetwork(networkName)
 	if err != nil {
-		fmt.Println("Network Not Found")
 		return add, err
 	}
 	if network.IsIPv6 == "no" {

+ 39 - 13
logic/nodes.go

@@ -164,7 +164,7 @@ func UpdateNodeCheckin(node *models.Node) error {
 	if err != nil {
 		return err
 	}
-
+	node.EgressDetails = models.EgressDetails{}
 	err = database.Insert(node.ID.String(), string(data), database.NODES_TABLE_NAME)
 	if err != nil {
 		return err
@@ -183,6 +183,7 @@ func UpsertNode(newNode *models.Node) error {
 	if err != nil {
 		return err
 	}
+	newNode.EgressDetails = models.EgressDetails{}
 	err = database.Insert(newNode.ID.String(), string(data), database.NODES_TABLE_NAME)
 	if err != nil {
 		return err
@@ -218,7 +219,7 @@ func UpdateNode(currentNode *models.Node, newNode *models.Node) error {
 				return err
 			}
 		}
-
+		newNode.EgressDetails = models.EgressDetails{}
 		newNode.SetLastModified()
 		if data, err := json.Marshal(newNode); err != nil {
 			return err
@@ -280,21 +281,21 @@ func DeleteNode(node *models.Node, purge bool) error {
 		// unset all the relayed nodes
 		SetRelayedNodes(false, node.ID.String(), node.RelayedNodes)
 	}
-	if node.InternetGwID != "" {
-		inetNode, err := GetNodeByID(node.InternetGwID)
+	if node.EgressDetails.InternetGwID != "" {
+		inetNode, err := GetNodeByID(node.EgressDetails.InternetGwID)
 		if err == nil {
 			clientNodeIDs := []string{}
-			for _, inetNodeClientID := range inetNode.InetNodeReq.InetNodeClientIDs {
+			for _, inetNodeClientID := range inetNode.EgressDetails.InetNodeReq.InetNodeClientIDs {
 				if inetNodeClientID == node.ID.String() {
 					continue
 				}
 				clientNodeIDs = append(clientNodeIDs, inetNodeClientID)
 			}
-			inetNode.InetNodeReq.InetNodeClientIDs = clientNodeIDs
+			inetNode.EgressDetails.InetNodeReq.InetNodeClientIDs = clientNodeIDs
 			UpsertNode(&inetNode)
 		}
 	}
-	if node.IsInternetGateway {
+	if node.EgressDetails.IsInternetGateway {
 		UnsetInternetGw(node)
 	}
 	if !purge && !alreadyDeleted {
@@ -320,8 +321,9 @@ func DeleteNode(node *models.Node, purge bool) error {
 	if err := DissasociateNodeFromHost(node, host); err != nil {
 		return err
 	}
-	go RemoveNodeFromAclPolicy(*node)
 
+	go RemoveNodeFromAclPolicy(*node)
+	go RemoveNodeFromEgress(*node)
 	return nil
 }
 
@@ -783,16 +785,16 @@ func ValidateNodeIp(currentNode *models.Node, newNode *models.ApiNode) error {
 	return nil
 }
 
-func ValidateEgressRange(gateway models.EgressGatewayRequest) error {
-	network, err := GetNetworkSettings(gateway.NetID)
+func ValidateEgressRange(netID string, ranges []string) error {
+	network, err := GetNetworkSettings(netID)
 	if err != nil {
-		slog.Error("error getting network with netid", "error", gateway.NetID, err.Error)
-		return errors.New("error getting network with netid:  " + gateway.NetID + " " + err.Error())
+		slog.Error("error getting network with netid", "error", netID, err.Error)
+		return errors.New("error getting network with netid:  " + netID + " " + err.Error())
 	}
 	ipv4Net := network.AddressRange
 	ipv6Net := network.AddressRange6
 
-	for _, v := range gateway.Ranges {
+	for _, v := range ranges {
 		if ipv4Net != "" {
 			if ContainsCIDR(ipv4Net, v) {
 				slog.Error("egress range should not be the same as or contained in the netmaker network address", "error", v, ipv4Net)
@@ -949,6 +951,30 @@ func AddTagMapWithStaticNodesWithUsers(netID models.NetworkID,
 	return tagNodesMap
 }
 
+func GetNodeIDsWithTag(tagID models.TagID) (ids []string) {
+
+	tag, err := GetTag(tagID)
+	if err != nil {
+		return
+	}
+	nodes, _ := GetNetworkNodes(tag.Network.String())
+	for _, nodeI := range nodes {
+		if nodeI.Tags == nil {
+			continue
+		}
+		if nodeI.Mutex != nil {
+			nodeI.Mutex.Lock()
+		}
+		if _, ok := nodeI.Tags[tagID]; ok {
+			ids = append(ids, nodeI.ID.String())
+		}
+		if nodeI.Mutex != nil {
+			nodeI.Mutex.Unlock()
+		}
+	}
+	return
+}
+
 func GetNodesWithTag(tagID models.TagID) map[string]models.Node {
 	nMap := make(map[string]models.Node)
 	tag, err := GetTag(tagID)

+ 50 - 44
logic/peers.go

@@ -6,6 +6,7 @@ import (
 	"net"
 	"net/netip"
 
+	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic/acls/nodeacls"
@@ -47,16 +48,19 @@ var (
 	}
 	// UnsetInternetGw
 	UnsetInternetGw = func(node *models.Node) {
-		node.IsInternetGateway = false
+		node.EgressDetails.IsInternetGateway = false
 	}
 	// SetInternetGw
 	SetInternetGw = func(node *models.Node, req models.InetNodeReq) {
-		node.IsInternetGateway = true
+		node.EgressDetails.IsInternetGateway = true
 	}
 	// GetAllowedIpForInetNodeClient
 	GetAllowedIpForInetNodeClient = func(node, peer *models.Node) []net.IPNet {
 		return []net.IPNet{}
 	}
+	ValidateInetGwReq = func(inetNode models.Node, req models.InetNodeReq, update bool) error {
+		return nil
+	}
 )
 
 // GetHostPeerInfo - fetches required peer info per network
@@ -161,26 +165,16 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 	}
 	defer func() {
 		if !hostPeerUpdate.FwUpdate.AllowAll {
-			aclRule := models.AclRule{
-				ID:              "allowed-network-rules",
-				AllowedProtocol: models.ALL,
-				Direction:       models.TrafficDirectionBi,
-				Allowed:         true,
-			}
-			for _, allowedNet := range hostPeerUpdate.FwUpdate.AllowedNetworks {
-				if allowedNet.IP.To4() != nil {
-					aclRule.IPList = append(aclRule.IPList, allowedNet)
-				} else {
-					aclRule.IP6List = append(aclRule.IP6List, allowedNet)
-				}
-			}
-			hostPeerUpdate.FwUpdate.AclRules["allowed-network-rules"] = aclRule
+
 			hostPeerUpdate.FwUpdate.EgressInfo["allowed-network-rules"] = models.EgressInfo{
-				EgressID: "allowed-network-rules",
-				EgressFwRules: map[string]models.AclRule{
-					"allowed-network-rules": aclRule,
-				},
+				EgressID:      "allowed-network-rules",
+				EgressFwRules: make(map[string]models.AclRule),
+			}
+			for _, aclRule := range hostPeerUpdate.FwUpdate.AllowedNetworks {
+				hostPeerUpdate.FwUpdate.AclRules[aclRule.ID] = aclRule
+				hostPeerUpdate.FwUpdate.EgressInfo["allowed-network-rules"].EgressFwRules[aclRule.ID] = aclRule
 			}
+
 		}
 	}()
 
@@ -189,14 +183,17 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 	for _, nodeID := range host.Nodes {
 		networkAllowAll := true
 		nodeID := nodeID
+		if nodeID == uuid.Nil.String() {
+			continue
+		}
 		node, err := GetNodeByID(nodeID)
 		if err != nil {
 			continue
 		}
-
 		if !node.Connected || node.PendingDelete || node.Action == models.NODE_DELETE {
 			continue
 		}
+		GetNodeEgressInfo(&node)
 		hostPeerUpdate = SetDefaultGw(node, hostPeerUpdate)
 		if !hostPeerUpdate.IsInternetGw {
 			hostPeerUpdate.IsInternetGw = IsInternetGw(node)
@@ -204,13 +201,22 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 		defaultUserPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy)
 		defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
 
-		if (defaultDevicePolicy.Enabled && defaultUserPolicy.Enabled) || (!checkIfAnyPolicyisUniDirectional(node) && !checkIfAnyActiveEgressPolicy(node)) {
-			if node.NetworkRange.IP != nil {
-				hostPeerUpdate.FwUpdate.AllowedNetworks = append(hostPeerUpdate.FwUpdate.AllowedNetworks, node.NetworkRange)
+		if (defaultDevicePolicy.Enabled && defaultUserPolicy.Enabled) ||
+			(!checkIfAnyPolicyisUniDirectional(node) && !checkIfAnyActiveEgressPolicy(node)) ||
+			checkIfNodeHasAccessToAllResources(&node) {
+			aclRule := models.AclRule{
+				ID:              fmt.Sprintf("%s-allowed-network-rules", node.ID.String()),
+				AllowedProtocol: models.ALL,
+				Direction:       models.TrafficDirectionBi,
+				Allowed:         true,
+				IPList:          []net.IPNet{node.NetworkRange},
+				IP6List:         []net.IPNet{node.NetworkRange6},
 			}
-			if node.NetworkRange6.IP != nil {
-				hostPeerUpdate.FwUpdate.AllowedNetworks = append(hostPeerUpdate.FwUpdate.AllowedNetworks, node.NetworkRange6)
+			if !(defaultDevicePolicy.Enabled && defaultUserPolicy.Enabled) {
+				aclRule.Dst = []net.IPNet{node.NetworkRange}
+				aclRule.Dst6 = []net.IPNet{node.NetworkRange6}
 			}
+			hostPeerUpdate.FwUpdate.AllowedNetworks = append(hostPeerUpdate.FwUpdate.AllowedNetworks, aclRule)
 		} else {
 			networkAllowAll = false
 			hostPeerUpdate.FwUpdate.AllowAll = false
@@ -247,8 +253,9 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 				PersistentKeepaliveInterval: &peerHost.PersistentKeepalive,
 				ReplaceAllowedIPs:           true,
 			}
+			AddEgressInfoToPeerByAccess(&node, &peer)
 			_, isFailOverPeer := node.FailOverPeers[peer.ID.String()]
-			if peer.IsEgressGateway {
+			if peer.EgressDetails.IsEgressGateway {
 				peerKey := peerHost.PublicKey.String()
 				if isFailOverPeer && peer.FailedOverBy.String() != node.ID.String() {
 					// get relay host
@@ -435,7 +442,7 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 				logger.Log(1, "error retrieving external clients:", err.Error())
 			}
 		}
-		if node.IsEgressGateway && node.EgressGatewayRequest.NatEnabled == "yes" && len(node.EgressGatewayRequest.Ranges) > 0 {
+		if node.EgressDetails.IsEgressGateway && len(node.EgressDetails.EgressGatewayRequest.Ranges) > 0 {
 			hostPeerUpdate.FwUpdate.IsEgressGw = true
 			hostPeerUpdate.FwUpdate.EgressInfo[node.ID.String()] = models.EgressInfo{
 				EgressID: node.ID.String(),
@@ -449,12 +456,12 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 					IP:   node.Address6.IP,
 					Mask: getCIDRMaskFromAddr(node.Address6.IP.String()),
 				},
-				EgressGWCfg:   node.EgressGatewayRequest,
+				EgressGWCfg:   node.EgressDetails.EgressGatewayRequest,
 				EgressFwRules: make(map[string]models.AclRule),
 			}
 
 		}
-		if node.IsEgressGateway {
+		if node.EgressDetails.IsEgressGateway {
 			if !networkAllowAll {
 				egressInfo := hostPeerUpdate.FwUpdate.EgressInfo[node.ID.String()]
 				if egressInfo.EgressFwRules == nil {
@@ -492,7 +499,6 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 				},
 			}
 		}
-
 	}
 	// == post peer calculations ==
 	// indicate removal if no allowed IPs were calculated
@@ -549,11 +555,11 @@ func GetPeerListenPort(host *models.Host) int {
 }
 
 func filterConflictingEgressRoutes(node, peer models.Node) []string {
-	egressIPs := slices.Clone(peer.EgressGatewayRanges)
-	if node.IsEgressGateway {
+	egressIPs := slices.Clone(peer.EgressDetails.EgressGatewayRanges)
+	if node.EgressDetails.IsEgressGateway {
 		// filter conflicting addrs
 		nodeEgressMap := make(map[string]struct{})
-		for _, rangeI := range node.EgressGatewayRanges {
+		for _, rangeI := range node.EgressDetails.EgressGatewayRanges {
 			nodeEgressMap[rangeI] = struct{}{}
 		}
 		for i := len(egressIPs) - 1; i >= 0; i-- {
@@ -567,11 +573,11 @@ func filterConflictingEgressRoutes(node, peer models.Node) []string {
 }
 
 func filterConflictingEgressRoutesWithMetric(node, peer models.Node) []models.EgressRangeMetric {
-	egressIPs := slices.Clone(peer.EgressGatewayRequest.RangesWithMetric)
-	if node.IsEgressGateway {
+	egressIPs := slices.Clone(peer.EgressDetails.EgressGatewayRequest.RangesWithMetric)
+	if node.EgressDetails.IsEgressGateway {
 		// filter conflicting addrs
 		nodeEgressMap := make(map[string]struct{})
-		for _, rangeI := range node.EgressGatewayRanges {
+		for _, rangeI := range node.EgressDetails.EgressGatewayRanges {
 			nodeEgressMap[rangeI] = struct{}{}
 		}
 		for i := len(egressIPs) - 1; i >= 0; i-- {
@@ -588,13 +594,13 @@ func filterConflictingEgressRoutesWithMetric(node, peer models.Node) []models.Eg
 func GetAllowedIPs(node, peer *models.Node, metrics *models.Metrics) []net.IPNet {
 	var allowedips []net.IPNet
 	allowedips = getNodeAllowedIPs(peer, node)
-	if peer.IsInternetGateway && node.InternetGwID == peer.ID.String() {
+	if peer.EgressDetails.IsInternetGateway && node.EgressDetails.InternetGwID == peer.ID.String() {
 		allowedips = append(allowedips, GetAllowedIpForInetNodeClient(node, peer)...)
 		return allowedips
 	}
 	if node.IsRelayed && node.RelayedBy == peer.ID.String() {
 		allowedips = append(allowedips, GetAllowedIpsForRelayed(node, peer)...)
-		if peer.InternetGwID != "" {
+		if peer.EgressDetails.InternetGwID != "" {
 			return allowedips
 		}
 	}
@@ -623,11 +629,11 @@ func GetEgressIPs(peer *models.Node) []net.IPNet {
 
 	// check for internet gateway
 	internetGateway := false
-	if slices.Contains(peer.EgressGatewayRanges, "0.0.0.0/0") || slices.Contains(peer.EgressGatewayRanges, "::/0") {
+	if slices.Contains(peer.EgressDetails.EgressGatewayRanges, "0.0.0.0/0") || slices.Contains(peer.EgressDetails.EgressGatewayRanges, "::/0") {
 		internetGateway = true
 	}
 	allowedips := []net.IPNet{}
-	for _, iprange := range peer.EgressGatewayRanges { // go through each cidr for egress gateway
+	for _, iprange := range peer.EgressDetails.EgressGatewayRanges { // go through each cidr for egress gateway
 		_, ipnet, err := net.ParseCIDR(iprange) // confirming it's valid cidr
 		if err != nil {
 			logger.Log(1, "could not parse gateway IP range. Not adding ", iprange)
@@ -669,13 +675,13 @@ func getNodeAllowedIPs(peer, node *models.Node) []net.IPNet {
 		allowedips = append(allowedips, allowed)
 	}
 	// handle egress gateway peers
-	if peer.IsEgressGateway {
+	if peer.EgressDetails.IsEgressGateway {
 		// hasGateway = true
 		egressIPs := GetEgressIPs(peer)
-		if node.IsEgressGateway {
+		if node.EgressDetails.IsEgressGateway {
 			// filter conflicting addrs
 			nodeEgressMap := make(map[string]struct{})
-			for _, rangeI := range node.EgressGatewayRanges {
+			for _, rangeI := range node.EgressDetails.EgressGatewayRanges {
 				nodeEgressMap[rangeI] = struct{}{}
 			}
 			for i := len(egressIPs) - 1; i >= 0; i-- {

+ 6 - 4
logic/relay.go

@@ -114,13 +114,14 @@ func ValidateRelay(relay models.RelayRequest, update bool) error {
 		if err != nil {
 			return err
 		}
+		GetNodeEgressInfo(&relayedNode)
 		if relayedNode.IsIngressGateway {
 			return errors.New("cannot relay an ingress gateway (" + relayedNodeID + ")")
 		}
-		if relayedNode.IsInternetGateway {
+		if relayedNode.EgressDetails.IsInternetGateway {
 			return errors.New("cannot relay an internet gateway (" + relayedNodeID + ")")
 		}
-		if relayedNode.InternetGwID != "" && relayedNode.InternetGwID != relay.NodeID {
+		if relayedNode.EgressDetails.InternetGwID != "" && relayedNode.EgressDetails.InternetGwID != relay.NodeID {
 			return errors.New("cannot relay an internet client (" + relayedNodeID + ")")
 		}
 		if relayedNode.IsFailOver {
@@ -193,8 +194,9 @@ func RelayedAllowedIPs(peer, node *models.Node) []net.IPNet {
 		if err != nil {
 			continue
 		}
+		GetNodeEgressInfo(&relayedNode)
 		allowed := getRelayedAddresses(relayedNodeID)
-		if relayedNode.IsEgressGateway {
+		if relayedNode.EgressDetails.IsEgressGateway {
 			allowed = append(allowed, GetEgressIPs(&relayedNode)...)
 		}
 		allowedIPs = append(allowedIPs, allowed...)
@@ -208,7 +210,7 @@ func GetAllowedIpsForRelayed(relayed, relay *models.Node) (allowedIPs []net.IPNe
 		logger.Log(0, "RelayedByRelay called with invalid parameters")
 		return
 	}
-	if relay.InternetGwID != "" {
+	if relay.EgressDetails.InternetGwID != "" {
 		return GetAllowedIpForInetNodeClient(relayed, relay)
 	}
 	peers, err := GetNetworkNodes(relay.Network)

+ 1 - 1
logic/tags.go

@@ -290,7 +290,7 @@ func CreateDefaultTags(netID models.NetworkID) {
 		TagName:   models.GwTagName,
 		Network:   netID,
 		CreatedBy: "auto",
-		CreatedAt: time.Now(),
+		CreatedAt: time.Now().UTC(),
 	}
 	_, err := GetTag(tag.ID)
 	if err == nil {

+ 2 - 0
logic/telemetry.go

@@ -20,6 +20,8 @@ var (
 	telServerRecord = models.Telemetry{}
 )
 
+var LogEvent = func(a *models.Event) {}
+
 // posthog_pub_key - Key for sending data to PostHog
 const posthog_pub_key = "phc_1vEXhPOA1P7HP5jP2dVU9xDTUqXHAelmtravyZ1vvES"
 

+ 0 - 14
logic/wireguard.go

@@ -9,24 +9,10 @@ func IfaceDelta(currentNode *models.Node, newNode *models.Node) bool {
 	// single comparison statements
 	if newNode.Address.String() != currentNode.Address.String() ||
 		newNode.Address6.String() != currentNode.Address6.String() ||
-		newNode.IsEgressGateway != currentNode.IsEgressGateway ||
-		newNode.IsIngressGateway != currentNode.IsIngressGateway ||
 		newNode.IsRelay != currentNode.IsRelay ||
-		newNode.DNSOn != currentNode.DNSOn ||
 		newNode.Connected != currentNode.Connected {
 		return true
 	}
-	// multi-comparison statements
-	if newNode.IsEgressGateway {
-		if len(currentNode.EgressGatewayRanges) != len(newNode.EgressGatewayRanges) {
-			return true
-		}
-		for _, address := range newNode.EgressGatewayRanges {
-			if !StringSliceContains(currentNode.EgressGatewayRanges, address) {
-				return true
-			}
-		}
-	}
 	if newNode.IsRelay {
 		if len(currentNode.RelayedNodes) != len(newNode.RelayedNodes) {
 			return true

+ 217 - 0
migrate/migrate.go

@@ -1,20 +1,24 @@
 package migrate
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 	"log"
 	"time"
 
 	"golang.org/x/exp/slog"
+	"gorm.io/datatypes"
 
 	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
+	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/servercfg"
 )
 
@@ -31,6 +35,7 @@ func Run() {
 	updateNodes()
 	updateAcls()
 	migrateToGws()
+	migrateToEgressV1()
 }
 
 func assignSuperAdmin() {
@@ -505,6 +510,218 @@ func migrateToGws() {
 	}
 }
 
+func migrateToEgressV1() {
+	nodes, _ := logic.GetAllNodes()
+	user, err := logic.GetSuperAdmin()
+	if err != nil {
+		return
+	}
+	for _, node := range nodes {
+		if node.IsEgressGateway {
+			egressHost, err := logic.GetHost(node.HostID.String())
+			if err != nil {
+				continue
+			}
+			for _, rangeI := range node.EgressGatewayRequest.Ranges {
+				e := schema.Egress{
+					ID:          uuid.New().String(),
+					Name:        fmt.Sprintf("%s egress", egressHost.Name),
+					Description: "",
+					Network:     node.Network,
+					Nodes: datatypes.JSONMap{
+						node.ID.String(): 256,
+					},
+					Tags:      make(datatypes.JSONMap),
+					Range:     rangeI,
+					Nat:       node.EgressGatewayRequest.NatEnabled == "yes",
+					Status:    true,
+					CreatedBy: user.UserName,
+					CreatedAt: time.Now().UTC(),
+				}
+				err = e.Create(db.WithContext(context.TODO()))
+				if err == nil {
+					node.IsEgressGateway = false
+					node.EgressGatewayRequest = models.EgressGatewayRequest{}
+					node.EgressGatewayNatEnabled = false
+					node.EgressGatewayRanges = []string{}
+					logic.UpsertNode(&node)
+					acl := models.Acl{
+						ID:          uuid.New().String(),
+						Name:        "egress node policy",
+						MetaData:    "",
+						Default:     false,
+						ServiceType: models.Any,
+						NetworkID:   models.NetworkID(node.Network),
+						Proto:       models.ALL,
+						RuleType:    models.DevicePolicy,
+						Src: []models.AclPolicyTag{
+
+							{
+								ID:    models.NodeTagID,
+								Value: "*",
+							},
+						},
+						Dst: []models.AclPolicyTag{
+							{
+								ID:    models.EgressID,
+								Value: e.ID,
+							},
+						},
+
+						AllowedDirection: models.TrafficDirectionUni,
+						Enabled:          true,
+						CreatedBy:        "auto",
+						CreatedAt:        time.Now().UTC(),
+					}
+					logic.InsertAcl(acl)
+					acl = models.Acl{
+						ID:          uuid.New().String(),
+						Name:        "egress node policy",
+						MetaData:    "",
+						Default:     false,
+						ServiceType: models.Any,
+						NetworkID:   models.NetworkID(node.Network),
+						Proto:       models.ALL,
+						RuleType:    models.UserPolicy,
+						Src: []models.AclPolicyTag{
+
+							{
+								ID:    models.UserGroupAclID,
+								Value: "*",
+							},
+						},
+						Dst: []models.AclPolicyTag{
+							{
+								ID:    models.EgressID,
+								Value: e.ID,
+							},
+						},
+
+						AllowedDirection: models.TrafficDirectionUni,
+						Enabled:          true,
+						CreatedBy:        "auto",
+						CreatedAt:        time.Now().UTC(),
+					}
+					logic.InsertAcl(acl)
+				}
+
+			}
+
+		}
+
+		if node.IsInternetGateway {
+			inetHost, err := logic.GetHost(node.HostID.String())
+			if err != nil {
+				continue
+			}
+			e := schema.Egress{
+				ID:          uuid.New().String(),
+				Name:        fmt.Sprintf("%s inet gw", inetHost.Name),
+				Description: "add description",
+				Network:     node.Network,
+				Nodes: datatypes.JSONMap{
+					node.ID.String(): 256,
+				},
+				Tags:      make(datatypes.JSONMap),
+				Range:     "",
+				IsInetGw:  true,
+				Nat:       node.EgressGatewayRequest.NatEnabled == "yes",
+				Status:    true,
+				CreatedBy: user.UserName,
+				CreatedAt: time.Now().UTC(),
+			}
+			err = e.Create(db.WithContext(context.TODO()))
+			if err == nil {
+				node.IsEgressGateway = false
+				node.EgressGatewayRequest = models.EgressGatewayRequest{}
+				node.EgressGatewayNatEnabled = false
+				node.EgressGatewayRanges = []string{}
+				node.IsInternetGateway = false
+				src := []models.AclPolicyTag{}
+				for _, inetClientID := range node.InetNodeReq.InetNodeClientIDs {
+					_, err := logic.GetNodeByID(inetClientID)
+					if err == nil {
+						src = append(src, models.AclPolicyTag{
+							ID:    models.NodeID,
+							Value: inetClientID,
+						})
+					}
+				}
+				acl := models.Acl{
+					ID:          uuid.New().String(),
+					Name:        "exit node policy",
+					MetaData:    "all traffic on source nodes will pass through the destination node in the policy",
+					Default:     false,
+					ServiceType: models.Any,
+					NetworkID:   models.NetworkID(node.Network),
+					Proto:       models.ALL,
+					RuleType:    models.DevicePolicy,
+					Src:         src,
+					Dst: []models.AclPolicyTag{
+						{
+							ID:    models.EgressID,
+							Value: e.ID,
+						},
+					},
+
+					AllowedDirection: models.TrafficDirectionBi,
+					Enabled:          true,
+					CreatedBy:        "auto",
+					CreatedAt:        time.Now().UTC(),
+				}
+				logic.InsertAcl(acl)
+
+				acl = models.Acl{
+					ID:          uuid.New().String(),
+					Name:        "exit node policy",
+					MetaData:    "all traffic on source nodes will pass through the destination node in the policy",
+					Default:     false,
+					ServiceType: models.Any,
+					NetworkID:   models.NetworkID(node.Network),
+					Proto:       models.ALL,
+					RuleType:    models.UserPolicy,
+					Src: []models.AclPolicyTag{
+						{
+							ID:    models.UserGroupAclID,
+							Value: fmt.Sprintf("%s-%s-grp", node.Network, models.NetworkAdmin),
+						},
+						{
+							ID:    models.UserGroupAclID,
+							Value: fmt.Sprintf("global-%s-grp", models.NetworkAdmin),
+						},
+						{
+							ID:    models.UserGroupAclID,
+							Value: fmt.Sprintf("%s-%s-grp", node.Network, models.NetworkUser),
+						},
+						{
+							ID:    models.UserGroupAclID,
+							Value: fmt.Sprintf("global-%s-grp", models.NetworkUser),
+						},
+					},
+					Dst: []models.AclPolicyTag{
+						{
+							ID:    models.EgressID,
+							Value: e.ID,
+						},
+					},
+
+					AllowedDirection: models.TrafficDirectionBi,
+					Enabled:          true,
+					CreatedBy:        "auto",
+					CreatedAt:        time.Now().UTC(),
+				}
+				logic.InsertAcl(acl)
+				node.InetNodeReq = models.InetNodeReq{}
+				logic.UpsertNode(&node)
+			}
+		}
+		if node.InternetGwID != "" {
+			node.InternetGwID = ""
+			logic.UpsertNode(&node)
+		}
+	}
+}
+
 func settings() {
 	_, err := database.FetchRecords(database.SERVER_SETTINGS)
 	if database.IsEmptyRecord(err) {

+ 60 - 0
models/accessToken.go

@@ -0,0 +1,60 @@
+package models
+
+import (
+	"context"
+	"time"
+
+	"github.com/gravitl/netmaker/db"
+)
+
+// accessTokenTableName - access tokens table
+const accessTokenTableName = "user_access_tokens"
+
+// UserAccessToken - token used to access netmaker
+type UserAccessToken struct {
+	ID        string    `gorm:"id,primary_key" json:"id"`
+	Name      string    `gorm:"name" json:"name"`
+	UserName  string    `gorm:"user_name" json:"user_name"`
+	ExpiresAt time.Time `gorm:"expires_at" json:"expires_at"`
+	LastUsed  time.Time `gorm:"last_used" json:"last_used"`
+	CreatedBy string    `gorm:"created_by" json:"created_by"`
+	CreatedAt time.Time `gorm:"created_at" json:"created_at"`
+}
+
+func (a *UserAccessToken) Table() string {
+	return accessTokenTableName
+}
+
+func (a *UserAccessToken) Get() error {
+	return db.FromContext(context.TODO()).Table(a.Table()).First(&a).Where("id = ?", a.ID).Error
+}
+
+func (a *UserAccessToken) Update() error {
+	return db.FromContext(context.TODO()).Table(a.Table()).Where("id = ?", a.ID).Updates(&a).Error
+}
+
+func (a *UserAccessToken) Create() error {
+	return db.FromContext(context.TODO()).Table(a.Table()).Create(&a).Error
+}
+
+func (a *UserAccessToken) List() (ats []UserAccessToken, err error) {
+	err = db.FromContext(context.TODO()).Table(a.Table()).Find(&ats).Error
+	return
+}
+
+func (a *UserAccessToken) ListByUser() (ats []UserAccessToken) {
+	db.FromContext(context.TODO()).Table(a.Table()).Where("user_name = ?", a.UserName).Find(&ats)
+	if ats == nil {
+		ats = []UserAccessToken{}
+	}
+	return
+}
+
+func (a *UserAccessToken) Delete() error {
+	return db.FromContext(context.TODO()).Table(a.Table()).Where("id = ?", a.ID).Delete(&a).Error
+}
+
+func (a *UserAccessToken) DeleteAllUserTokens() error {
+	return db.FromContext(context.TODO()).Table(a.Table()).Where("user_name = ? OR created_by = ?", a.UserName, a.UserName).Delete(&a).Error
+
+}

+ 1 - 0
models/acl.go

@@ -60,6 +60,7 @@ const (
 	NodeTagID                AclGroupType = "tag"
 	NodeID                   AclGroupType = "device"
 	EgressRange              AclGroupType = "egress-range"
+	EgressID                 AclGroupType = "egress-id"
 	NetmakerIPAclID          AclGroupType = "ip"
 	NetmakerSubNetRangeAClID AclGroupType = "ipset"
 )

+ 6 - 15
models/api_node.go

@@ -79,21 +79,16 @@ func (a *ApiNode) ConvertToServerNode(currentNode *Node) *Node {
 	convertedNode.PendingDelete = a.PendingDelete
 	convertedNode.FailedOverBy = currentNode.FailedOverBy
 	convertedNode.FailOverPeers = currentNode.FailOverPeers
-	convertedNode.IsEgressGateway = a.IsEgressGateway
 	convertedNode.IsIngressGateway = a.IsIngressGateway
-	// prevents user from changing ranges, must delete and recreate
-	convertedNode.EgressGatewayRanges = currentNode.EgressGatewayRanges
 	convertedNode.IngressGatewayRange = currentNode.IngressGatewayRange
 	convertedNode.IngressGatewayRange6 = currentNode.IngressGatewayRange6
 	convertedNode.DNSOn = a.DNSOn
 	convertedNode.IngressDNS = a.IngressDns
 	convertedNode.IngressPersistentKeepalive = a.IngressPersistentKeepalive
 	convertedNode.IngressMTU = a.IngressMTU
-	convertedNode.IsInternetGateway = a.IsInternetGateway
-	convertedNode.EgressGatewayRequest = currentNode.EgressGatewayRequest
-	convertedNode.EgressGatewayNatEnabled = currentNode.EgressGatewayNatEnabled
-	convertedNode.InternetGwID = currentNode.InternetGwID
-	convertedNode.InetNodeReq = currentNode.InetNodeReq
+	convertedNode.EgressDetails.IsInternetGateway = a.IsInternetGateway
+	convertedNode.EgressDetails.InternetGwID = currentNode.EgressDetails.InternetGwID
+	convertedNode.EgressDetails.InetNodeReq = currentNode.EgressDetails.InetNodeReq
 	convertedNode.RelayedNodes = a.RelayedNodes
 	convertedNode.DefaultACL = a.DefaultACL
 	convertedNode.OwnerID = currentNode.OwnerID
@@ -187,11 +182,7 @@ func (nm *Node) ConvertToAPINode() *ApiNode {
 	apiNode.IsRelay = nm.IsRelay
 	apiNode.RelayedBy = nm.RelayedBy
 	apiNode.RelayedNodes = nm.RelayedNodes
-	apiNode.IsEgressGateway = nm.IsEgressGateway
 	apiNode.IsIngressGateway = nm.IsIngressGateway
-	apiNode.EgressGatewayRanges = nm.EgressGatewayRanges
-	apiNode.EgressGatewayRangesWithMetric = nm.EgressGatewayRequest.RangesWithMetric
-	apiNode.EgressGatewayNatEnabled = nm.EgressGatewayNatEnabled
 	apiNode.DNSOn = nm.DNSOn
 	apiNode.IngressDns = nm.IngressDNS
 	apiNode.IngressPersistentKeepalive = nm.IngressPersistentKeepalive
@@ -200,9 +191,9 @@ func (nm *Node) ConvertToAPINode() *ApiNode {
 	apiNode.Connected = nm.Connected
 	apiNode.PendingDelete = nm.PendingDelete
 	apiNode.DefaultACL = nm.DefaultACL
-	apiNode.IsInternetGateway = nm.IsInternetGateway
-	apiNode.InternetGwID = nm.InternetGwID
-	apiNode.InetNodeReq = nm.InetNodeReq
+	apiNode.IsInternetGateway = nm.EgressDetails.IsInternetGateway
+	apiNode.InternetGwID = nm.EgressDetails.InternetGwID
+	apiNode.InetNodeReq = nm.EgressDetails.InetNodeReq
 	apiNode.IsFailOver = nm.IsFailOver
 	apiNode.FailOverPeers = nm.FailOverPeers
 	apiNode.FailedOverBy = nm.FailedOverBy

+ 14 - 0
models/egress.go

@@ -0,0 +1,14 @@
+package models
+
+type EgressReq struct {
+	ID          string         `json:"id"`
+	Name        string         `json:"name"`
+	Network     string         `json:"network"`
+	Description string         `json:"description"`
+	Nodes       map[string]int `json:"nodes"`
+	Tags        []string       `json:"tags"`
+	Range       string         `json:"range"`
+	Nat         bool           `json:"nat"`
+	Status      bool           `json:"status"`
+	IsInetGw    bool           `json:"is_internet_gateway"`
+}

+ 74 - 0
models/events.go

@@ -0,0 +1,74 @@
+package models
+
+type Action string
+
+const (
+	Create            Action = "CREATE"
+	Update            Action = "UPDATE"
+	Delete            Action = "DELETE"
+	DeleteAll         Action = "DELETE_ALL"
+	Login             Action = "LOGIN"
+	LogOut            Action = "LOGOUT"
+	Connect           Action = "CONNECT"
+	Sync              Action = "SYNC"
+	Disconnect        Action = "DISCONNECT"
+	JoinHostToNet     Action = "JOIN_HOST_TO_NETWORK"
+	RemoveHostFromNet Action = "REMOVE_HOST_FROM_NETWORK"
+)
+
+type SubjectType string
+
+const (
+	UserSub            SubjectType = "USER"
+	UserAccessTokenSub SubjectType = "USER_ACCESS_TOKEN"
+	DeviceSub          SubjectType = "DEVICE"
+	NodeSub            SubjectType = "NODE"
+	GatewaySub         SubjectType = "GATEWAY"
+	SettingSub         SubjectType = "SETTING"
+	AclSub             SubjectType = "ACL"
+	TagSub             SubjectType = "TAG"
+	UserRoleSub        SubjectType = "USER_ROLE"
+	UserGroupSub       SubjectType = "USER_GROUP"
+	UserInviteSub      SubjectType = "USER_INVITE"
+	PendingUserSub     SubjectType = "PENDING_USER"
+	EgressSub          SubjectType = "EGRESS"
+	NetworkSub         SubjectType = "NETWORK"
+	DashboardSub       SubjectType = "DASHBOARD"
+	EnrollmentKeySub   SubjectType = "ENROLLMENT_KEY"
+	ClientAppSub       SubjectType = "CLIENT-APP"
+)
+
+func (sub SubjectType) String() string {
+	return string(sub)
+}
+
+type Origin string
+
+const (
+	Dashboard Origin = "DASHBOARD"
+	Api       Origin = "API"
+	NMCTL     Origin = "NMCTL"
+	ClientApp Origin = "CLIENT-APP"
+)
+
+type Subject struct {
+	ID   string      `json:"id"`
+	Name string      `json:"name"`
+	Type SubjectType `json:"subject_type"`
+	Info interface{} `json:"info"`
+}
+
+type Diff struct {
+	Old interface{}
+	New interface{}
+}
+
+type Event struct {
+	Action      Action
+	Source      Subject
+	Origin      Origin
+	Target      Subject
+	TriggeredBy string
+	NetworkID   NetworkID
+	Diff        Diff
+}

+ 1 - 1
models/mqtt.go

@@ -107,7 +107,7 @@ type KeyUpdate struct {
 // FwUpdate - struct for firewall updates
 type FwUpdate struct {
 	AllowAll        bool                   `json:"allow_all"`
-	AllowedNetworks []net.IPNet            `json:"networks"`
+	AllowedNetworks []AclRule              `json:"networks"`
 	IsEgressGw      bool                   `json:"is_egress_gw"`
 	IsIngressGw     bool                   `json:"is_ingress_gw"`
 	EgressInfo      map[string]EgressInfo  `json:"egress_info"`

+ 14 - 12
models/node.go

@@ -109,7 +109,7 @@ type Node struct {
 	DefaultACL        string              `json:"defaultacl,omitempty"    bson:"defaultacl,omitempty"    yaml:"defaultacl,omitempty"    validate:"checkyesornoorunset"`
 	OwnerID           string              `json:"ownerid,omitempty"       bson:"ownerid,omitempty"       yaml:"ownerid,omitempty"`
 	IsFailOver        bool                `json:"is_fail_over"                                           yaml:"is_fail_over"`
-	FailOverPeers     map[string]struct{} `json:"fail_over_peers"                                        yaml:"fail_over_peers"`
+	FailOverPeers     map[string]struct{} `json:"fail_over_peers"                                       yaml:"fail_over_peers"`
 	FailedOverBy      uuid.UUID           `json:"failed_over_by"                                         yaml:"failed_over_by"`
 	IsInternetGateway bool                `json:"isinternetgateway"                                      yaml:"isinternetgateway"`
 	InetNodeReq       InetNodeReq         `json:"inet_node_req"                                          yaml:"inet_node_req"`
@@ -121,6 +121,16 @@ type Node struct {
 	StaticNode        ExtClient           `json:"static_node"`
 	Status            NodeStatus          `json:"node_status"`
 	Mutex             *sync.Mutex         `json:"-"`
+	EgressDetails     EgressDetails       `json:"-"`
+}
+type EgressDetails struct {
+	EgressGatewayNatEnabled bool
+	EgressGatewayRequest    EgressGatewayRequest
+	IsEgressGateway         bool
+	EgressGatewayRanges     []string
+	IsInternetGateway       bool        `json:"isinternetgateway"                                      yaml:"isinternetgateway"`
+	InetNodeReq             InetNodeReq `json:"inet_node_req"                                          yaml:"inet_node_req"`
+	InternetGwID            string      `json:"internetgw_node_id"                                     yaml:"internetgw_node_id"`
 }
 
 // LegacyNode - legacy struct for node model
@@ -377,17 +387,17 @@ func (node *LegacyNode) SetIsStaticDefault() {
 
 // Node.SetLastModified - set last modified initial time
 func (node *Node) SetLastModified() {
-	node.LastModified = time.Now()
+	node.LastModified = time.Now().UTC()
 }
 
 // Node.SetLastCheckIn - set checkin time of node
 func (node *Node) SetLastCheckIn() {
-	node.LastCheckIn = time.Now()
+	node.LastCheckIn = time.Now().UTC()
 }
 
 // Node.SetLastPeerUpdate - sets last peer update time
 func (node *Node) SetLastPeerUpdate() {
-	node.LastPeerUpdate = time.Now()
+	node.LastPeerUpdate = time.Now().UTC()
 }
 
 // Node.SetExpirationDateTime - sets node expiry time
@@ -442,15 +452,9 @@ func (newNode *Node) Fill(
 	if newNode.Network == "" {
 		newNode.Network = currentNode.Network
 	}
-	if newNode.IsEgressGateway != currentNode.IsEgressGateway {
-		newNode.IsEgressGateway = currentNode.IsEgressGateway
-	}
 	if newNode.IsIngressGateway != currentNode.IsIngressGateway {
 		newNode.IsIngressGateway = currentNode.IsIngressGateway
 	}
-	if newNode.EgressGatewayRanges == nil {
-		newNode.EgressGatewayRanges = currentNode.EgressGatewayRanges
-	}
 	if newNode.IngressGatewayRange == "" {
 		newNode.IngressGatewayRange = currentNode.IngressGatewayRange
 	}
@@ -567,7 +571,6 @@ func (ln *LegacyNode) ConvertToNewNode() (*Host, *Node) {
 		}
 	}
 	node.Action = ln.Action
-	node.IsEgressGateway = parseBool(ln.IsEgressGateway)
 	node.IsIngressGateway = parseBool(ln.IsIngressGateway)
 	node.DNSOn = parseBool(ln.DNSOn)
 
@@ -601,7 +604,6 @@ func (n *Node) Legacy(h *Host, s *ServerConfig, net *Network) *LegacyNode {
 	//l.IsRelay = formatBool(n.IsRelay)
 	//l.IsDocker = formatBool(n.IsDocker)
 	//l.IsK8S = formatBool(n.IsK8S)
-	l.IsEgressGateway = formatBool(n.IsEgressGateway)
 	l.IsIngressGateway = formatBool(n.IsIngressGateway)
 	//l.EgressGatewayRanges = n.EgressGatewayRanges
 	//l.EgressGatewayNatEnabled = n.EgressGatewayNatEnabled

+ 1 - 0
models/structs.go

@@ -156,6 +156,7 @@ type ExtPeersResponse struct {
 type EgressRangeMetric struct {
 	Network     string `json:"network"`
 	RouteMetric uint32 `json:"route_metric"` // preffered range 1-999
+	Nat         bool   `json:"nat"`
 }
 
 // EgressGatewayRequest - egress gateway request

+ 16 - 1
pro/auth/azure-ad.go

@@ -184,7 +184,22 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		logger.Log(1, "could not parse jwt for user", authRequest.UserName)
 		return
 	}
-
+	logic.LogEvent(&models.Event{
+		Action: models.Login,
+		Source: models.Subject{
+			ID:   user.UserName,
+			Name: user.UserName,
+			Type: models.UserSub,
+		},
+		TriggeredBy: user.UserName,
+		Target: models.Subject{
+			ID:   models.DashboardSub.String(),
+			Name: models.DashboardSub.String(),
+			Type: models.DashboardSub,
+			Info: user,
+		},
+		Origin: models.Dashboard,
+	})
 	logger.Log(1, "completed azure OAuth sigin in for", content.Email)
 	http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.Email, http.StatusPermanentRedirect)
 }

+ 16 - 1
pro/auth/github.go

@@ -175,7 +175,22 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 		logger.Log(1, "could not parse jwt for user", authRequest.UserName)
 		return
 	}
-
+	logic.LogEvent(&models.Event{
+		Action: models.Login,
+		Source: models.Subject{
+			ID:   user.UserName,
+			Name: user.UserName,
+			Type: models.UserSub,
+		},
+		TriggeredBy: user.UserName,
+		Target: models.Subject{
+			ID:   models.DashboardSub.String(),
+			Name: models.DashboardSub.String(),
+			Type: models.DashboardSub,
+			Info: user,
+		},
+		Origin: models.Dashboard,
+	})
 	logger.Log(1, "completed github OAuth sigin in for", content.Email)
 	http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.Email, http.StatusPermanentRedirect)
 }

+ 18 - 0
pro/auth/google.go

@@ -69,6 +69,7 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		return
 	}
+
 	var inviteExists bool
 	// check if invite exists for User
 	in, err := logic.GetUserInvite(content.Email)
@@ -167,6 +168,23 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	logic.LogEvent(&models.Event{
+		Action: models.Login,
+		Source: models.Subject{
+			ID:   user.UserName,
+			Name: user.UserName,
+			Type: models.UserSub,
+		},
+		TriggeredBy: user.UserName,
+		Target: models.Subject{
+			ID:   models.DashboardSub.String(),
+			Name: models.DashboardSub.String(),
+			Type: models.DashboardSub,
+			Info: user,
+		},
+		Origin: models.Dashboard,
+	})
+
 	logger.Log(1, "completed google OAuth sigin in for", content.Email)
 	http.Redirect(w, r, fmt.Sprintf("%s/login?login=%s&user=%s", servercfg.GetFrontendURL(), jwt, content.Email), http.StatusPermanentRedirect)
 }

+ 16 - 1
pro/auth/oidc.go

@@ -175,7 +175,22 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 		logger.Log(1, "could not parse jwt for user", authRequest.UserName, jwtErr.Error())
 		return
 	}
-
+	logic.LogEvent(&models.Event{
+		Action: models.Login,
+		Source: models.Subject{
+			ID:   user.UserName,
+			Name: user.UserName,
+			Type: models.UserSub,
+		},
+		TriggeredBy: user.UserName,
+		Target: models.Subject{
+			ID:   models.DashboardSub.String(),
+			Name: models.DashboardSub.String(),
+			Type: models.DashboardSub,
+			Info: user,
+		},
+		Origin: models.Dashboard,
+	})
 	logger.Log(1, "completed OIDC OAuth signin in for", content.Email)
 	http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.Email, http.StatusPermanentRedirect)
 }

+ 114 - 0
pro/controllers/events.go

@@ -0,0 +1,114 @@
+package controllers
+
+import (
+	"net/http"
+	"strconv"
+
+	"github.com/gorilla/mux"
+	"github.com/gravitl/netmaker/db"
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/schema"
+)
+
+func EventHandlers(r *mux.Router) {
+	r.HandleFunc("/api/v1/network/activity", logic.SecurityCheck(true, http.HandlerFunc(listNetworkActivity))).Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/user/activity", logic.SecurityCheck(true, http.HandlerFunc(listUserActivity))).Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/activity", logic.SecurityCheck(true, http.HandlerFunc(listActivity))).Methods(http.MethodGet)
+}
+
+// @Summary     list activity.
+// @Router      /api/v1/activity [get]
+// @Tags        Activity
+// @Param       network_id query string true "network_id required to get the network events"
+// @Success     200 {object}  models.ReturnSuccessResponseWithJson
+// @Failure     500 {object} models.ErrorResponse
+func listNetworkActivity(w http.ResponseWriter, r *http.Request) {
+	netID := r.URL.Query().Get("network_id")
+	// Parse query parameters with defaults
+	if netID == "" {
+		logic.ReturnErrorResponse(w, r, models.ErrorResponse{
+			Code:    http.StatusBadRequest,
+			Message: "network_id param is missing",
+		})
+		return
+	}
+	page, _ := strconv.Atoi(r.URL.Query().Get("page"))
+	pageSize, _ := strconv.Atoi(r.URL.Query().Get("per_page"))
+	ctx := db.WithContext(r.Context())
+	netActivity, err := (&schema.Event{NetworkID: models.NetworkID(netID)}).ListByNetwork(db.SetPagination(ctx, page, pageSize))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, models.ErrorResponse{
+			Code:    http.StatusInternalServerError,
+			Message: err.Error(),
+		})
+		return
+	}
+
+	logic.ReturnSuccessResponseWithJson(w, r, netActivity, "successfully fetched network activity")
+}
+
+// @Summary     list activity.
+// @Router      /api/v1/activity [get]
+// @Tags        Activity
+// @Param       network_id query string true "network_id required to get the network events"
+// @Success     200 {object}  models.ReturnSuccessResponseWithJson
+// @Failure     500 {object} models.ErrorResponse
+func listUserActivity(w http.ResponseWriter, r *http.Request) {
+	username := r.URL.Query().Get("username")
+	// Parse query parameters with defaults
+	if username == "" {
+		logic.ReturnErrorResponse(w, r, models.ErrorResponse{
+			Code:    http.StatusBadRequest,
+			Message: "username param is missing",
+		})
+		return
+	}
+	page, _ := strconv.Atoi(r.URL.Query().Get("page"))
+	pageSize, _ := strconv.Atoi(r.URL.Query().Get("per_page"))
+	ctx := db.WithContext(r.Context())
+	userActivity, err := (&schema.Event{TriggeredBy: username}).ListByUser(db.SetPagination(ctx, page, pageSize))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, models.ErrorResponse{
+			Code:    http.StatusInternalServerError,
+			Message: err.Error(),
+		})
+		return
+	}
+
+	logic.ReturnSuccessResponseWithJson(w, r, userActivity, "successfully fetched user activity "+username)
+}
+
+// @Summary     list activity.
+// @Router      /api/v1/activity [get]
+// @Tags        Activity
+// @Success     200 {object}  models.ReturnSuccessResponseWithJson
+// @Failure     500 {object} models.ErrorResponse
+func listActivity(w http.ResponseWriter, r *http.Request) {
+	username := r.URL.Query().Get("username")
+	network := r.URL.Query().Get("network_id")
+	page, _ := strconv.Atoi(r.URL.Query().Get("page"))
+	pageSize, _ := strconv.Atoi(r.URL.Query().Get("per_page"))
+	ctx := db.WithContext(r.Context())
+	var err error
+	var events []schema.Event
+	e := &schema.Event{TriggeredBy: username, NetworkID: models.NetworkID(network)}
+	if username != "" && network != "" {
+		events, err = e.ListByUserAndNetwork(db.SetPagination(ctx, page, pageSize))
+	} else if username != "" && network == "" {
+		events, err = e.ListByUser(db.SetPagination(ctx, page, pageSize))
+	} else if username == "" && network != "" {
+		events, err = e.ListByNetwork(db.SetPagination(ctx, page, pageSize))
+	} else {
+		events, err = e.List(db.SetPagination(ctx, page, pageSize))
+	}
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, models.ErrorResponse{
+			Code:    http.StatusInternalServerError,
+			Message: err.Error(),
+		})
+		return
+	}
+
+	logic.ReturnSuccessResponseWithJson(w, r, events, "successfully fetched all events ")
+}

+ 30 - 4
pro/controllers/failover.go

@@ -205,6 +205,8 @@ func failOverME(w http.ResponseWriter, r *http.Request) {
 		)
 		return
 	}
+	logic.GetNodeEgressInfo(&node)
+	logic.GetNodeEgressInfo(&peerNode)
 	if peerNode.IsFailOver {
 		logic.ReturnErrorResponse(
 			w,
@@ -245,7 +247,7 @@ func failOverME(w http.ResponseWriter, r *http.Request) {
 		)
 		return
 	}
-	if node.IsInternetGateway && peerNode.InternetGwID == node.ID.String() {
+	if node.EgressDetails.IsInternetGateway && peerNode.EgressDetails.InternetGwID == node.ID.String() {
 		logic.ReturnErrorResponse(
 			w,
 			r,
@@ -256,7 +258,7 @@ func failOverME(w http.ResponseWriter, r *http.Request) {
 		)
 		return
 	}
-	if node.InternetGwID != "" && node.InternetGwID == peerNode.ID.String() {
+	if node.EgressDetails.InternetGwID != "" && node.EgressDetails.InternetGwID == peerNode.ID.String() {
 		logic.ReturnErrorResponse(
 			w,
 			r,
@@ -349,6 +351,8 @@ func checkfailOverCtx(w http.ResponseWriter, r *http.Request) {
 		)
 		return
 	}
+	logic.GetNodeEgressInfo(&node)
+	logic.GetNodeEgressInfo(&peerNode)
 	if peerNode.IsFailOver {
 		logic.ReturnErrorResponse(
 			w,
@@ -389,7 +393,18 @@ func checkfailOverCtx(w http.ResponseWriter, r *http.Request) {
 		)
 		return
 	}
-	if node.IsInternetGateway && peerNode.InternetGwID == node.ID.String() {
+	if node.EgressDetails.InternetGwID != "" || peerNode.EgressDetails.InternetGwID != "" {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				errors.New("node using a internet gw by the peer node"),
+				"badrequest",
+			),
+		)
+		return
+	}
+	if node.EgressDetails.IsInternetGateway && peerNode.EgressDetails.InternetGwID == node.ID.String() {
 		logic.ReturnErrorResponse(
 			w,
 			r,
@@ -400,7 +415,7 @@ func checkfailOverCtx(w http.ResponseWriter, r *http.Request) {
 		)
 		return
 	}
-	if node.InternetGwID != "" && node.InternetGwID == peerNode.ID.String() {
+	if node.EgressDetails.InternetGwID != "" && node.EgressDetails.InternetGwID == peerNode.ID.String() {
 		logic.ReturnErrorResponse(
 			w,
 			r,
@@ -411,6 +426,17 @@ func checkfailOverCtx(w http.ResponseWriter, r *http.Request) {
 		)
 		return
 	}
+	if ok := logic.IsPeerAllowed(node, peerNode, true); !ok {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				errors.New("peers are not allowed to communicate"),
+				"badrequest",
+			),
+		)
+		return
+	}
 
 	err = proLogic.CheckFailOverCtx(failOverNode, node, peerNode)
 	if err != nil {

+ 2 - 2
pro/controllers/inet_gws.go

@@ -44,7 +44,7 @@ func createInternetGw(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
-	if node.IsInternetGateway {
+	if node.EgressDetails.IsInternetGateway {
 		logic.ReturnSuccessResponse(w, r, "node is already acting as internet gateway")
 		return
 	}
@@ -132,7 +132,7 @@ func updateInternetGw(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
-	if !node.IsInternetGateway {
+	if !node.EgressDetails.IsInternetGateway {
 		logic.ReturnErrorResponse(
 			w,
 			r,

+ 178 - 5
pro/controllers/users.go

@@ -250,6 +250,21 @@ func inviteUsers(w http.ResponseWriter, r *http.Request) {
 		if err != nil {
 			slog.Error("failed to insert invite for user", "email", invite.Email, "error", err)
 		}
+		logic.LogEvent(&models.Event{
+			Action: models.Create,
+			Source: models.Subject{
+				ID:   callerUserName,
+				Name: callerUserName,
+				Type: models.UserSub,
+			},
+			TriggeredBy: callerUserName,
+			Target: models.Subject{
+				ID:   inviteeEmail,
+				Name: inviteeEmail,
+				Type: models.UserInviteSub,
+			},
+			Origin: models.Dashboard,
+		})
 		// notify user with magic link
 		go func(invite models.UserInvite) {
 			// Set E-Mail body. You can set plain text or html with text/html
@@ -268,6 +283,7 @@ func inviteUsers(w http.ResponseWriter, r *http.Request) {
 			}
 		}(invite)
 	}
+
 	logic.ReturnSuccessResponse(w, r, "triggered user invites")
 }
 
@@ -311,6 +327,21 @@ func deleteUserInvite(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		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:   email,
+			Name: email,
+			Type: models.UserInviteSub,
+		},
+		Origin: models.Dashboard,
+	})
 	logic.ReturnSuccessResponse(w, r, "deleted user invite")
 }
 
@@ -465,6 +496,21 @@ func createUserGroup(w http.ResponseWriter, r *http.Request) {
 		user.UserGroups[userGroupReq.Group.ID] = struct{}{}
 		logic.UpsertUser(*user)
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Create,
+		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:   userGroupReq.Group.ID.String(),
+			Name: userGroupReq.Group.Name,
+			Type: models.UserGroupSub,
+		},
+		Origin: models.Dashboard,
+	})
 	logic.ReturnSuccessResponseWithJson(w, r, userGroupReq.Group, "created user group")
 }
 
@@ -511,7 +557,25 @@ func updateUserGroup(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-
+	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:   userGroup.ID.String(),
+			Name: userGroup.Name,
+			Type: models.UserGroupSub,
+		},
+		Diff: models.Diff{
+			Old: currUserG,
+			New: userGroup,
+		},
+		Origin: models.Dashboard,
+	})
 	// reset configs for service user
 	go proLogic.UpdatesUserGwAccessOnGrpUpdates(currUserG.NetworkRoles, userGroup.NetworkRoles)
 	logic.ReturnSuccessResponseWithJson(w, r, userGroup, "updated user group")
@@ -556,6 +620,21 @@ func deleteUserGroup(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		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:   userG.ID.String(),
+			Name: userG.Name,
+			Type: models.UserGroupSub,
+		},
+		Origin: models.Dashboard,
+	})
 	go proLogic.UpdatesUserGwAccessOnGrpUpdates(userG.NetworkRoles, make(map[models.NetworkID]map[models.UserRoleID]struct{}))
 	logic.ReturnSuccessResponseWithJson(w, r, nil, "deleted user group")
 }
@@ -636,6 +715,21 @@ func createRole(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Create,
+		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:   userRole.ID.String(),
+			Name: userRole.Name,
+			Type: models.UserRoleSub,
+		},
+		Origin: models.ClientApp,
+	})
 	logic.ReturnSuccessResponseWithJson(w, r, userRole, "created user role")
 }
 
@@ -670,6 +764,25 @@ func updateRole(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+	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:   userRole.ID.String(),
+			Name: userRole.Name,
+			Type: models.UserRoleSub,
+		},
+		Diff: models.Diff{
+			Old: currRole,
+			New: userRole,
+		},
+		Origin: models.Dashboard,
+	})
 	// reset configs for service user
 	go proLogic.UpdatesUserGwAccessOnRoleUpdates(currRole.NetworkLevelAccess, userRole.NetworkLevelAccess, string(userRole.NetworkID))
 	logic.ReturnSuccessResponseWithJson(w, r, userRole, "updated user role")
@@ -698,6 +811,21 @@ func deleteRole(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		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:   role.ID.String(),
+			Name: role.Name,
+			Type: models.UserRoleSub,
+		},
+		Origin: models.Dashboard,
+	})
 	go proLogic.UpdatesUserGwAccessOnRoleUpdates(role.NetworkLevelAccess, make(map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope), role.NetworkID.String())
 	logic.ReturnSuccessResponseWithJson(w, r, nil, "deleted user role")
 }
@@ -947,7 +1075,7 @@ func getUserRemoteAccessNetworkGateways(w http.ResponseWriter, r *http.Request)
 			GwID:              node.ID.String(),
 			GWName:            host.Name,
 			Network:           node.Network,
-			IsInternetGateway: node.IsInternetGateway,
+			IsInternetGateway: node.EgressDetails.IsInternetGateway,
 			Metadata:          node.Metadata,
 		})
 
@@ -1074,7 +1202,7 @@ func getRemoteAccessGatewayConf(w http.ResponseWriter, r *http.Request) {
 		Network:           node.Network,
 		GwClient:          userConf,
 		Connected:         true,
-		IsInternetGateway: node.IsInternetGateway,
+		IsInternetGateway: node.EgressDetails.IsInternetGateway,
 		GwPeerPublicKey:   host.PublicKey.String(),
 		GwListenPort:      logic.GetPeerListenPort(host),
 		Metadata:          node.Metadata,
@@ -1166,7 +1294,7 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 				Network:           node.Network,
 				GwClient:          extClient,
 				Connected:         true,
-				IsInternetGateway: node.IsInternetGateway,
+				IsInternetGateway: node.EgressDetails.IsInternetGateway,
 				GwPeerPublicKey:   host.PublicKey.String(),
 				GwListenPort:      logic.GetPeerListenPort(host),
 				Metadata:          node.Metadata,
@@ -1210,7 +1338,7 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 			GwID:              node.ID.String(),
 			GWName:            host.Name,
 			Network:           node.Network,
-			IsInternetGateway: node.IsInternetGateway,
+			IsInternetGateway: node.EgressDetails.IsInternetGateway,
 			GwPeerPublicKey:   host.PublicKey.String(),
 			GwListenPort:      logic.GetPeerListenPort(host),
 			Metadata:          node.Metadata,
@@ -1356,6 +1484,21 @@ func approvePendingUser(w http.ResponseWriter, r *http.Request) {
 			break
 		}
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Create,
+		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:   username,
+			Name: username,
+			Type: models.PendingUserSub,
+		},
+		Origin: models.Dashboard,
+	})
 	logic.ReturnSuccessResponse(w, r, "approved "+username)
 }
 
@@ -1387,6 +1530,21 @@ func deletePendingUser(w http.ResponseWriter, r *http.Request) {
 			break
 		}
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		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:   username,
+			Name: username,
+			Type: models.PendingUserSub,
+		},
+		Origin: models.Dashboard,
+	})
 	logic.ReturnSuccessResponse(w, r, "deleted pending "+username)
 }
 
@@ -1402,6 +1560,21 @@ func deleteAllPendingUsers(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to delete all pending users "+err.Error()), "internal"))
 		return
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.DeleteAll,
+		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:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.PendingUserSub,
+		},
+		Origin: models.Dashboard,
+	})
 	logic.ReturnSuccessResponse(w, r, "cleared all pending users")
 }
 

+ 4 - 0
pro/initialize.go

@@ -34,6 +34,7 @@ func InitPro() {
 		proControllers.FailOverHandlers,
 		proControllers.InetHandlers,
 		proControllers.RacHandlers,
+		proControllers.EventHandlers,
 	)
 	controller.ListRoles = proControllers.ListRoles
 	logic.EnterpriseCheckFuncs = append(logic.EnterpriseCheckFuncs, func() {
@@ -94,6 +95,7 @@ func InitPro() {
 		proLogic.InitFailOverCache()
 		auth.StartSyncHook()
 		email.Init()
+		proLogic.EventWatcher()
 	})
 	logic.ResetFailOver = proLogic.ResetFailOver
 	logic.ResetFailedOverPeer = proLogic.ResetFailedOverPeer
@@ -111,6 +113,7 @@ func InitPro() {
 	logic.DeleteMetrics = proLogic.DeleteMetrics
 	logic.GetTrialEndDate = getTrialEndDate
 	logic.SetDefaultGw = proLogic.SetDefaultGw
+	logic.ValidateInetGwReq = proLogic.ValidateInetGwReq
 	logic.SetDefaultGwForRelayedUpdate = proLogic.SetDefaultGwForRelayedUpdate
 	logic.UnsetInternetGw = proLogic.UnsetInternetGw
 	logic.SetInternetGw = proLogic.SetInternetGw
@@ -142,6 +145,7 @@ func InitPro() {
 	logic.ResetAuthProvider = auth.ResetAuthProvider
 	logic.ResetIDPSyncHook = auth.ResetIDPSyncHook
 	logic.EmailInit = email.Init
+	logic.LogEvent = proLogic.LogEvent
 }
 
 func retrieveProLogo() string {

+ 47 - 0
pro/logic/events.go

@@ -0,0 +1,47 @@
+package logic
+
+import (
+	"context"
+	"encoding/json"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/uuid"
+	"github.com/gravitl/netmaker/db"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/schema"
+)
+
+var EventActivityCh = make(chan models.Event, 100)
+
+func LogEvent(a *models.Event) {
+	EventActivityCh <- *a
+}
+
+func EventWatcher() {
+
+	for e := range EventActivityCh {
+		if e.Action == models.Update {
+			// check if diff
+			if cmp.Equal(e.Diff.Old, e.Diff.New) {
+				continue
+			}
+		}
+		sourceJson, _ := json.Marshal(e.Source)
+		dstJson, _ := json.Marshal(e.Target)
+		diff, _ := json.Marshal(e.Diff)
+		a := schema.Event{
+			ID:          uuid.New().String(),
+			Action:      e.Action,
+			Source:      sourceJson,
+			Target:      dstJson,
+			Origin:      e.Origin,
+			NetworkID:   e.NetworkID,
+			TriggeredBy: e.TriggeredBy,
+			Diff:        diff,
+			TimeStamp:   time.Now().UTC(),
+		}
+		a.Create(db.WithContext(context.TODO()))
+	}
+
+}

+ 3 - 2
pro/logic/failover.go

@@ -165,6 +165,7 @@ func GetFailOverPeerIps(peer, node *models.Node) []net.IPNet {
 	for failOverpeerID := range node.FailOverPeers {
 		failOverpeer, err := logic.GetNodeByID(failOverpeerID)
 		if err == nil && failOverpeer.FailedOverBy == peer.ID {
+			logic.GetNodeEgressInfo(&failOverpeer)
 			if failOverpeer.Address.IP != nil {
 				allowed := net.IPNet{
 					IP:   failOverpeer.Address.IP,
@@ -179,7 +180,7 @@ func GetFailOverPeerIps(peer, node *models.Node) []net.IPNet {
 				}
 				allowedips = append(allowedips, allowed)
 			}
-			if failOverpeer.IsEgressGateway {
+			if failOverpeer.EgressDetails.IsEgressGateway {
 				allowedips = append(allowedips, logic.GetEgressIPs(&failOverpeer)...)
 			}
 			if failOverpeer.IsRelay {
@@ -199,7 +200,7 @@ func GetFailOverPeerIps(peer, node *models.Node) []net.IPNet {
 						}
 						allowedips = append(allowedips, allowed)
 					}
-					if rNode.IsEgressGateway {
+					if rNode.EgressDetails.IsEgressGateway {
 						allowedips = append(allowedips, logic.GetEgressIPs(&rNode)...)
 					}
 				}

+ 21 - 15
pro/logic/nodes.go

@@ -24,7 +24,7 @@ func ValidateInetGwReq(inetNode models.Node, req models.InetNodeReq, update bool
 	if inetHost.FirewallInUse == models.FIREWALL_NONE {
 		return errors.New("iptables or nftables needs to be installed")
 	}
-	if inetNode.InternetGwID != "" {
+	if inetNode.EgressDetails.InternetGwID != "" {
 		return fmt.Errorf("node %s is using a internet gateway already", inetHost.Name)
 	}
 	if inetNode.IsRelayed {
@@ -36,22 +36,28 @@ func ValidateInetGwReq(inetNode models.Node, req models.InetNodeReq, update bool
 		if err != nil {
 			return err
 		}
+		if clientNode.IsFailOver {
+			return errors.New("failover node cannot be set to use internet gateway")
+		}
 		clientHost, err := logic.GetHost(clientNode.HostID.String())
 		if err != nil {
 			return err
 		}
+		if clientHost.IsDefault {
+			return errors.New("default host cannot be set to use internet gateway")
+		}
 		if clientHost.OS != models.OS_Types.Linux && clientHost.OS != models.OS_Types.Windows {
 			return errors.New("can only attach linux or windows machine to a internet gateway")
 		}
-		if clientNode.IsInternetGateway {
+		if clientNode.EgressDetails.IsInternetGateway {
 			return fmt.Errorf("node %s acting as internet gateway cannot use another internet gateway", clientHost.Name)
 		}
 		if update {
-			if clientNode.InternetGwID != "" && clientNode.InternetGwID != inetNode.ID.String() {
+			if clientNode.EgressDetails.InternetGwID != "" && clientNode.EgressDetails.InternetGwID != inetNode.ID.String() {
 				return fmt.Errorf("node %s is already using a internet gateway", clientHost.Name)
 			}
 		} else {
-			if clientNode.InternetGwID != "" {
+			if clientNode.EgressDetails.InternetGwID != "" {
 				return fmt.Errorf("node %s is already using a internet gateway", clientHost.Name)
 			}
 		}
@@ -68,7 +74,7 @@ func ValidateInetGwReq(inetNode models.Node, req models.InetNodeReq, update bool
 			if err != nil {
 				continue
 			}
-			if node.InternetGwID != "" && node.InternetGwID != inetNode.ID.String() {
+			if node.EgressDetails.InternetGwID != "" && node.EgressDetails.InternetGwID != inetNode.ID.String() {
 				return errors.New("nodes on same host cannot use different internet gateway")
 			}
 
@@ -79,14 +85,14 @@ func ValidateInetGwReq(inetNode models.Node, req models.InetNodeReq, update bool
 
 // SetInternetGw - sets the node as internet gw based on flag bool
 func SetInternetGw(node *models.Node, req models.InetNodeReq) {
-	node.IsInternetGateway = true
-	node.InetNodeReq = req
+	node.EgressDetails.IsInternetGateway = true
+	node.EgressDetails.InetNodeReq = req
 	for _, clientNodeID := range req.InetNodeClientIDs {
 		clientNode, err := logic.GetNodeByID(clientNodeID)
 		if err != nil {
 			continue
 		}
-		clientNode.InternetGwID = node.ID.String()
+		clientNode.EgressDetails.InternetGwID = node.ID.String()
 		logic.UpsertNode(&clientNode)
 	}
 
@@ -99,19 +105,19 @@ func UnsetInternetGw(node *models.Node) {
 		return
 	}
 	for _, clientNode := range nodes {
-		if node.ID.String() == clientNode.InternetGwID {
-			clientNode.InternetGwID = ""
+		if node.ID.String() == clientNode.EgressDetails.InternetGwID {
+			clientNode.EgressDetails.InternetGwID = ""
 			logic.UpsertNode(&clientNode)
 		}
 
 	}
-	node.IsInternetGateway = false
-	node.InetNodeReq = models.InetNodeReq{}
+	node.EgressDetails.IsInternetGateway = false
+	node.EgressDetails.InetNodeReq = models.InetNodeReq{}
 
 }
 
 func SetDefaultGwForRelayedUpdate(relayed, relay models.Node, peerUpdate models.HostPeerUpdate) models.HostPeerUpdate {
-	if relay.InternetGwID != "" {
+	if relay.EgressDetails.InternetGwID != "" {
 		relayedHost, err := logic.GetHost(relayed.HostID.String())
 		if err != nil {
 			return peerUpdate
@@ -127,9 +133,9 @@ func SetDefaultGwForRelayedUpdate(relayed, relay models.Node, peerUpdate models.
 }
 
 func SetDefaultGw(node models.Node, peerUpdate models.HostPeerUpdate) models.HostPeerUpdate {
-	if node.InternetGwID != "" {
+	if node.EgressDetails.InternetGwID != "" {
 
-		inetNode, err := logic.GetNodeByID(node.InternetGwID)
+		inetNode, err := logic.GetNodeByID(node.EgressDetails.InternetGwID)
 		if err != nil {
 			return peerUpdate
 		}

+ 2 - 20
pro/logic/user_mgmt.go

@@ -8,7 +8,6 @@ import (
 	"time"
 
 	"github.com/gravitl/netmaker/database"
-	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
@@ -691,30 +690,13 @@ func GetUserRAGNodesV1(user models.User) (gws map[string]models.Node) {
 
 func GetUserRAGNodes(user models.User) (gws map[string]models.Node) {
 	gws = make(map[string]models.Node)
-	userGwAccessScope := GetUserNetworkRolesWithRemoteVPNAccess(user)
-	logger.Log(3, fmt.Sprintf("User Gw Access Scope: %+v", userGwAccessScope))
-	_, allNetAccess := userGwAccessScope["*"]
 	nodes, err := logic.GetAllNodes()
 	if err != nil {
 		return
 	}
 	for _, node := range nodes {
-		if node.IsIngressGateway && !node.PendingDelete {
-			if allNetAccess {
-				gws[node.ID.String()] = node
-			} else {
-				gwRsrcMap := userGwAccessScope[models.NetworkID(node.Network)]
-				scope, ok := gwRsrcMap[models.AllRemoteAccessGwRsrcID]
-				if !ok {
-					if scope, ok = gwRsrcMap[models.RsrcID(node.ID.String())]; !ok {
-						continue
-					}
-				}
-				if scope.VPNaccess {
-					gws[node.ID.String()] = node
-				}
-
-			}
+		if ok, _ := logic.IsUserAllowedToCommunicate(user.UserName, node); ok {
+			gws[node.ID.String()] = node
 		}
 	}
 	return

+ 4 - 0
schema/activity.go

@@ -0,0 +1,4 @@
+package schema
+
+type Activity struct {
+}

+ 70 - 0
schema/egress.go

@@ -0,0 +1,70 @@
+package schema
+
+import (
+	"context"
+	"time"
+
+	"github.com/gravitl/netmaker/db"
+	"gorm.io/datatypes"
+)
+
+const egressTable = "egresses"
+
+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"`
+	IsInetGw    bool              `gorm:"is_inet_gw" json:"is_internet_gateway"`
+	Status      bool              `gorm:"status" json:"status"`
+	CreatedBy   string            `gorm:"created_by" json:"created_by"`
+	CreatedAt   time.Time         `gorm:"created_at" json:"created_at"`
+	UpdatedAt   time.Time         `gorm:"updated_at" json:"updated_at"`
+}
+
+func (e *Egress) Table() string {
+	return egressTable
+}
+
+func (e *Egress) Get(ctx context.Context) error {
+	return db.FromContext(ctx).Table(e.Table()).First(&e).Where("id = ?", e.ID).Error
+}
+
+func (e *Egress) Update(ctx context.Context) error {
+	return db.FromContext(ctx).Table(e.Table()).Where("id = ?", e.ID).Updates(&e).Error
+}
+
+func (e *Egress) UpdateNatStatus(ctx context.Context) error {
+	return db.FromContext(ctx).Table(e.Table()).Where("id = ?", e.ID).Updates(map[string]any{
+		"nat": e.Nat,
+	}).Error
+}
+
+func (e *Egress) UpdateINetGwStatus(ctx context.Context) error {
+	return db.FromContext(ctx).Table(e.Table()).Where("id = ?", e.ID).Updates(map[string]any{
+		"is_inet_gw": e.IsInetGw,
+	}).Error
+}
+
+func (e *Egress) UpdateEgressStatus(ctx context.Context) error {
+	return db.FromContext(ctx).Table(e.Table()).Where("id = ?", e.ID).Updates(map[string]any{
+		"status": e.Status,
+	}).Error
+}
+
+func (e *Egress) Create(ctx context.Context) error {
+	return db.FromContext(ctx).Table(e.Table()).Create(&e).Error
+}
+
+func (e *Egress) ListByNetwork(ctx context.Context) (egs []Egress, err error) {
+	err = db.FromContext(ctx).Table(e.Table()).Where("network = ?", e.Network).Find(&egs).Error
+	return
+}
+
+func (e *Egress) Delete(ctx context.Context) error {
+	return db.FromContext(ctx).Table(e.Table()).Where("id = ?", e.ID).Delete(&e).Error
+}

+ 55 - 0
schema/event.go

@@ -0,0 +1,55 @@
+package schema
+
+import (
+	"context"
+	"time"
+
+	"github.com/gravitl/netmaker/db"
+	"github.com/gravitl/netmaker/models"
+	"gorm.io/datatypes"
+)
+
+type Event struct {
+	ID          string           `gorm:"primaryKey" json:"id"`
+	Action      models.Action    `gorm:"action" json:"action"`
+	Source      datatypes.JSON   `gorm:"source" json:"source"`
+	Origin      models.Origin    `gorm:"origin" json:"origin"`
+	Target      datatypes.JSON   `gorm:"target" json:"target"`
+	NetworkID   models.NetworkID `gorm:"network_id" json:"network_id"`
+	TriggeredBy string           `gorm:"triggered_by" json:"triggered_by"`
+	Diff        datatypes.JSON   `gorm:"diff" json:"diff"`
+	TimeStamp   time.Time        `gorm:"time_stamp" json:"time_stamp"`
+}
+
+func (a *Event) Get(ctx context.Context) error {
+	return db.FromContext(ctx).Model(&Event{}).First(&a).Where("id = ?", a.ID).Error
+}
+
+func (a *Event) Update(ctx context.Context) error {
+	return db.FromContext(ctx).Model(&Event{}).Where("id = ?", a.ID).Updates(&a).Error
+}
+
+func (a *Event) Create(ctx context.Context) error {
+	return db.FromContext(ctx).Model(&Event{}).Create(&a).Error
+}
+
+func (a *Event) ListByNetwork(ctx context.Context) (ats []Event, err error) {
+	err = db.FromContext(ctx).Model(&Event{}).Where("network_id = ?", a.NetworkID).Order("time_stamp DESC").Find(&ats).Error
+	return
+}
+
+func (a *Event) ListByUser(ctx context.Context) (ats []Event, err error) {
+	err = db.FromContext(ctx).Model(&Event{}).Where("triggered_by = ?", a.TriggeredBy).Order("time_stamp DESC").Find(&ats).Error
+	return
+}
+
+func (a *Event) ListByUserAndNetwork(ctx context.Context) (ats []Event, err error) {
+	err = db.FromContext(ctx).Model(&Event{}).Where("network_id = ? AND triggered_by = ?",
+		a.NetworkID, a.TriggeredBy).Order("time_stamp DESC").Find(&ats).Error
+	return
+}
+
+func (a *Event) List(ctx context.Context) (ats []Event, err error) {
+	err = db.FromContext(ctx).Model(&Event{}).Order("time_stamp DESC").Find(&ats).Error
+	return
+}

+ 2 - 0
schema/models.go

@@ -4,6 +4,8 @@ package schema
 func ListModels() []interface{} {
 	return []interface{}{
 		&Job{},
+		&Egress{},
 		&UserAccessToken{},
+		&Event{},
 	}
 }