Browse Source

Merge branch 'develop' into NET-1991

# Conflicts:
#	go.mod
#	go.sum
#	pro/controllers/users.go
Vishal Dalwadi 3 months ago
parent
commit
16a9dca774
67 changed files with 3053 additions and 492 deletions
  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
 					return
 				}
 				}
 			}
 			}
-			logic.CheckHostPorts(&result.Host)
+			_ = logic.CheckHostPorts(&result.Host)
 			if err := logic.CreateHost(&result.Host); err != nil {
 			if err := logic.CreateHost(&result.Host); err != nil {
 				handleHostRegErr(conn, err)
 				handleHostRegErr(conn, err)
 				return
 				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
 	sso       bool
 	tenantId  string
 	tenantId  string
 	saas      bool
 	saas      bool
+	authToken string
 )
 )
 
 
 var contextSetCmd = &cobra.Command{
 var contextSetCmd = &cobra.Command{
@@ -30,13 +31,14 @@ var contextSetCmd = &cobra.Command{
 			Username:  username,
 			Username:  username,
 			Password:  password,
 			Password:  password,
 			MasterKey: masterKey,
 			MasterKey: masterKey,
+			AuthToken: authToken,
 			SSO:       sso,
 			SSO:       sso,
 			TenantId:  tenantId,
 			TenantId:  tenantId,
 			Saas:      saas,
 			Saas:      saas,
 		}
 		}
 		if !ctx.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()
 				cmd.Usage()
 			}
 			}
 			if ctx.Endpoint == "" {
 			if ctx.Endpoint == "" {
@@ -49,8 +51,8 @@ var contextSetCmd = &cobra.Command{
 				cmd.Usage()
 				cmd.Usage()
 			}
 			}
 			ctx.Endpoint = fmt.Sprintf(functions.TenantUrlTemplate, tenantId)
 			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()
 				cmd.Usage()
 			}
 			}
 		}
 		}
@@ -62,6 +64,7 @@ func init() {
 	contextSetCmd.Flags().StringVar(&endpoint, "endpoint", "", "Endpoint of the API Server")
 	contextSetCmd.Flags().StringVar(&endpoint, "endpoint", "", "Endpoint of the API Server")
 	contextSetCmd.Flags().StringVar(&username, "username", "", "Username")
 	contextSetCmd.Flags().StringVar(&username, "username", "", "Username")
 	contextSetCmd.Flags().StringVar(&password, "password", "", "Password")
 	contextSetCmd.Flags().StringVar(&password, "password", "", "Password")
+	contextSetCmd.Flags().StringVar(&authToken, "auth_token", "", "Auth Token")
 	contextSetCmd.MarkFlagsRequiredTogether("username", "password")
 	contextSetCmd.MarkFlagsRequiredTogether("username", "password")
 	contextSetCmd.Flags().BoolVar(&sso, "sso", false, "Login via Single Sign On (SSO)?")
 	contextSetCmd.Flags().BoolVar(&sso, "sso", false, "Login via Single Sign On (SSO)?")
 	contextSetCmd.Flags().StringVar(&masterKey, "master_key", "", "Master Key")
 	contextSetCmd.Flags().StringVar(&masterKey, "master_key", "", "Master Key")

+ 4 - 1
cli/cmd/root.go

@@ -1,9 +1,9 @@
 package cmd
 package cmd
 
 
 import (
 import (
-	"github.com/gravitl/netmaker/cli/cmd/gateway"
 	"os"
 	"os"
 
 
+	"github.com/gravitl/netmaker/cli/cmd/access_token"
 	"github.com/gravitl/netmaker/cli/cmd/acl"
 	"github.com/gravitl/netmaker/cli/cmd/acl"
 	"github.com/gravitl/netmaker/cli/cmd/commons"
 	"github.com/gravitl/netmaker/cli/cmd/commons"
 	"github.com/gravitl/netmaker/cli/cmd/context"
 	"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/enrollment_key"
 	"github.com/gravitl/netmaker/cli/cmd/ext_client"
 	"github.com/gravitl/netmaker/cli/cmd/ext_client"
 	"github.com/gravitl/netmaker/cli/cmd/failover"
 	"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/host"
 	"github.com/gravitl/netmaker/cli/cmd/metrics"
 	"github.com/gravitl/netmaker/cli/cmd/metrics"
 	"github.com/gravitl/netmaker/cli/cmd/network"
 	"github.com/gravitl/netmaker/cli/cmd/network"
 	"github.com/gravitl/netmaker/cli/cmd/node"
 	"github.com/gravitl/netmaker/cli/cmd/node"
 	"github.com/gravitl/netmaker/cli/cmd/server"
 	"github.com/gravitl/netmaker/cli/cmd/server"
 	"github.com/gravitl/netmaker/cli/cmd/user"
 	"github.com/gravitl/netmaker/cli/cmd/user"
+
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 )
 )
 
 
@@ -57,4 +59,5 @@ func init() {
 	rootCmd.AddCommand(enrollment_key.GetRoot())
 	rootCmd.AddCommand(enrollment_key.GetRoot())
 	rootCmd.AddCommand(failover.GetRoot())
 	rootCmd.AddCommand(failover.GetRoot())
 	rootCmd.AddCommand(gateway.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)
 	body := new(T)
 	if len(resBodyBytes) > 0 {
 	if len(resBodyBytes) > 0 {
 		if err := json.Unmarshal(resBodyBytes, body); err != nil {
 		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
 	return body

+ 58 - 5
controllers/acls.go

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

+ 1 - 0
controllers/controller.go

@@ -39,6 +39,7 @@ var HttpHandlers = []interface{}{
 	enrollmentKeyHandlers,
 	enrollmentKeyHandlers,
 	tagHandlers,
 	tagHandlers,
 	aclHandlers,
 	aclHandlers,
+	egressHandlers,
 	legacyHandlers,
 	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) {
 func deleteEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 	params := mux.Vars(r)
 	params := mux.Vars(r)
 	keyID := params["keyID"]
 	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 {
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "failed to remove enrollment key: ", err.Error())
 		logger.Log(0, r.Header.Get("user"), "failed to remove enrollment key: ", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		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)
 	logger.Log(2, r.Header.Get("user"), "deleted enrollment key", keyID)
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 }
 }
@@ -173,6 +193,21 @@ func createEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		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")
 	logger.Log(2, r.Header.Get("user"), "created enrollment key")
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(newEnrollmentKey)
 	json.NewEncoder(w).Encode(newEnrollmentKey)
@@ -208,6 +243,7 @@ func updateEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 			return
 			return
 		}
 		}
 	}
 	}
+	currKey, _ := logic.GetEnrollmentKey(keyId)
 
 
 	newEnrollmentKey, err := logic.UpdateEnrollmentKey(keyId, relayId, enrollmentKeyBody.Groups)
 	newEnrollmentKey, err := logic.UpdateEnrollmentKey(keyId, relayId, enrollmentKeyBody.Groups)
 	if err != nil {
 	if err != nil {
@@ -221,7 +257,25 @@ func updateEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		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)
 	slog.Info("updated enrollment key", "id", keyId)
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(newEnrollmentKey)
 	json.NewEncoder(w).Encode(newEnrollmentKey)
@@ -302,7 +356,7 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 	if !hostExists {
 	if !hostExists {
 		newHost.PersistentKeepalive = models.DefaultPersistentKeepAlive
 		newHost.PersistentKeepalive = models.DefaultPersistentKeepAlive
 		// register host
 		// register host
-		logic.CheckHostPorts(&newHost)
+		_ = logic.CheckHostPorts(&newHost)
 		// create EMQX credentials and ACLs for host
 		// create EMQX credentials and ACLs for host
 		if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
 		if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
 			if err := mq.GetEmqxHandler().CreateEmqxUser(newHost.ID.String(), newHost.HostPass); err != nil {
 			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,
 		ServerConf:    server,
 		RequestedHost: newHost,
 		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")
 	logger.Log(0, newHost.Name, newHost.ID.String(), "registered with Netmaker")
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(&response)
 	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"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
+	logic.GetNodeEgressInfo(&gwnode)
 	host, err := logic.GetHost(gwnode.HostID.String())
 	host, err := logic.GetHost(gwnode.HostID.String())
 	if err != nil {
 	if err != nil {
 		logger.Log(
 		logger.Log(
@@ -261,7 +262,7 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 
 
 	var newAllowedIPs string
 	var newAllowedIPs string
-	if logic.IsInternetGw(gwnode) || gwnode.InternetGwID != "" {
+	if logic.IsInternetGw(gwnode) || gwnode.EgressDetails.InternetGwID != "" {
 		egressrange := "0.0.0.0/0"
 		egressrange := "0.0.0.0/0"
 		if gwnode.Address6.IP != nil && client.Address6 != "" {
 		if gwnode.Address6.IP != nil && client.Address6 != "" {
 			egressrange += "," + "::/0"
 			egressrange += "," + "::/0"
@@ -540,7 +541,7 @@ func getExtClientHAConf(w http.ResponseWriter, r *http.Request) {
 		keepalive = "PersistentKeepalive = " + strconv.Itoa(int(gwnode.IngressPersistentKeepalive))
 		keepalive = "PersistentKeepalive = " + strconv.Itoa(int(gwnode.IngressPersistentKeepalive))
 	}
 	}
 	var newAllowedIPs string
 	var newAllowedIPs string
-	if logic.IsInternetGw(gwnode) || gwnode.InternetGwID != "" {
+	if logic.IsInternetGw(gwnode) || gwnode.EgressDetails.InternetGwID != "" {
 		egressrange := "0.0.0.0/0"
 		egressrange := "0.0.0.0/0"
 		if gwnode.Address6.IP != nil && client.Address6 != "" {
 		if gwnode.Address6.IP != nil && client.Address6 != "" {
 			egressrange += "," + "::/0"
 			egressrange += "," + "::/0"
@@ -688,7 +689,7 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 	var gateway models.EgressGatewayRequest
 	var gateway models.EgressGatewayRequest
 	gateway.NetID = params["network"]
 	gateway.NetID = params["network"]
 	gateway.Ranges = customExtClient.ExtraAllowedIPs
 	gateway.Ranges = customExtClient.ExtraAllowedIPs
-	err := logic.ValidateEgressRange(gateway)
+	err := logic.ValidateEgressRange(gateway.NetID, gateway.Ranges)
 	if err != nil {
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "error validating egress range: ", err.Error())
 		logger.Log(0, r.Header.Get("user"), "error validating egress range: ", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
@@ -798,6 +799,27 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 		"clientid",
 		"clientid",
 		extclient.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)
 	w.WriteHeader(http.StatusOK)
 	go func() {
 	go func() {
 		if err := logic.SetClientDefaultACLs(&extclient); err != nil {
 		if err := logic.SetClientDefaultACLs(&extclient); err != nil {
@@ -876,7 +898,7 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 	var gateway models.EgressGatewayRequest
 	var gateway models.EgressGatewayRequest
 	gateway.NetID = params["network"]
 	gateway.NetID = params["network"]
 	gateway.Ranges = update.ExtraAllowedIPs
 	gateway.Ranges = update.ExtraAllowedIPs
-	err = logic.ValidateEgressRange(gateway)
+	err = logic.ValidateEgressRange(gateway.NetID, gateway.Ranges)
 	if err != nil {
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "error validating egress range: ", err.Error())
 		logger.Log(0, r.Header.Get("user"), "error validating egress range: ", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		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"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 		return
 	}
 	}
+	host, err := logic.GetHost(node.HostID.String())
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
 	var req models.CreateGwReq
 	var req models.CreateGwReq
 	err = json.NewDecoder(r.Body).Decode(&req)
 	err = json.NewDecoder(r.Body).Decode(&req)
 	if err != nil {
 	if err != nil {
@@ -89,7 +94,21 @@ func createGateway(w http.ResponseWriter, r *http.Request) {
 	)
 	)
 	logic.GetNodeStatus(&relayNode, false)
 	logic.GetNodeStatus(&relayNode, false)
 	apiNode := relayNode.ConvertToAPINode()
 	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)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(apiNode)
 	json.NewEncoder(w).Encode(apiNode)
 	go func() {
 	go func() {
@@ -138,6 +157,11 @@ func deleteGateway(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
+	host, err := logic.GetHost(node.HostID.String())
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
 	node.IsGw = false
 	node.IsGw = false
 	logic.UpsertNode(&node)
 	logic.UpsertNode(&node)
 	logger.Log(1, r.Header.Get("user"), "deleted gw", nodeid, "on network", netid)
 	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)
 	logic.GetNodeStatus(&node, false)
 	apiNode := node.ConvertToAPINode()
 	apiNode := node.ConvertToAPINode()
 	logger.Log(1, r.Header.Get("user"), "deleted ingress gateway", nodeid)
 	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"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(keyErr, "internal"))
 		return
 		return
 	}
 	}
-
+	_ = logic.CheckHostPorts(host)
 	serverConf.TrafficKey = key
 	serverConf.TrafficKey = key
 	response := models.HostPull{
 	response := models.HostPull{
 		Host:              *host,
 		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()
 	apiHostData := newHost.ConvertNMHostToAPI()
 	logger.Log(2, r.Header.Get("user"), "updated host", newHost.ID.String())
 	logger.Log(2, r.Header.Get("user"), "updated host", newHost.ID.String())
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
@@ -420,7 +438,21 @@ func deleteHost(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
-
+	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()
 	apiHostData := currHost.ConvertNMHostToAPI()
 	logger.Log(2, r.Header.Get("user"), "removed host", currHost.Name)
 	logger.Log(2, r.Header.Get("user"), "removed host", currHost.Name)
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
@@ -492,6 +524,22 @@ func addHostToNetwork(w http.ResponseWriter, r *http.Request) {
 		r.Header.Get("user"),
 		r.Header.Get("user"),
 		fmt.Sprintf("added host %s to network %s", currHost.Name, network),
 		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)
 	w.WriteHeader(http.StatusOK)
 }
 }
 
 
@@ -623,6 +671,22 @@ func deleteHostFromNetwork(w http.ResponseWriter, r *http.Request) {
 			logic.SetDNS()
 			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(
 	logger.Log(
 		2,
 		2,
 		r.Header.Get("user"),
 		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)
 			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())
 	slog.Info("requested host pull", "user", r.Header.Get("user"), "host", host.ID.String())
 	w.WriteHeader(http.StatusOK)
 	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.IsRelay = false
 	node.RelayedNodes = []string{}
 	node.RelayedNodes = []string{}
 	node.DNSOn = models.ParseBool(legacy.DNSOn)
 	node.DNSOn = models.ParseBool(legacy.DNSOn)
-	node.LastModified = time.Now()
+	node.LastModified = time.Now().UTC()
 	node.ExpirationDateTime = time.Unix(legacy.ExpirationDateTime, 0)
 	node.ExpirationDateTime = time.Unix(legacy.ExpirationDateTime, 0)
 	node.EgressGatewayNatEnabled = models.ParseBool(legacy.EgressGatewayNatEnabled)
 	node.EgressGatewayNatEnabled = models.ParseBool(legacy.EgressGatewayNatEnabled)
 	node.EgressGatewayRequest = legacy.EgressGatewayRequest
 	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)
 	err = logic.DeleteNetwork(network, force, doneCh)
 	if err != nil {
 	if err != nil {
-		errtype := "badrequest"
+		errtype := logic.BadReq
 		if strings.Contains(err.Error(), "Node check failed") {
 		if strings.Contains(err.Error(), "Node check failed") {
-			errtype = "forbidden"
+			errtype = logic.Forbidden
 		}
 		}
 		logger.Log(0, r.Header.Get("user"),
 		logger.Log(0, r.Header.Get("user"),
 			fmt.Sprintf("failed to delete network [%s]: %v", network, err))
 			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.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)
 	logger.Log(1, r.Header.Get("user"), "deleted network", network)
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode("success")
 	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")
 			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)
 	logger.Log(1, r.Header.Get("user"), "created network", network.NetID)
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(network)
 	json.NewEncoder(w).Encode(network)

+ 22 - 10
controllers/node.go

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

+ 0 - 94
controllers/node_test.go

@@ -18,100 +18,6 @@ import (
 var nonLinuxHost models.Host
 var nonLinuxHost models.Host
 var linuxHost 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) {
 func TestGetNetworkNodes(t *testing.T) {
 	deleteAllNetworks()
 	deleteAllNetworks()
 	createNet()
 	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"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to udpate server settings "+err.Error()), "internal"))
 		return
 		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")
 	go reInit(currSettings, req, force == "true")
 	logic.ReturnSuccessResponseWithJson(w, r, req, "updated server settings successfully")
 	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,
 		Network:   req.Network,
 		CreatedBy: user.UserName,
 		CreatedBy: user.UserName,
 		ColorCode: req.ColorCode,
 		ColorCode: req.ColorCode,
-		CreatedAt: time.Now(),
+		CreatedAt: time.Now().UTC(),
 	}
 	}
 	_, err = logic.GetTag(tag.ID)
 	_, err = logic.GetTag(tag.ID)
 	if err == nil {
 	if err == nil {
@@ -131,6 +131,22 @@ func createTag(w http.ResponseWriter, r *http.Request) {
 			logic.UpsertNode(&node)
 			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)
 	go mq.PublishPeerUpdate(false)
 
 
 	var res models.TagListRespNodes = models.TagListRespNodes{
 	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"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 		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)
 	updateTag.NewName = strings.TrimSpace(updateTag.NewName)
 	var newID models.TagID
 	var newID models.TagID
 	if updateTag.NewName != "" {
 	if updateTag.NewName != "" {
@@ -198,7 +233,8 @@ func updateTag(w http.ResponseWriter, r *http.Request) {
 		}
 		}
 		mq.PublishPeerUpdate(false)
 		mq.PublishPeerUpdate(false)
 	}()
 	}()
-
+	e.Diff.New = updateTag
+	logic.LogEvent(e)
 	var res models.TagListRespNodes = models.TagListRespNodes{
 	var res models.TagListRespNodes = models.TagListRespNodes{
 		Tag:         tag,
 		Tag:         tag,
 		UsedByCnt:   len(updateTag.TaggedNodes),
 		UsedByCnt:   len(updateTag.TaggedNodes),
@@ -241,5 +277,21 @@ func deleteTag(w http.ResponseWriter, r *http.Request) {
 		logic.RemoveTagFromEnrollmentKeys(tag.ID)
 		logic.RemoveTagFromEnrollmentKeys(tag.ID)
 		mq.PublishPeerUpdate(false)
 		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)
 	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(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(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/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
 // @Summary     Authenticate a user to retrieve an authorization token
@@ -66,25 +67,25 @@ func createUserAccessToken(w http.ResponseWriter, r *http.Request) {
 	if err != nil {
 	if err != nil {
 		logger.Log(0, "error decoding request body: ",
 		logger.Log(0, "error decoding request body: ",
 			err.Error())
 			err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
 		return
 		return
 	}
 	}
 	if req.Name == "" {
 	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
 		return
 	}
 	}
 	if req.UserName == "" {
 	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
 		return
 	}
 	}
 	caller, err := logic.GetUser(r.Header.Get("user"))
 	caller, err := logic.GetUser(r.Header.Get("user"))
 	if err != nil {
 	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.UnAuthorized))
 		return
 		return
 	}
 	}
 	user, err := logic.GetUser(req.UserName)
 	user, err := logic.GetUser(req.UserName)
 	if err != nil {
 	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.UnAuthorized))
 		return
 		return
 	}
 	}
 	if caller.UserName != user.UserName && caller.PlatformRoleID != models.SuperAdminRole {
 	if caller.UserName != user.UserName && caller.PlatformRoleID != models.SuperAdminRole {
@@ -108,7 +109,7 @@ func createUserAccessToken(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(
 		logic.ReturnErrorResponse(
 			w,
 			w,
 			r,
 			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
 		return
 	}
 	}
@@ -117,10 +118,26 @@ func createUserAccessToken(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(
 		logic.ReturnErrorResponse(
 			w,
 			w,
 			r,
 			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
 		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{
 	logic.ReturnSuccessResponseWithJson(w, r, models.SuccessfulUserLoginResponse{
 		AuthToken: jwt,
 		AuthToken: jwt,
 		UserName:  req.UserName,
 		UserName:  req.UserName,
@@ -165,7 +182,7 @@ func deleteUserAccessTokens(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 	err := a.Get(r.Context())
 	err := a.Get(r.Context())
 	if err != nil {
 	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
 		return
 	}
 	}
 	caller, err := logic.GetUser(r.Header.Get("user"))
 	caller, err := logic.GetUser(r.Header.Get("user"))
@@ -199,6 +216,22 @@ func deleteUserAccessTokens(w http.ResponseWriter, r *http.Request) {
 		)
 		)
 		return
 		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")
 	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"))
 			logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("access denied to dashboard"), "unauthorized"))
 			return
 			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
 	username := authRequest.UserName
@@ -682,6 +747,21 @@ func createUser(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 		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.DeleteUserInvite(user.UserName)
 	logic.DeletePendingUser(user.UserName)
 	logic.DeletePendingUser(user.UserName)
 	go mq.PublishPeerUpdate(false)
 	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) {
 	if userchange.PlatformRoleID != user.PlatformRoleID || !logic.CompareMaps(user.UserGroups, userchange.UserGroups) {
 		(&schema.UserAccessToken{UserName: user.UserName}).DeleteAllUserTokens(r.Context())
 		(&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)
 	user, err = logic.UpdateUser(&userchange, user)
 	if err != nil {
 	if err != nil {
 		logger.Log(0, username,
 		logger.Log(0, username,
@@ -827,6 +926,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 		return
 	}
 	}
+	logic.LogEvent(&e)
 	go mq.PublishPeerUpdate(false)
 	go mq.PublishPeerUpdate(false)
 	logger.Log(1, username, "was updated")
 	logger.Log(1, username, "was updated")
 	json.NewEncoder(w).Encode(logic.ToReturnUser(*user))
 	json.NewEncoder(w).Encode(logic.ToReturnUser(*user))
@@ -905,6 +1005,21 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		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
 	// check and delete extclient with this ownerID
 	go func() {
 	go func() {
 		extclients, err := logic.GetAllExtClients()
 		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")
 	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
 	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.
 // BeginTx returns a context with a new transaction.
 // If the context already has a db connection instance,
 // If the context already has a db connection instance,
 // it uses that instance. Otherwise, it uses the
 // 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/blang/semver v3.5.1+incompatible
 	github.com/eclipse/paho.mqtt.golang v1.5.0
 	github.com/eclipse/paho.mqtt.golang v1.5.0
 	github.com/go-playground/validator/v10 v10.26.0
 	github.com/go-playground/validator/v10 v10.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/google/uuid v1.6.0
 	github.com/gorilla/handlers v1.5.2
 	github.com/gorilla/handlers v1.5.2
 	github.com/gorilla/mux v1.8.1
 	github.com/gorilla/mux v1.8.1
 	github.com/lib/pq v1.10.9
 	github.com/lib/pq v1.10.9
-	github.com/mattn/go-sqlite3 v1.14.24
+	github.com/mattn/go-sqlite3 v1.14.28
 	github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa
 	github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa
 	github.com/seancfoley/ipaddress-go v1.7.0
 	github.com/seancfoley/ipaddress-go v1.7.0
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/stretchr/testify v1.10.0
 	github.com/stretchr/testify v1.10.0
 	github.com/txn2/txeh v1.5.5
 	github.com/txn2/txeh v1.5.5
 	go.uber.org/automaxprocs v1.6.0
 	go.uber.org/automaxprocs v1.6.0
-	golang.org/x/crypto v0.37.0
+	golang.org/x/crypto v0.38.0
 	golang.org/x/net v0.39.0 // indirect
 	golang.org/x/net v0.39.0 // indirect
 	golang.org/x/oauth2 v0.29.0
 	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
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb
 	gopkg.in/yaml.v3 v3.0.1
 	gopkg.in/yaml.v3 v3.0.1
 )
 )
@@ -32,11 +32,11 @@ require (
 require (
 require (
 	filippo.io/edwards25519 v1.1.0
 	filippo.io/edwards25519 v1.1.0
 	github.com/c-robinson/iplib v1.0.8
 	github.com/c-robinson/iplib v1.0.8
-	github.com/posthog/posthog-go v1.2.24
+	github.com/posthog/posthog-go v1.5.5
 )
 )
 
 
 require (
 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
 	github.com/gorilla/websocket v1.5.3
 	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
 	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
 )
 )
@@ -49,9 +49,10 @@ require (
 	github.com/spf13/cobra v1.9.1
 	github.com/spf13/cobra v1.9.1
 	google.golang.org/api v0.229.0
 	google.golang.org/api v0.229.0
 	gopkg.in/mail.v2 v2.3.1
 	gopkg.in/mail.v2 v2.3.1
+	gorm.io/datatypes v1.2.5
 	gorm.io/driver/postgres v1.5.11
 	gorm.io/driver/postgres v1.5.11
 	gorm.io/driver/sqlite v1.5.7
 	gorm.io/driver/sqlite v1.5.7
-	gorm.io/gorm v1.25.12
+	gorm.io/gorm v1.26.1
 )
 )
 
 
 require (
 require (
@@ -65,6 +66,9 @@ require (
 	github.com/google/s2a-go v0.1.9 // indirect
 	github.com/google/s2a-go v0.1.9 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
 	github.com/googleapis/gax-go/v2 v2.14.1 // 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/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jackc/pgpassfile v1.0.0 // indirect
 	github.com/jackc/pgpassfile v1.0.0 // indirect
 	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // 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/grpc v1.71.1 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
+	gorm.io/driver/mysql v1.5.6 // indirect
 )
 )
 
 
 require (
 require (
@@ -96,5 +101,5 @@ require (
 	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/mattn/go-runewidth v0.0.13 // indirect
 	github.com/mattn/go-runewidth v0.0.13 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // 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/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 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
 github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
 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 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 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=
 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/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 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
 github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
 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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
 github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
@@ -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/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 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
 github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 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 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
 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.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
 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-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 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/posthog/posthog-go v1.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 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
 github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
 github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 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=
 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-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.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 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 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/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 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
 golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
 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-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.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.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 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
 golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 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=
 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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I=
+gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4=
+gorm.io/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 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
 gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
 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 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
 gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
 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
 package logic
 
 
 import (
 import (
+	"context"
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
@@ -12,10 +13,23 @@ import (
 
 
 	"github.com/google/uuid"
 	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/servercfg"
 	"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 (
 var (
 	aclCacheMutex = &sync.RWMutex{}
 	aclCacheMutex = &sync.RWMutex{}
 	aclCacheMap   = make(map[string]models.Acl)
 	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() {
 		if currentNode.Network != netID.String() {
 			continue
 			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{}{}
 					resultMap[egressRangeI] = struct{}{}
 				}
 				}
 			}
 			}
@@ -257,78 +271,102 @@ func GetEgressRanges(netID models.NetworkID) (map[string][]string, map[string]st
 	return nodeEgressMap, resultMap, nil
 	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 {
 	switch t.ID {
 	case models.NodeTagID:
 	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
 		// check if tag is valid
 		_, err := GetTag(models.TagID(t.Value))
 		_, err := GetTag(models.TagID(t.Value))
 		if err != nil {
 		if err != nil {
-			return false
+			return errors.New("invalid tag " + t.Value)
 		}
 		}
 	case models.NodeID:
 	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)
 		_, nodeErr := GetNodeByID(t.Value)
 		if nodeErr != nil {
 		if nodeErr != nil {
-			_, staticNodeErr := GetExtClient(t.Value, netID.String())
+			_, staticNodeErr := GetExtClient(t.Value, a.NetworkID.String())
 			if staticNodeErr != nil {
 			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:
 	case models.UserAclID:
-		if policyType == models.DevicePolicy {
-			return false
+		if a.RuleType == models.DevicePolicy {
+			return errors.New("device policy source mismatch")
 		}
 		}
 		if !isSrc {
 		if !isSrc {
-			return false
+			return errors.New("user cannot be added to destination")
 		}
 		}
 		_, err := GetUser(t.Value)
 		_, err := GetUser(t.Value)
 		if err != nil {
 		if err != nil {
-			return false
+			return errors.New("invalid user " + t.Value)
 		}
 		}
 	case models.UserGroupAclID:
 	case models.UserGroupAclID:
-		if policyType == models.DevicePolicy {
-			return false
+		if a.RuleType == models.DevicePolicy {
+			return errors.New("device policy source mismatch")
 		}
 		}
 		if !isSrc {
 		if !isSrc {
-			return false
+			return errors.New("user cannot be added to destination")
 		}
 		}
 		err := IsGroupValid(models.UserGroupID(t.Value))
 		err := IsGroupValid(models.UserGroupID(t.Value))
 		if err != nil {
 		if err != nil {
-			return false
+			return errors.New("invalid user group " + t.Value)
 		}
 		}
 		// check if group belongs to this network
 		// check if group belongs to this network
-		netGrps := GetUserGroupsInNetwork(netID)
+		netGrps := GetUserGroupsInNetwork(a.NetworkID)
 		if _, ok := netGrps[models.UserGroupID(t.Value)]; !ok {
 		if _, ok := netGrps[models.UserGroupID(t.Value)]; !ok {
-			return false
+			return errors.New("invalid user group " + t.Value)
 		}
 		}
 	default:
 	default:
-		return false
+		return errors.New("invalid policy")
 	}
 	}
-	return true
+	return nil
 }
 }
 
 
 // IsAclPolicyValid - validates if acl policy is valid
 // 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
 	//check if src and dst are valid
 	if acl.AllowedDirection != models.TrafficDirectionBi &&
 	if acl.AllowedDirection != models.TrafficDirectionBi &&
 		acl.AllowedDirection != models.TrafficDirectionUni {
 		acl.AllowedDirection != models.TrafficDirectionUni {
-		return false
+		return errors.New("invalid traffic direction")
 	}
 	}
 	switch acl.RuleType {
 	switch acl.RuleType {
 	case models.UserPolicy:
 	case models.UserPolicy:
@@ -339,8 +377,8 @@ func IsAclPolicyValid(acl models.Acl) bool {
 				continue
 				continue
 			}
 			}
 			// check if user group is valid
 			// 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 {
 		for _, dstI := range acl.Dst {
@@ -350,8 +388,8 @@ func IsAclPolicyValid(acl models.Acl) bool {
 			}
 			}
 
 
 			// check if user group is valid
 			// 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:
 	case models.DevicePolicy:
@@ -360,8 +398,8 @@ func IsAclPolicyValid(acl models.Acl) bool {
 				continue
 				continue
 			}
 			}
 			// check if user group is valid
 			// 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 {
 		for _, dstI := range acl.Dst {
@@ -370,12 +408,26 @@ func IsAclPolicyValid(acl models.Acl) bool {
 				continue
 				continue
 			}
 			}
 			// check if user group is valid
 			// 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
 // UpdateAcl - updates allowed fields on acls and commits to DB
@@ -623,6 +675,17 @@ func IsUserAllowedToCommunicate(userName string, peer models.Node) (bool, []mode
 			continue
 			continue
 		}
 		}
 		dstMap := convAclTagToValueMap(policy.Dst)
 		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 {
 		if _, ok := dstMap["*"]; ok {
 			allowedPolicies = append(allowedPolicies, policy)
 			allowedPolicies = append(allowedPolicies, policy)
 			continue
 			continue
@@ -712,8 +775,20 @@ func IsPeerAllowed(node, peer models.Node, checkDefaultPolicy bool) bool {
 		if !policy.Enabled {
 		if !policy.Enabled {
 			continue
 			continue
 		}
 		}
+
 		srcMap = convAclTagToValueMap(policy.Src)
 		srcMap = convAclTagToValueMap(policy.Src)
 		dstMap = convAclTagToValueMap(policy.Dst)
 		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) {
 		if checkTagGroupPolicy(srcMap, dstMap, node, peer, nodeTags, peerTags) {
 			return true
 			return true
 		}
 		}
@@ -975,6 +1050,17 @@ func IsNodeAllowedToCommunicateV1(node, peer models.Node, checkDefaultPolicy boo
 		allowed := false
 		allowed := false
 		srcMap = convAclTagToValueMap(policy.Src)
 		srcMap = convAclTagToValueMap(policy.Src)
 		dstMap = convAclTagToValueMap(policy.Dst)
 		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["*"]
 		_, srcAll := srcMap["*"]
 		_, dstAll := dstMap["*"]
 		_, dstAll := dstMap["*"]
 		if policy.AllowedDirection == models.TrafficDirectionBi {
 		if policy.AllowedDirection == models.TrafficDirectionBi {
@@ -1158,7 +1244,7 @@ func getEgressUserRulesForNode(targetnode *models.Node,
 	acls := listUserPolicies(models.NetworkID(targetnode.Network))
 	acls := listUserPolicies(models.NetworkID(targetnode.Network))
 	var targetNodeTags = make(map[models.TagID]struct{})
 	var targetNodeTags = make(map[models.TagID]struct{})
 	targetNodeTags["*"] = struct{}{}
 	targetNodeTags["*"] = struct{}{}
-	for _, rangeI := range targetnode.EgressGatewayRanges {
+	for _, rangeI := range targetnode.EgressDetails.EgressGatewayRanges {
 		targetNodeTags[models.TagID(rangeI)] = struct{}{}
 		targetNodeTags[models.TagID(rangeI)] = struct{}{}
 	}
 	}
 	for _, acl := range acls {
 	for _, acl := range acls {
@@ -1166,6 +1252,18 @@ func getEgressUserRulesForNode(targetnode *models.Node,
 			continue
 			continue
 		}
 		}
 		dstTags := convAclTagToValueMap(acl.Dst)
 		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["*"]
 		_, all := dstTags["*"]
 		addUsers := false
 		addUsers := false
 		if !all {
 		if !all {
@@ -1225,16 +1323,34 @@ func getEgressUserRulesForNode(targetnode *models.Node,
 				r.IP6List = append(r.IP6List, userNode.StaticNode.AddressIPNet6())
 				r.IP6List = append(r.IP6List, userNode.StaticNode.AddressIPNet6())
 			}
 			}
 			for _, dstI := range acl.Dst {
 			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 {
 func checkIfAnyActiveEgressPolicy(targetNode models.Node) bool {
-	if !targetNode.IsEgressGateway {
+	if !targetNode.EgressDetails.IsEgressGateway {
 		return false
 		return false
 	}
 	}
 	var targetNodeTags = make(map[models.TagID]struct{})
 	var targetNodeTags = make(map[models.TagID]struct{})
@@ -1371,8 +1487,20 @@ func checkIfAnyActiveEgressPolicy(targetNode models.Node) bool {
 		}
 		}
 		srcTags := convAclTagToValueMap(acl.Src)
 		srcTags := convAclTagToValueMap(acl.Src)
 		dstTags := convAclTagToValueMap(acl.Dst)
 		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 {
 		for nodeTag := range targetNodeTags {
-			if acl.RuleType == models.DevicePolicy {
+			if acl.RuleType == models.DevicePolicy && acl.AllowedDirection == models.TrafficDirectionBi {
 				if _, ok := srcTags[nodeTag.String()]; ok {
 				if _, ok := srcTags[nodeTag.String()]; ok {
 					return true
 					return true
 				}
 				}
@@ -1411,7 +1539,7 @@ func checkIfAnyPolicyisUniDirectional(targetNode models.Node) bool {
 		if !acl.Enabled {
 		if !acl.Enabled {
 			continue
 			continue
 		}
 		}
-		if acl.AllowedDirection == models.TrafficDirectionBi {
+		if acl.AllowedDirection == models.TrafficDirectionBi && acl.Proto == models.ALL && acl.ServiceType == models.Any {
 			continue
 			continue
 		}
 		}
 		if acl.Proto != models.ALL || acl.ServiceType != models.Any {
 		if acl.Proto != models.ALL || acl.ServiceType != models.Any {
@@ -1440,6 +1568,60 @@ func checkIfAnyPolicyisUniDirectional(targetNode models.Node) bool {
 	return false
 	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) {
 func GetAclRulesForNode(targetnodeI *models.Node) (rules map[string]models.AclRule) {
 	targetnode := *targetnodeI
 	targetnode := *targetnodeI
 	defer func() {
 	defer func() {
@@ -1454,7 +1636,7 @@ func GetAclRulesForNode(targetnodeI *models.Node) (rules map[string]models.AclRu
 	} else {
 	} else {
 		taggedNodes = GetTagMapWithNodesByNetwork(models.NetworkID(targetnode.Network), true)
 		taggedNodes = GetTagMapWithNodesByNetwork(models.NetworkID(targetnode.Network), true)
 	}
 	}
-
+	fmt.Printf("TAGGED NODES: %+v\n", taggedNodes)
 	acls := listDevicePolicies(models.NetworkID(targetnode.Network))
 	acls := listDevicePolicies(models.NetworkID(targetnode.Network))
 	var targetNodeTags = make(map[models.TagID]struct{})
 	var targetNodeTags = make(map[models.TagID]struct{})
 	if targetnode.Mutex != nil {
 	if targetnode.Mutex != nil {
@@ -1475,6 +1657,17 @@ func GetAclRulesForNode(targetnodeI *models.Node) (rules map[string]models.AclRu
 		}
 		}
 		srcTags := convAclTagToValueMap(acl.Src)
 		srcTags := convAclTagToValueMap(acl.Src)
 		dstTags := convAclTagToValueMap(acl.Dst)
 		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["*"]
 		_, srcAll := srcTags["*"]
 		_, dstAll := dstTags["*"]
 		_, dstAll := dstTags["*"]
 		aclRule := models.AclRule{
 		aclRule := models.AclRule{
@@ -1502,7 +1695,7 @@ func GetAclRulesForNode(targetnodeI *models.Node) (rules map[string]models.AclRu
 					existsInDstTag = true
 					existsInDstTag = true
 				}
 				}
 
 
-				if existsInSrcTag && !existsInDstTag {
+				if existsInSrcTag /* && !existsInDstTag*/ {
 					// get all dst tags
 					// get all dst tags
 					for dst := range dstTags {
 					for dst := range dstTags {
 						if dst == nodeTag.String() {
 						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
 					// get all src tags
 					for src := range srcTags {
 					for src := range srcTags {
 						if src == nodeTag.String() {
 						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 {
 			} else {
 				_, all := dstTags["*"]
 				_, all := dstTags["*"]
 				if _, ok := dstTags[nodeTag.String()]; ok || all {
 				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
 			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
 			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 {
 	for _, acl := range acls {
 		if !acl.Enabled {
 		if !acl.Enabled {
@@ -1689,46 +1896,43 @@ func GetEgressRulesForNode(targetnode models.Node) (rules map[string]models.AclR
 		dstTags := convAclTagToValueMap(acl.Dst)
 		dstTags := convAclTagToValueMap(acl.Dst)
 		_, srcAll := srcTags["*"]
 		_, srcAll := srcTags["*"]
 		_, dstAll := dstTags["*"]
 		_, dstAll := dstTags["*"]
+		aclRule := models.AclRule{
+			ID:              acl.ID,
+			AllowedProtocol: acl.Proto,
+			AllowedPorts:    acl.Port,
+			Direction:       acl.AllowedDirection,
+			Allowed:         true,
+		}
 		for nodeTag := range targetNodeTags {
 		for nodeTag := range targetNodeTags {
-			aclRule := models.AclRule{
-				ID:              acl.ID,
-				AllowedProtocol: acl.Proto,
-				AllowedPorts:    acl.Port,
-				Direction:       acl.AllowedDirection,
-				Allowed:         true,
-			}
+
 			if nodeTag != "*" {
 			if nodeTag != "*" {
 				ip, cidr, err := net.ParseCIDR(nodeTag.String())
 				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 {
 			if acl.AllowedDirection == models.TrafficDirectionBi {
 				var existsInSrcTag bool
 				var existsInSrcTag bool
 				var existsInDstTag bool
 				var existsInDstTag bool
-
 				if _, ok := srcTags[nodeTag.String()]; ok || srcAll {
 				if _, ok := srcTags[nodeTag.String()]; ok || srcAll {
 					existsInSrcTag = true
 					existsInSrcTag = true
 				}
 				}
 				if _, ok := dstTags[nodeTag.String()]; ok || dstAll {
 				if _, ok := dstTags[nodeTag.String()]; ok || dstAll {
 					existsInDstTag = true
 					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 {
 				if existsInSrcTag && !existsInDstTag {
 					// get all dst tags
 					// get all dst tags
 					for dst := range dstTags {
 					for dst := range dstTags {
@@ -1835,8 +2039,16 @@ func GetEgressRulesForNode(targetnode models.Node) (rules map[string]models.AclR
 					}
 					}
 				}
 				}
 			} else {
 			} 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
 					// get all src tags
 					for src := range srcTags {
 					for src := range srcTags {
 						if src == nodeTag.String() {
 						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
 	return

+ 1 - 1
logic/auth.go

@@ -242,7 +242,7 @@ func VerifyAuthRequest(authRequest models.UserAuthParams) (string, error) {
 	}
 	}
 
 
 	// update last login time
 	// update last login time
-	result.LastLoginTime = time.Now()
+	result.LastLoginTime = time.Now().UTC()
 	err = UpsertUser(result)
 	err = UpsertUser(result)
 	if err != nil {
 	if err != nil {
 		slog.Error("error upserting user", "error", err)
 		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"
 	"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
 // 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
 	var status = http.StatusInternalServerError
 	switch errType {
 	switch errType {
-	case "internal":
+	case Internal:
 		status = http.StatusInternalServerError
 		status = http.StatusInternalServerError
-	case "badrequest":
+	case BadReq:
 		status = http.StatusBadRequest
 		status = http.StatusBadRequest
-	case "notfound":
+	case NotFound:
 		status = http.StatusNotFound
 		status = http.StatusNotFound
-	case "unauthorized":
+	case UnAuthorized:
 		status = http.StatusUnauthorized
 		status = http.StatusUnauthorized
-	case "forbidden":
+	case Forbidden:
 		status = http.StatusForbidden
 		status = http.StatusForbidden
 	default:
 	default:
 		status = http.StatusInternalServerError
 		status = http.StatusInternalServerError

+ 49 - 5
logic/extpeers.go

@@ -1,6 +1,7 @@
 package logic
 package logic
 
 
 import (
 import (
+	"context"
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
@@ -13,9 +14,11 @@ import (
 
 
 	"github.com/goombaio/namegenerator"
 	"github.com/goombaio/namegenerator"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/exp/slog"
 	"golang.org/x/exp/slog"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
@@ -71,13 +74,19 @@ func GetEgressRangesOnNetwork(client *models.ExtClient) ([]string, error) {
 	if err != nil {
 	if err != nil {
 		return []string{}, err
 		return []string{}, err
 	}
 	}
+	// clientNode := client.ConvertToStaticNode()
 	for _, currentNode := range networkNodes {
 	for _, currentNode := range networkNodes {
 		if currentNode.Network != client.Network {
 		if currentNode.Network != client.Network {
 			continue
 			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)
 		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())
 	go RemoveNodeFromAclPolicy(extClient.ConvertToStaticNode())
 	return nil
 	return nil
 }
 }
@@ -627,7 +655,15 @@ func getFwRulesForNodeAndPeerOnGw(node, peer models.Node, allowedPolicies []mode
 
 
 		// add egress range rules
 		// add egress range rules
 		for _, dstI := range policy.Dst {
 		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)
 				ip, cidr, err := net.ParseCIDR(dstI.Value)
 				if err == nil {
 				if err == nil {
 					if ip.To4() != nil {
 					if ip.To4() != nil {
@@ -708,7 +744,15 @@ func getFwRulesForUserNodesOnGw(node models.Node, nodes []models.Node) (rules []
 
 
 						// add egress ranges
 						// add egress ranges
 						for _, dstI := range policy.Dst {
 						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)
 								ip, cidr, err := net.ParseCIDR(dstI.Value)
 								if err == nil {
 								if err == nil {
 									if ip.To4() != nil && userNodeI.StaticNode.Address != "" {
 									if ip.To4() != nil && userNodeI.StaticNode.Address != "" {

+ 28 - 14
logic/gateway.go

@@ -1,6 +1,7 @@
 package logic
 package logic
 
 
 import (
 import (
+	"context"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"slices"
 	"slices"
@@ -8,14 +9,27 @@ import (
 	"time"
 	"time"
 
 
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 )
 )
 
 
 // IsInternetGw - checks if node is acting as internet gw
 // IsInternetGw - checks if node is acting as internet gw
 func IsInternetGw(node models.Node) bool {
 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
 // GetInternetGateways - gets all the nodes that are internet gateways
@@ -26,7 +40,7 @@ func GetInternetGateways() ([]models.Node, error) {
 	}
 	}
 	igs := make([]models.Node, 0)
 	igs := make([]models.Node, 0)
 	for _, node := range nodes {
 	for _, node := range nodes {
-		if node.IsInternetGateway {
+		if node.EgressDetails.IsInternetGateway {
 			igs = append(igs, node)
 			igs = append(igs, node)
 		}
 		}
 	}
 	}
@@ -56,7 +70,7 @@ func GetAllEgresses() ([]models.Node, error) {
 	}
 	}
 	egresses := make([]models.Node, 0)
 	egresses := make([]models.Node, 0)
 	for _, node := range nodes {
 	for _, node := range nodes {
-		if node.IsEgressGateway {
+		if node.EgressDetails.IsEgressGateway {
 			egresses = append(egresses, node)
 			egresses = append(egresses, node)
 		}
 		}
 	}
 	}
@@ -133,11 +147,11 @@ func CreateEgressGateway(gateway models.EgressGatewayRequest) (models.Node, erro
 	if gateway.Ranges == nil {
 	if gateway.Ranges == nil {
 		gateway.Ranges = make([]string, 0)
 		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()
 	node.SetLastModified()
 	if err = UpsertNode(&node); err != nil {
 	if err = UpsertNode(&node); err != nil {
 		return models.Node{}, err
 		return models.Node{}, err
@@ -156,9 +170,9 @@ func DeleteEgressGateway(network, nodeid string) (models.Node, error) {
 	if err != nil {
 	if err != nil {
 		return models.Node{}, err
 		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()
 	node.SetLastModified()
 	if err = UpsertNode(&node); err != nil {
 	if err = UpsertNode(&node); err != nil {
 		return models.Node{}, err
 		return models.Node{}, err
@@ -191,12 +205,12 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq
 	node.IsIngressGateway = true
 	node.IsIngressGateway = true
 	node.IsGw = true
 	node.IsGw = true
 	if !servercfg.IsPro {
 	if !servercfg.IsPro {
-		node.IsInternetGateway = ingress.IsInternetGateway
+		node.EgressDetails.IsInternetGateway = ingress.IsInternetGateway
 	}
 	}
 	node.IngressGatewayRange = network.AddressRange
 	node.IngressGatewayRange = network.AddressRange
 	node.IngressGatewayRange6 = network.AddressRange6
 	node.IngressGatewayRange6 = network.AddressRange6
 	node.IngressDNS = ingress.ExtclientDNS
 	node.IngressDNS = ingress.ExtclientDNS
-	if node.IsInternetGateway && node.IngressDNS == "" {
+	if node.EgressDetails.IsInternetGateway && node.IngressDNS == "" {
 		node.IngressDNS = "1.1.1.1"
 		node.IngressDNS = "1.1.1.1"
 	}
 	}
 	node.IngressPersistentKeepalive = 20
 	node.IngressPersistentKeepalive = 20
@@ -267,10 +281,10 @@ func DeleteIngressGateway(nodeid string) (models.Node, []models.ExtClient, error
 		return models.Node{}, removedClients, err
 		return models.Node{}, removedClients, err
 	}
 	}
 	logger.Log(3, "deleting ingress gateway")
 	logger.Log(3, "deleting ingress gateway")
-	node.LastModified = time.Now()
+	node.LastModified = time.Now().UTC()
 	node.IsIngressGateway = false
 	node.IsIngressGateway = false
 	if !servercfg.IsPro {
 	if !servercfg.IsPro {
-		node.IsInternetGateway = false
+		node.EgressDetails.IsInternetGateway = false
 	}
 	}
 	delete(node.Tags, models.TagID(fmt.Sprintf("%s.%s", node.Network, models.GwTagName)))
 	delete(node.Tags, models.TagID(fmt.Sprintf("%s.%s", node.Network, models.GwTagName)))
 	node.IngressGatewayRange = ""
 	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
 // CheckHostPort checks host endpoints to ensures that hosts on the same server
 // with the same endpoint have different listen ports
 // with the same endpoint have different listen ports
 // in the case of 64535 hosts or more with same endpoint, ports will not be changed
 // 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)
 	portsInUse := make(map[int]bool, 0)
 	hosts, err := GetAllHosts()
 	hosts, err := GetAllHosts()
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
+	originalPort := h.ListenPort
+	defer func() {
+		if originalPort != h.ListenPort {
+			changed = true
+		}
+	}()
+	if h.EndpointIP == nil {
+		return
+	}
 	for _, host := range hosts {
 	for _, host := range hosts {
 		if host.ID.String() == h.ID.String() {
 		if host.ID.String() == h.ID.String() {
 			// skip self
 			// skip self
 			continue
 			continue
 		}
 		}
+		if host.EndpointIP == nil {
+			continue
+		}
 		if !host.EndpointIP.Equal(h.EndpointIP) {
 		if !host.EndpointIP.Equal(h.EndpointIP) {
 			continue
 			continue
 		}
 		}
@@ -566,11 +578,16 @@ func CheckHostPorts(h *models.Host) {
 	}
 	}
 	// iterate until port is not found or max iteration is reached
 	// iterate until port is not found or max iteration is reached
 	for i := 0; portsInUse[h.ListenPort] && i < maxPort-minPort+1; i++ {
 	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 {
 		if h.ListenPort > maxPort {
 			h.ListenPort = minPort
 			h.ListenPort = minPort
 		}
 		}
 	}
 	}
+	return
 }
 }
 
 
 // HostExists - checks if given host already exists
 // 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")
 				err = errors.New("token revoked")
 				return "", err
 				return "", err
 			}
 			}
-			a.LastUsed = time.Now()
+			a.LastUsed = time.Now().UTC()
 			a.Update(db.WithContext(context.TODO()))
 			a.Update(db.WithContext(context.TODO()))
 		}
 		}
 	}
 	}
@@ -181,7 +181,7 @@ func VerifyUserToken(tokenString string) (username string, issuperadmin, isadmin
 				err = errors.New("token revoked")
 				err = errors.New("token revoked")
 				return "", false, false, err
 				return "", false, false, err
 			}
 			}
-			a.LastUsed = time.Now()
+			a.LastUsed = time.Now().UTC()
 			a.Update(db.WithContext(context.TODO()))
 			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
 	var network models.Network
 	network, err := GetParentNetwork(networkName)
 	network, err := GetParentNetwork(networkName)
 	if err != nil {
 	if err != nil {
-		fmt.Println("Network Not Found")
 		return add, err
 		return add, err
 	}
 	}
 	if network.IsIPv6 == "no" {
 	if network.IsIPv6 == "no" {
@@ -567,7 +566,6 @@ func UniqueAddress6Cache(networkName string, reverse bool) (net.IP, error) {
 	var network models.Network
 	var network models.Network
 	network, err := GetParentNetwork(networkName)
 	network, err := GetParentNetwork(networkName)
 	if err != nil {
 	if err != nil {
-		fmt.Println("Network Not Found")
 		return add, err
 		return add, err
 	}
 	}
 	if network.IsIPv6 == "no" {
 	if network.IsIPv6 == "no" {

+ 39 - 13
logic/nodes.go

@@ -164,7 +164,7 @@ func UpdateNodeCheckin(node *models.Node) error {
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-
+	node.EgressDetails = models.EgressDetails{}
 	err = database.Insert(node.ID.String(), string(data), database.NODES_TABLE_NAME)
 	err = database.Insert(node.ID.String(), string(data), database.NODES_TABLE_NAME)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -183,6 +183,7 @@ func UpsertNode(newNode *models.Node) error {
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
+	newNode.EgressDetails = models.EgressDetails{}
 	err = database.Insert(newNode.ID.String(), string(data), database.NODES_TABLE_NAME)
 	err = database.Insert(newNode.ID.String(), string(data), database.NODES_TABLE_NAME)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -218,7 +219,7 @@ func UpdateNode(currentNode *models.Node, newNode *models.Node) error {
 				return err
 				return err
 			}
 			}
 		}
 		}
-
+		newNode.EgressDetails = models.EgressDetails{}
 		newNode.SetLastModified()
 		newNode.SetLastModified()
 		if data, err := json.Marshal(newNode); err != nil {
 		if data, err := json.Marshal(newNode); err != nil {
 			return err
 			return err
@@ -280,21 +281,21 @@ func DeleteNode(node *models.Node, purge bool) error {
 		// unset all the relayed nodes
 		// unset all the relayed nodes
 		SetRelayedNodes(false, node.ID.String(), node.RelayedNodes)
 		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 {
 		if err == nil {
 			clientNodeIDs := []string{}
 			clientNodeIDs := []string{}
-			for _, inetNodeClientID := range inetNode.InetNodeReq.InetNodeClientIDs {
+			for _, inetNodeClientID := range inetNode.EgressDetails.InetNodeReq.InetNodeClientIDs {
 				if inetNodeClientID == node.ID.String() {
 				if inetNodeClientID == node.ID.String() {
 					continue
 					continue
 				}
 				}
 				clientNodeIDs = append(clientNodeIDs, inetNodeClientID)
 				clientNodeIDs = append(clientNodeIDs, inetNodeClientID)
 			}
 			}
-			inetNode.InetNodeReq.InetNodeClientIDs = clientNodeIDs
+			inetNode.EgressDetails.InetNodeReq.InetNodeClientIDs = clientNodeIDs
 			UpsertNode(&inetNode)
 			UpsertNode(&inetNode)
 		}
 		}
 	}
 	}
-	if node.IsInternetGateway {
+	if node.EgressDetails.IsInternetGateway {
 		UnsetInternetGw(node)
 		UnsetInternetGw(node)
 	}
 	}
 	if !purge && !alreadyDeleted {
 	if !purge && !alreadyDeleted {
@@ -320,8 +321,9 @@ func DeleteNode(node *models.Node, purge bool) error {
 	if err := DissasociateNodeFromHost(node, host); err != nil {
 	if err := DissasociateNodeFromHost(node, host); err != nil {
 		return err
 		return err
 	}
 	}
-	go RemoveNodeFromAclPolicy(*node)
 
 
+	go RemoveNodeFromAclPolicy(*node)
+	go RemoveNodeFromEgress(*node)
 	return nil
 	return nil
 }
 }
 
 
@@ -783,16 +785,16 @@ func ValidateNodeIp(currentNode *models.Node, newNode *models.ApiNode) error {
 	return nil
 	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 {
 	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
 	ipv4Net := network.AddressRange
 	ipv6Net := network.AddressRange6
 	ipv6Net := network.AddressRange6
 
 
-	for _, v := range gateway.Ranges {
+	for _, v := range ranges {
 		if ipv4Net != "" {
 		if ipv4Net != "" {
 			if ContainsCIDR(ipv4Net, v) {
 			if ContainsCIDR(ipv4Net, v) {
 				slog.Error("egress range should not be the same as or contained in the netmaker network address", "error", v, ipv4Net)
 				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
 	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 {
 func GetNodesWithTag(tagID models.TagID) map[string]models.Node {
 	nMap := make(map[string]models.Node)
 	nMap := make(map[string]models.Node)
 	tag, err := GetTag(tagID)
 	tag, err := GetTag(tagID)

+ 50 - 44
logic/peers.go

@@ -6,6 +6,7 @@ import (
 	"net"
 	"net"
 	"net/netip"
 	"net/netip"
 
 
+	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic/acls/nodeacls"
 	"github.com/gravitl/netmaker/logic/acls/nodeacls"
@@ -47,16 +48,19 @@ var (
 	}
 	}
 	// UnsetInternetGw
 	// UnsetInternetGw
 	UnsetInternetGw = func(node *models.Node) {
 	UnsetInternetGw = func(node *models.Node) {
-		node.IsInternetGateway = false
+		node.EgressDetails.IsInternetGateway = false
 	}
 	}
 	// SetInternetGw
 	// SetInternetGw
 	SetInternetGw = func(node *models.Node, req models.InetNodeReq) {
 	SetInternetGw = func(node *models.Node, req models.InetNodeReq) {
-		node.IsInternetGateway = true
+		node.EgressDetails.IsInternetGateway = true
 	}
 	}
 	// GetAllowedIpForInetNodeClient
 	// GetAllowedIpForInetNodeClient
 	GetAllowedIpForInetNodeClient = func(node, peer *models.Node) []net.IPNet {
 	GetAllowedIpForInetNodeClient = func(node, peer *models.Node) []net.IPNet {
 		return []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
 // GetHostPeerInfo - fetches required peer info per network
@@ -161,26 +165,16 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 	}
 	}
 	defer func() {
 	defer func() {
 		if !hostPeerUpdate.FwUpdate.AllowAll {
 		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{
 			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 {
 	for _, nodeID := range host.Nodes {
 		networkAllowAll := true
 		networkAllowAll := true
 		nodeID := nodeID
 		nodeID := nodeID
+		if nodeID == uuid.Nil.String() {
+			continue
+		}
 		node, err := GetNodeByID(nodeID)
 		node, err := GetNodeByID(nodeID)
 		if err != nil {
 		if err != nil {
 			continue
 			continue
 		}
 		}
-
 		if !node.Connected || node.PendingDelete || node.Action == models.NODE_DELETE {
 		if !node.Connected || node.PendingDelete || node.Action == models.NODE_DELETE {
 			continue
 			continue
 		}
 		}
+		GetNodeEgressInfo(&node)
 		hostPeerUpdate = SetDefaultGw(node, hostPeerUpdate)
 		hostPeerUpdate = SetDefaultGw(node, hostPeerUpdate)
 		if !hostPeerUpdate.IsInternetGw {
 		if !hostPeerUpdate.IsInternetGw {
 			hostPeerUpdate.IsInternetGw = IsInternetGw(node)
 			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)
 		defaultUserPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy)
 		defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
 		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 {
 		} else {
 			networkAllowAll = false
 			networkAllowAll = false
 			hostPeerUpdate.FwUpdate.AllowAll = false
 			hostPeerUpdate.FwUpdate.AllowAll = false
@@ -247,8 +253,9 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 				PersistentKeepaliveInterval: &peerHost.PersistentKeepalive,
 				PersistentKeepaliveInterval: &peerHost.PersistentKeepalive,
 				ReplaceAllowedIPs:           true,
 				ReplaceAllowedIPs:           true,
 			}
 			}
+			AddEgressInfoToPeerByAccess(&node, &peer)
 			_, isFailOverPeer := node.FailOverPeers[peer.ID.String()]
 			_, isFailOverPeer := node.FailOverPeers[peer.ID.String()]
-			if peer.IsEgressGateway {
+			if peer.EgressDetails.IsEgressGateway {
 				peerKey := peerHost.PublicKey.String()
 				peerKey := peerHost.PublicKey.String()
 				if isFailOverPeer && peer.FailedOverBy.String() != node.ID.String() {
 				if isFailOverPeer && peer.FailedOverBy.String() != node.ID.String() {
 					// get relay host
 					// 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())
 				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.IsEgressGw = true
 			hostPeerUpdate.FwUpdate.EgressInfo[node.ID.String()] = models.EgressInfo{
 			hostPeerUpdate.FwUpdate.EgressInfo[node.ID.String()] = models.EgressInfo{
 				EgressID: node.ID.String(),
 				EgressID: node.ID.String(),
@@ -449,12 +456,12 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 					IP:   node.Address6.IP,
 					IP:   node.Address6.IP,
 					Mask: getCIDRMaskFromAddr(node.Address6.IP.String()),
 					Mask: getCIDRMaskFromAddr(node.Address6.IP.String()),
 				},
 				},
-				EgressGWCfg:   node.EgressGatewayRequest,
+				EgressGWCfg:   node.EgressDetails.EgressGatewayRequest,
 				EgressFwRules: make(map[string]models.AclRule),
 				EgressFwRules: make(map[string]models.AclRule),
 			}
 			}
 
 
 		}
 		}
-		if node.IsEgressGateway {
+		if node.EgressDetails.IsEgressGateway {
 			if !networkAllowAll {
 			if !networkAllowAll {
 				egressInfo := hostPeerUpdate.FwUpdate.EgressInfo[node.ID.String()]
 				egressInfo := hostPeerUpdate.FwUpdate.EgressInfo[node.ID.String()]
 				if egressInfo.EgressFwRules == nil {
 				if egressInfo.EgressFwRules == nil {
@@ -492,7 +499,6 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 				},
 				},
 			}
 			}
 		}
 		}
-
 	}
 	}
 	// == post peer calculations ==
 	// == post peer calculations ==
 	// indicate removal if no allowed IPs were calculated
 	// 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 {
 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
 		// filter conflicting addrs
 		nodeEgressMap := make(map[string]struct{})
 		nodeEgressMap := make(map[string]struct{})
-		for _, rangeI := range node.EgressGatewayRanges {
+		for _, rangeI := range node.EgressDetails.EgressGatewayRanges {
 			nodeEgressMap[rangeI] = struct{}{}
 			nodeEgressMap[rangeI] = struct{}{}
 		}
 		}
 		for i := len(egressIPs) - 1; i >= 0; i-- {
 		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 {
 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
 		// filter conflicting addrs
 		nodeEgressMap := make(map[string]struct{})
 		nodeEgressMap := make(map[string]struct{})
-		for _, rangeI := range node.EgressGatewayRanges {
+		for _, rangeI := range node.EgressDetails.EgressGatewayRanges {
 			nodeEgressMap[rangeI] = struct{}{}
 			nodeEgressMap[rangeI] = struct{}{}
 		}
 		}
 		for i := len(egressIPs) - 1; i >= 0; i-- {
 		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 {
 func GetAllowedIPs(node, peer *models.Node, metrics *models.Metrics) []net.IPNet {
 	var allowedips []net.IPNet
 	var allowedips []net.IPNet
 	allowedips = getNodeAllowedIPs(peer, node)
 	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)...)
 		allowedips = append(allowedips, GetAllowedIpForInetNodeClient(node, peer)...)
 		return allowedips
 		return allowedips
 	}
 	}
 	if node.IsRelayed && node.RelayedBy == peer.ID.String() {
 	if node.IsRelayed && node.RelayedBy == peer.ID.String() {
 		allowedips = append(allowedips, GetAllowedIpsForRelayed(node, peer)...)
 		allowedips = append(allowedips, GetAllowedIpsForRelayed(node, peer)...)
-		if peer.InternetGwID != "" {
+		if peer.EgressDetails.InternetGwID != "" {
 			return allowedips
 			return allowedips
 		}
 		}
 	}
 	}
@@ -623,11 +629,11 @@ func GetEgressIPs(peer *models.Node) []net.IPNet {
 
 
 	// check for internet gateway
 	// check for internet gateway
 	internetGateway := false
 	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
 		internetGateway = true
 	}
 	}
 	allowedips := []net.IPNet{}
 	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
 		_, ipnet, err := net.ParseCIDR(iprange) // confirming it's valid cidr
 		if err != nil {
 		if err != nil {
 			logger.Log(1, "could not parse gateway IP range. Not adding ", iprange)
 			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)
 		allowedips = append(allowedips, allowed)
 	}
 	}
 	// handle egress gateway peers
 	// handle egress gateway peers
-	if peer.IsEgressGateway {
+	if peer.EgressDetails.IsEgressGateway {
 		// hasGateway = true
 		// hasGateway = true
 		egressIPs := GetEgressIPs(peer)
 		egressIPs := GetEgressIPs(peer)
-		if node.IsEgressGateway {
+		if node.EgressDetails.IsEgressGateway {
 			// filter conflicting addrs
 			// filter conflicting addrs
 			nodeEgressMap := make(map[string]struct{})
 			nodeEgressMap := make(map[string]struct{})
-			for _, rangeI := range node.EgressGatewayRanges {
+			for _, rangeI := range node.EgressDetails.EgressGatewayRanges {
 				nodeEgressMap[rangeI] = struct{}{}
 				nodeEgressMap[rangeI] = struct{}{}
 			}
 			}
 			for i := len(egressIPs) - 1; i >= 0; i-- {
 			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 {
 		if err != nil {
 			return err
 			return err
 		}
 		}
+		GetNodeEgressInfo(&relayedNode)
 		if relayedNode.IsIngressGateway {
 		if relayedNode.IsIngressGateway {
 			return errors.New("cannot relay an ingress gateway (" + relayedNodeID + ")")
 			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 + ")")
 			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 + ")")
 			return errors.New("cannot relay an internet client (" + relayedNodeID + ")")
 		}
 		}
 		if relayedNode.IsFailOver {
 		if relayedNode.IsFailOver {
@@ -193,8 +194,9 @@ func RelayedAllowedIPs(peer, node *models.Node) []net.IPNet {
 		if err != nil {
 		if err != nil {
 			continue
 			continue
 		}
 		}
+		GetNodeEgressInfo(&relayedNode)
 		allowed := getRelayedAddresses(relayedNodeID)
 		allowed := getRelayedAddresses(relayedNodeID)
-		if relayedNode.IsEgressGateway {
+		if relayedNode.EgressDetails.IsEgressGateway {
 			allowed = append(allowed, GetEgressIPs(&relayedNode)...)
 			allowed = append(allowed, GetEgressIPs(&relayedNode)...)
 		}
 		}
 		allowedIPs = append(allowedIPs, allowed...)
 		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")
 		logger.Log(0, "RelayedByRelay called with invalid parameters")
 		return
 		return
 	}
 	}
-	if relay.InternetGwID != "" {
+	if relay.EgressDetails.InternetGwID != "" {
 		return GetAllowedIpForInetNodeClient(relayed, relay)
 		return GetAllowedIpForInetNodeClient(relayed, relay)
 	}
 	}
 	peers, err := GetNetworkNodes(relay.Network)
 	peers, err := GetNetworkNodes(relay.Network)

+ 1 - 1
logic/tags.go

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

+ 2 - 0
logic/telemetry.go

@@ -20,6 +20,8 @@ var (
 	telServerRecord = models.Telemetry{}
 	telServerRecord = models.Telemetry{}
 )
 )
 
 
+var LogEvent = func(a *models.Event) {}
+
 // posthog_pub_key - Key for sending data to PostHog
 // posthog_pub_key - Key for sending data to PostHog
 const posthog_pub_key = "phc_1vEXhPOA1P7HP5jP2dVU9xDTUqXHAelmtravyZ1vvES"
 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
 	// single comparison statements
 	if newNode.Address.String() != currentNode.Address.String() ||
 	if newNode.Address.String() != currentNode.Address.String() ||
 		newNode.Address6.String() != currentNode.Address6.String() ||
 		newNode.Address6.String() != currentNode.Address6.String() ||
-		newNode.IsEgressGateway != currentNode.IsEgressGateway ||
-		newNode.IsIngressGateway != currentNode.IsIngressGateway ||
 		newNode.IsRelay != currentNode.IsRelay ||
 		newNode.IsRelay != currentNode.IsRelay ||
-		newNode.DNSOn != currentNode.DNSOn ||
 		newNode.Connected != currentNode.Connected {
 		newNode.Connected != currentNode.Connected {
 		return true
 		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 newNode.IsRelay {
 		if len(currentNode.RelayedNodes) != len(newNode.RelayedNodes) {
 		if len(currentNode.RelayedNodes) != len(newNode.RelayedNodes) {
 			return true
 			return true

+ 217 - 0
migrate/migrate.go

@@ -1,20 +1,24 @@
 package migrate
 package migrate
 
 
 import (
 import (
+	"context"
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
 	"log"
 	"log"
 	"time"
 	"time"
 
 
 	"golang.org/x/exp/slog"
 	"golang.org/x/exp/slog"
+	"gorm.io/datatypes"
 
 
 	"github.com/google/uuid"
 	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/mq"
+	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 )
 )
 
 
@@ -31,6 +35,7 @@ func Run() {
 	updateNodes()
 	updateNodes()
 	updateAcls()
 	updateAcls()
 	migrateToGws()
 	migrateToGws()
+	migrateToEgressV1()
 }
 }
 
 
 func assignSuperAdmin() {
 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() {
 func settings() {
 	_, err := database.FetchRecords(database.SERVER_SETTINGS)
 	_, err := database.FetchRecords(database.SERVER_SETTINGS)
 	if database.IsEmptyRecord(err) {
 	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"
 	NodeTagID                AclGroupType = "tag"
 	NodeID                   AclGroupType = "device"
 	NodeID                   AclGroupType = "device"
 	EgressRange              AclGroupType = "egress-range"
 	EgressRange              AclGroupType = "egress-range"
+	EgressID                 AclGroupType = "egress-id"
 	NetmakerIPAclID          AclGroupType = "ip"
 	NetmakerIPAclID          AclGroupType = "ip"
 	NetmakerSubNetRangeAClID AclGroupType = "ipset"
 	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.PendingDelete = a.PendingDelete
 	convertedNode.FailedOverBy = currentNode.FailedOverBy
 	convertedNode.FailedOverBy = currentNode.FailedOverBy
 	convertedNode.FailOverPeers = currentNode.FailOverPeers
 	convertedNode.FailOverPeers = currentNode.FailOverPeers
-	convertedNode.IsEgressGateway = a.IsEgressGateway
 	convertedNode.IsIngressGateway = a.IsIngressGateway
 	convertedNode.IsIngressGateway = a.IsIngressGateway
-	// prevents user from changing ranges, must delete and recreate
-	convertedNode.EgressGatewayRanges = currentNode.EgressGatewayRanges
 	convertedNode.IngressGatewayRange = currentNode.IngressGatewayRange
 	convertedNode.IngressGatewayRange = currentNode.IngressGatewayRange
 	convertedNode.IngressGatewayRange6 = currentNode.IngressGatewayRange6
 	convertedNode.IngressGatewayRange6 = currentNode.IngressGatewayRange6
 	convertedNode.DNSOn = a.DNSOn
 	convertedNode.DNSOn = a.DNSOn
 	convertedNode.IngressDNS = a.IngressDns
 	convertedNode.IngressDNS = a.IngressDns
 	convertedNode.IngressPersistentKeepalive = a.IngressPersistentKeepalive
 	convertedNode.IngressPersistentKeepalive = a.IngressPersistentKeepalive
 	convertedNode.IngressMTU = a.IngressMTU
 	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.RelayedNodes = a.RelayedNodes
 	convertedNode.DefaultACL = a.DefaultACL
 	convertedNode.DefaultACL = a.DefaultACL
 	convertedNode.OwnerID = currentNode.OwnerID
 	convertedNode.OwnerID = currentNode.OwnerID
@@ -187,11 +182,7 @@ func (nm *Node) ConvertToAPINode() *ApiNode {
 	apiNode.IsRelay = nm.IsRelay
 	apiNode.IsRelay = nm.IsRelay
 	apiNode.RelayedBy = nm.RelayedBy
 	apiNode.RelayedBy = nm.RelayedBy
 	apiNode.RelayedNodes = nm.RelayedNodes
 	apiNode.RelayedNodes = nm.RelayedNodes
-	apiNode.IsEgressGateway = nm.IsEgressGateway
 	apiNode.IsIngressGateway = nm.IsIngressGateway
 	apiNode.IsIngressGateway = nm.IsIngressGateway
-	apiNode.EgressGatewayRanges = nm.EgressGatewayRanges
-	apiNode.EgressGatewayRangesWithMetric = nm.EgressGatewayRequest.RangesWithMetric
-	apiNode.EgressGatewayNatEnabled = nm.EgressGatewayNatEnabled
 	apiNode.DNSOn = nm.DNSOn
 	apiNode.DNSOn = nm.DNSOn
 	apiNode.IngressDns = nm.IngressDNS
 	apiNode.IngressDns = nm.IngressDNS
 	apiNode.IngressPersistentKeepalive = nm.IngressPersistentKeepalive
 	apiNode.IngressPersistentKeepalive = nm.IngressPersistentKeepalive
@@ -200,9 +191,9 @@ func (nm *Node) ConvertToAPINode() *ApiNode {
 	apiNode.Connected = nm.Connected
 	apiNode.Connected = nm.Connected
 	apiNode.PendingDelete = nm.PendingDelete
 	apiNode.PendingDelete = nm.PendingDelete
 	apiNode.DefaultACL = nm.DefaultACL
 	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.IsFailOver = nm.IsFailOver
 	apiNode.FailOverPeers = nm.FailOverPeers
 	apiNode.FailOverPeers = nm.FailOverPeers
 	apiNode.FailedOverBy = nm.FailedOverBy
 	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
 // FwUpdate - struct for firewall updates
 type FwUpdate struct {
 type FwUpdate struct {
 	AllowAll        bool                   `json:"allow_all"`
 	AllowAll        bool                   `json:"allow_all"`
-	AllowedNetworks []net.IPNet            `json:"networks"`
+	AllowedNetworks []AclRule              `json:"networks"`
 	IsEgressGw      bool                   `json:"is_egress_gw"`
 	IsEgressGw      bool                   `json:"is_egress_gw"`
 	IsIngressGw     bool                   `json:"is_ingress_gw"`
 	IsIngressGw     bool                   `json:"is_ingress_gw"`
 	EgressInfo      map[string]EgressInfo  `json:"egress_info"`
 	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"`
 	DefaultACL        string              `json:"defaultacl,omitempty"    bson:"defaultacl,omitempty"    yaml:"defaultacl,omitempty"    validate:"checkyesornoorunset"`
 	OwnerID           string              `json:"ownerid,omitempty"       bson:"ownerid,omitempty"       yaml:"ownerid,omitempty"`
 	OwnerID           string              `json:"ownerid,omitempty"       bson:"ownerid,omitempty"       yaml:"ownerid,omitempty"`
 	IsFailOver        bool                `json:"is_fail_over"                                           yaml:"is_fail_over"`
 	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"`
 	FailedOverBy      uuid.UUID           `json:"failed_over_by"                                         yaml:"failed_over_by"`
 	IsInternetGateway bool                `json:"isinternetgateway"                                      yaml:"isinternetgateway"`
 	IsInternetGateway bool                `json:"isinternetgateway"                                      yaml:"isinternetgateway"`
 	InetNodeReq       InetNodeReq         `json:"inet_node_req"                                          yaml:"inet_node_req"`
 	InetNodeReq       InetNodeReq         `json:"inet_node_req"                                          yaml:"inet_node_req"`
@@ -121,6 +121,16 @@ type Node struct {
 	StaticNode        ExtClient           `json:"static_node"`
 	StaticNode        ExtClient           `json:"static_node"`
 	Status            NodeStatus          `json:"node_status"`
 	Status            NodeStatus          `json:"node_status"`
 	Mutex             *sync.Mutex         `json:"-"`
 	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
 // LegacyNode - legacy struct for node model
@@ -377,17 +387,17 @@ func (node *LegacyNode) SetIsStaticDefault() {
 
 
 // Node.SetLastModified - set last modified initial time
 // Node.SetLastModified - set last modified initial time
 func (node *Node) SetLastModified() {
 func (node *Node) SetLastModified() {
-	node.LastModified = time.Now()
+	node.LastModified = time.Now().UTC()
 }
 }
 
 
 // Node.SetLastCheckIn - set checkin time of node
 // Node.SetLastCheckIn - set checkin time of node
 func (node *Node) SetLastCheckIn() {
 func (node *Node) SetLastCheckIn() {
-	node.LastCheckIn = time.Now()
+	node.LastCheckIn = time.Now().UTC()
 }
 }
 
 
 // Node.SetLastPeerUpdate - sets last peer update time
 // Node.SetLastPeerUpdate - sets last peer update time
 func (node *Node) SetLastPeerUpdate() {
 func (node *Node) SetLastPeerUpdate() {
-	node.LastPeerUpdate = time.Now()
+	node.LastPeerUpdate = time.Now().UTC()
 }
 }
 
 
 // Node.SetExpirationDateTime - sets node expiry time
 // Node.SetExpirationDateTime - sets node expiry time
@@ -442,15 +452,9 @@ func (newNode *Node) Fill(
 	if newNode.Network == "" {
 	if newNode.Network == "" {
 		newNode.Network = currentNode.Network
 		newNode.Network = currentNode.Network
 	}
 	}
-	if newNode.IsEgressGateway != currentNode.IsEgressGateway {
-		newNode.IsEgressGateway = currentNode.IsEgressGateway
-	}
 	if newNode.IsIngressGateway != currentNode.IsIngressGateway {
 	if newNode.IsIngressGateway != currentNode.IsIngressGateway {
 		newNode.IsIngressGateway = currentNode.IsIngressGateway
 		newNode.IsIngressGateway = currentNode.IsIngressGateway
 	}
 	}
-	if newNode.EgressGatewayRanges == nil {
-		newNode.EgressGatewayRanges = currentNode.EgressGatewayRanges
-	}
 	if newNode.IngressGatewayRange == "" {
 	if newNode.IngressGatewayRange == "" {
 		newNode.IngressGatewayRange = currentNode.IngressGatewayRange
 		newNode.IngressGatewayRange = currentNode.IngressGatewayRange
 	}
 	}
@@ -567,7 +571,6 @@ func (ln *LegacyNode) ConvertToNewNode() (*Host, *Node) {
 		}
 		}
 	}
 	}
 	node.Action = ln.Action
 	node.Action = ln.Action
-	node.IsEgressGateway = parseBool(ln.IsEgressGateway)
 	node.IsIngressGateway = parseBool(ln.IsIngressGateway)
 	node.IsIngressGateway = parseBool(ln.IsIngressGateway)
 	node.DNSOn = parseBool(ln.DNSOn)
 	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.IsRelay = formatBool(n.IsRelay)
 	//l.IsDocker = formatBool(n.IsDocker)
 	//l.IsDocker = formatBool(n.IsDocker)
 	//l.IsK8S = formatBool(n.IsK8S)
 	//l.IsK8S = formatBool(n.IsK8S)
-	l.IsEgressGateway = formatBool(n.IsEgressGateway)
 	l.IsIngressGateway = formatBool(n.IsIngressGateway)
 	l.IsIngressGateway = formatBool(n.IsIngressGateway)
 	//l.EgressGatewayRanges = n.EgressGatewayRanges
 	//l.EgressGatewayRanges = n.EgressGatewayRanges
 	//l.EgressGatewayNatEnabled = n.EgressGatewayNatEnabled
 	//l.EgressGatewayNatEnabled = n.EgressGatewayNatEnabled

+ 1 - 0
models/structs.go

@@ -156,6 +156,7 @@ type ExtPeersResponse struct {
 type EgressRangeMetric struct {
 type EgressRangeMetric struct {
 	Network     string `json:"network"`
 	Network     string `json:"network"`
 	RouteMetric uint32 `json:"route_metric"` // preffered range 1-999
 	RouteMetric uint32 `json:"route_metric"` // preffered range 1-999
+	Nat         bool   `json:"nat"`
 }
 }
 
 
 // EgressGatewayRequest - egress gateway request
 // 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)
 		logger.Log(1, "could not parse jwt for user", authRequest.UserName)
 		return
 		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)
 	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)
 	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)
 		logger.Log(1, "could not parse jwt for user", authRequest.UserName)
 		return
 		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)
 	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)
 	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)
 		handleOauthNotConfigured(w)
 		return
 		return
 	}
 	}
+
 	var inviteExists bool
 	var inviteExists bool
 	// check if invite exists for User
 	// check if invite exists for User
 	in, err := logic.GetUserInvite(content.Email)
 	in, err := logic.GetUserInvite(content.Email)
@@ -167,6 +168,23 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 		return
 		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)
 	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)
 	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())
 		logger.Log(1, "could not parse jwt for user", authRequest.UserName, jwtErr.Error())
 		return
 		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)
 	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)
 	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
 		return
 	}
 	}
+	logic.GetNodeEgressInfo(&node)
+	logic.GetNodeEgressInfo(&peerNode)
 	if peerNode.IsFailOver {
 	if peerNode.IsFailOver {
 		logic.ReturnErrorResponse(
 		logic.ReturnErrorResponse(
 			w,
 			w,
@@ -245,7 +247,7 @@ func failOverME(w http.ResponseWriter, r *http.Request) {
 		)
 		)
 		return
 		return
 	}
 	}
-	if node.IsInternetGateway && peerNode.InternetGwID == node.ID.String() {
+	if node.EgressDetails.IsInternetGateway && peerNode.EgressDetails.InternetGwID == node.ID.String() {
 		logic.ReturnErrorResponse(
 		logic.ReturnErrorResponse(
 			w,
 			w,
 			r,
 			r,
@@ -256,7 +258,7 @@ func failOverME(w http.ResponseWriter, r *http.Request) {
 		)
 		)
 		return
 		return
 	}
 	}
-	if node.InternetGwID != "" && node.InternetGwID == peerNode.ID.String() {
+	if node.EgressDetails.InternetGwID != "" && node.EgressDetails.InternetGwID == peerNode.ID.String() {
 		logic.ReturnErrorResponse(
 		logic.ReturnErrorResponse(
 			w,
 			w,
 			r,
 			r,
@@ -349,6 +351,8 @@ func checkfailOverCtx(w http.ResponseWriter, r *http.Request) {
 		)
 		)
 		return
 		return
 	}
 	}
+	logic.GetNodeEgressInfo(&node)
+	logic.GetNodeEgressInfo(&peerNode)
 	if peerNode.IsFailOver {
 	if peerNode.IsFailOver {
 		logic.ReturnErrorResponse(
 		logic.ReturnErrorResponse(
 			w,
 			w,
@@ -389,7 +393,18 @@ func checkfailOverCtx(w http.ResponseWriter, r *http.Request) {
 		)
 		)
 		return
 		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(
 		logic.ReturnErrorResponse(
 			w,
 			w,
 			r,
 			r,
@@ -400,7 +415,7 @@ func checkfailOverCtx(w http.ResponseWriter, r *http.Request) {
 		)
 		)
 		return
 		return
 	}
 	}
-	if node.InternetGwID != "" && node.InternetGwID == peerNode.ID.String() {
+	if node.EgressDetails.InternetGwID != "" && node.EgressDetails.InternetGwID == peerNode.ID.String() {
 		logic.ReturnErrorResponse(
 		logic.ReturnErrorResponse(
 			w,
 			w,
 			r,
 			r,
@@ -411,6 +426,17 @@ func checkfailOverCtx(w http.ResponseWriter, r *http.Request) {
 		)
 		)
 		return
 		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)
 	err = proLogic.CheckFailOverCtx(failOverNode, node, peerNode)
 	if err != nil {
 	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"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 		return
 	}
 	}
-	if node.IsInternetGateway {
+	if node.EgressDetails.IsInternetGateway {
 		logic.ReturnSuccessResponse(w, r, "node is already acting as internet gateway")
 		logic.ReturnSuccessResponse(w, r, "node is already acting as internet gateway")
 		return
 		return
 	}
 	}
@@ -132,7 +132,7 @@ func updateInternetGw(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 		return
 	}
 	}
-	if !node.IsInternetGateway {
+	if !node.EgressDetails.IsInternetGateway {
 		logic.ReturnErrorResponse(
 		logic.ReturnErrorResponse(
 			w,
 			w,
 			r,
 			r,

+ 178 - 5
pro/controllers/users.go

@@ -250,6 +250,21 @@ func inviteUsers(w http.ResponseWriter, r *http.Request) {
 		if err != nil {
 		if err != nil {
 			slog.Error("failed to insert invite for user", "email", invite.Email, "error", err)
 			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
 		// notify user with magic link
 		go func(invite models.UserInvite) {
 		go func(invite models.UserInvite) {
 			// Set E-Mail body. You can set plain text or html with text/html
 			// 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)
 		}(invite)
 	}
 	}
+
 	logic.ReturnSuccessResponse(w, r, "triggered user invites")
 	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"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		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")
 	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{}{}
 		user.UserGroups[userGroupReq.Group.ID] = struct{}{}
 		logic.UpsertUser(*user)
 		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")
 	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"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		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
 	// reset configs for service user
 	go proLogic.UpdatesUserGwAccessOnGrpUpdates(currUserG.NetworkRoles, userGroup.NetworkRoles)
 	go proLogic.UpdatesUserGwAccessOnGrpUpdates(currUserG.NetworkRoles, userGroup.NetworkRoles)
 	logic.ReturnSuccessResponseWithJson(w, r, userGroup, "updated user group")
 	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"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		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{}))
 	go proLogic.UpdatesUserGwAccessOnGrpUpdates(userG.NetworkRoles, make(map[models.NetworkID]map[models.UserRoleID]struct{}))
 	logic.ReturnSuccessResponseWithJson(w, r, nil, "deleted user group")
 	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"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		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")
 	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"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		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
 	// reset configs for service user
 	go proLogic.UpdatesUserGwAccessOnRoleUpdates(currRole.NetworkLevelAccess, userRole.NetworkLevelAccess, string(userRole.NetworkID))
 	go proLogic.UpdatesUserGwAccessOnRoleUpdates(currRole.NetworkLevelAccess, userRole.NetworkLevelAccess, string(userRole.NetworkID))
 	logic.ReturnSuccessResponseWithJson(w, r, userRole, "updated user role")
 	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"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		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())
 	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")
 	logic.ReturnSuccessResponseWithJson(w, r, nil, "deleted user role")
 }
 }
@@ -947,7 +1075,7 @@ func getUserRemoteAccessNetworkGateways(w http.ResponseWriter, r *http.Request)
 			GwID:              node.ID.String(),
 			GwID:              node.ID.String(),
 			GWName:            host.Name,
 			GWName:            host.Name,
 			Network:           node.Network,
 			Network:           node.Network,
-			IsInternetGateway: node.IsInternetGateway,
+			IsInternetGateway: node.EgressDetails.IsInternetGateway,
 			Metadata:          node.Metadata,
 			Metadata:          node.Metadata,
 		})
 		})
 
 
@@ -1074,7 +1202,7 @@ func getRemoteAccessGatewayConf(w http.ResponseWriter, r *http.Request) {
 		Network:           node.Network,
 		Network:           node.Network,
 		GwClient:          userConf,
 		GwClient:          userConf,
 		Connected:         true,
 		Connected:         true,
-		IsInternetGateway: node.IsInternetGateway,
+		IsInternetGateway: node.EgressDetails.IsInternetGateway,
 		GwPeerPublicKey:   host.PublicKey.String(),
 		GwPeerPublicKey:   host.PublicKey.String(),
 		GwListenPort:      logic.GetPeerListenPort(host),
 		GwListenPort:      logic.GetPeerListenPort(host),
 		Metadata:          node.Metadata,
 		Metadata:          node.Metadata,
@@ -1166,7 +1294,7 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 				Network:           node.Network,
 				Network:           node.Network,
 				GwClient:          extClient,
 				GwClient:          extClient,
 				Connected:         true,
 				Connected:         true,
-				IsInternetGateway: node.IsInternetGateway,
+				IsInternetGateway: node.EgressDetails.IsInternetGateway,
 				GwPeerPublicKey:   host.PublicKey.String(),
 				GwPeerPublicKey:   host.PublicKey.String(),
 				GwListenPort:      logic.GetPeerListenPort(host),
 				GwListenPort:      logic.GetPeerListenPort(host),
 				Metadata:          node.Metadata,
 				Metadata:          node.Metadata,
@@ -1210,7 +1338,7 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 			GwID:              node.ID.String(),
 			GwID:              node.ID.String(),
 			GWName:            host.Name,
 			GWName:            host.Name,
 			Network:           node.Network,
 			Network:           node.Network,
-			IsInternetGateway: node.IsInternetGateway,
+			IsInternetGateway: node.EgressDetails.IsInternetGateway,
 			GwPeerPublicKey:   host.PublicKey.String(),
 			GwPeerPublicKey:   host.PublicKey.String(),
 			GwListenPort:      logic.GetPeerListenPort(host),
 			GwListenPort:      logic.GetPeerListenPort(host),
 			Metadata:          node.Metadata,
 			Metadata:          node.Metadata,
@@ -1356,6 +1484,21 @@ func approvePendingUser(w http.ResponseWriter, r *http.Request) {
 			break
 			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)
 	logic.ReturnSuccessResponse(w, r, "approved "+username)
 }
 }
 
 
@@ -1387,6 +1530,21 @@ func deletePendingUser(w http.ResponseWriter, r *http.Request) {
 			break
 			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)
 	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"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to delete all pending users "+err.Error()), "internal"))
 		return
 		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")
 	logic.ReturnSuccessResponse(w, r, "cleared all pending users")
 }
 }
 
 

+ 4 - 0
pro/initialize.go

@@ -34,6 +34,7 @@ func InitPro() {
 		proControllers.FailOverHandlers,
 		proControllers.FailOverHandlers,
 		proControllers.InetHandlers,
 		proControllers.InetHandlers,
 		proControllers.RacHandlers,
 		proControllers.RacHandlers,
+		proControllers.EventHandlers,
 	)
 	)
 	controller.ListRoles = proControllers.ListRoles
 	controller.ListRoles = proControllers.ListRoles
 	logic.EnterpriseCheckFuncs = append(logic.EnterpriseCheckFuncs, func() {
 	logic.EnterpriseCheckFuncs = append(logic.EnterpriseCheckFuncs, func() {
@@ -94,6 +95,7 @@ func InitPro() {
 		proLogic.InitFailOverCache()
 		proLogic.InitFailOverCache()
 		auth.StartSyncHook()
 		auth.StartSyncHook()
 		email.Init()
 		email.Init()
+		proLogic.EventWatcher()
 	})
 	})
 	logic.ResetFailOver = proLogic.ResetFailOver
 	logic.ResetFailOver = proLogic.ResetFailOver
 	logic.ResetFailedOverPeer = proLogic.ResetFailedOverPeer
 	logic.ResetFailedOverPeer = proLogic.ResetFailedOverPeer
@@ -111,6 +113,7 @@ func InitPro() {
 	logic.DeleteMetrics = proLogic.DeleteMetrics
 	logic.DeleteMetrics = proLogic.DeleteMetrics
 	logic.GetTrialEndDate = getTrialEndDate
 	logic.GetTrialEndDate = getTrialEndDate
 	logic.SetDefaultGw = proLogic.SetDefaultGw
 	logic.SetDefaultGw = proLogic.SetDefaultGw
+	logic.ValidateInetGwReq = proLogic.ValidateInetGwReq
 	logic.SetDefaultGwForRelayedUpdate = proLogic.SetDefaultGwForRelayedUpdate
 	logic.SetDefaultGwForRelayedUpdate = proLogic.SetDefaultGwForRelayedUpdate
 	logic.UnsetInternetGw = proLogic.UnsetInternetGw
 	logic.UnsetInternetGw = proLogic.UnsetInternetGw
 	logic.SetInternetGw = proLogic.SetInternetGw
 	logic.SetInternetGw = proLogic.SetInternetGw
@@ -142,6 +145,7 @@ func InitPro() {
 	logic.ResetAuthProvider = auth.ResetAuthProvider
 	logic.ResetAuthProvider = auth.ResetAuthProvider
 	logic.ResetIDPSyncHook = auth.ResetIDPSyncHook
 	logic.ResetIDPSyncHook = auth.ResetIDPSyncHook
 	logic.EmailInit = email.Init
 	logic.EmailInit = email.Init
+	logic.LogEvent = proLogic.LogEvent
 }
 }
 
 
 func retrieveProLogo() string {
 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 {
 	for failOverpeerID := range node.FailOverPeers {
 		failOverpeer, err := logic.GetNodeByID(failOverpeerID)
 		failOverpeer, err := logic.GetNodeByID(failOverpeerID)
 		if err == nil && failOverpeer.FailedOverBy == peer.ID {
 		if err == nil && failOverpeer.FailedOverBy == peer.ID {
+			logic.GetNodeEgressInfo(&failOverpeer)
 			if failOverpeer.Address.IP != nil {
 			if failOverpeer.Address.IP != nil {
 				allowed := net.IPNet{
 				allowed := net.IPNet{
 					IP:   failOverpeer.Address.IP,
 					IP:   failOverpeer.Address.IP,
@@ -179,7 +180,7 @@ func GetFailOverPeerIps(peer, node *models.Node) []net.IPNet {
 				}
 				}
 				allowedips = append(allowedips, allowed)
 				allowedips = append(allowedips, allowed)
 			}
 			}
-			if failOverpeer.IsEgressGateway {
+			if failOverpeer.EgressDetails.IsEgressGateway {
 				allowedips = append(allowedips, logic.GetEgressIPs(&failOverpeer)...)
 				allowedips = append(allowedips, logic.GetEgressIPs(&failOverpeer)...)
 			}
 			}
 			if failOverpeer.IsRelay {
 			if failOverpeer.IsRelay {
@@ -199,7 +200,7 @@ func GetFailOverPeerIps(peer, node *models.Node) []net.IPNet {
 						}
 						}
 						allowedips = append(allowedips, allowed)
 						allowedips = append(allowedips, allowed)
 					}
 					}
-					if rNode.IsEgressGateway {
+					if rNode.EgressDetails.IsEgressGateway {
 						allowedips = append(allowedips, logic.GetEgressIPs(&rNode)...)
 						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 {
 	if inetHost.FirewallInUse == models.FIREWALL_NONE {
 		return errors.New("iptables or nftables needs to be installed")
 		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)
 		return fmt.Errorf("node %s is using a internet gateway already", inetHost.Name)
 	}
 	}
 	if inetNode.IsRelayed {
 	if inetNode.IsRelayed {
@@ -36,22 +36,28 @@ func ValidateInetGwReq(inetNode models.Node, req models.InetNodeReq, update bool
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
+		if clientNode.IsFailOver {
+			return errors.New("failover node cannot be set to use internet gateway")
+		}
 		clientHost, err := logic.GetHost(clientNode.HostID.String())
 		clientHost, err := logic.GetHost(clientNode.HostID.String())
 		if err != nil {
 		if err != nil {
 			return err
 			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 {
 		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")
 			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)
 			return fmt.Errorf("node %s acting as internet gateway cannot use another internet gateway", clientHost.Name)
 		}
 		}
 		if update {
 		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)
 				return fmt.Errorf("node %s is already using a internet gateway", clientHost.Name)
 			}
 			}
 		} else {
 		} else {
-			if clientNode.InternetGwID != "" {
+			if clientNode.EgressDetails.InternetGwID != "" {
 				return fmt.Errorf("node %s is already using a internet gateway", clientHost.Name)
 				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 {
 			if err != nil {
 				continue
 				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")
 				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
 // SetInternetGw - sets the node as internet gw based on flag bool
 func SetInternetGw(node *models.Node, req models.InetNodeReq) {
 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 {
 	for _, clientNodeID := range req.InetNodeClientIDs {
 		clientNode, err := logic.GetNodeByID(clientNodeID)
 		clientNode, err := logic.GetNodeByID(clientNodeID)
 		if err != nil {
 		if err != nil {
 			continue
 			continue
 		}
 		}
-		clientNode.InternetGwID = node.ID.String()
+		clientNode.EgressDetails.InternetGwID = node.ID.String()
 		logic.UpsertNode(&clientNode)
 		logic.UpsertNode(&clientNode)
 	}
 	}
 
 
@@ -99,19 +105,19 @@ func UnsetInternetGw(node *models.Node) {
 		return
 		return
 	}
 	}
 	for _, clientNode := range nodes {
 	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)
 			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 {
 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())
 		relayedHost, err := logic.GetHost(relayed.HostID.String())
 		if err != nil {
 		if err != nil {
 			return peerUpdate
 			return peerUpdate
@@ -127,9 +133,9 @@ func SetDefaultGwForRelayedUpdate(relayed, relay models.Node, peerUpdate models.
 }
 }
 
 
 func SetDefaultGw(node models.Node, peerUpdate models.HostPeerUpdate) models.HostPeerUpdate {
 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 {
 		if err != nil {
 			return peerUpdate
 			return peerUpdate
 		}
 		}

+ 2 - 20
pro/logic/user_mgmt.go

@@ -8,7 +8,6 @@ import (
 	"time"
 	"time"
 
 
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
-	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
 	"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) {
 func GetUserRAGNodes(user models.User) (gws map[string]models.Node) {
 	gws = make(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()
 	nodes, err := logic.GetAllNodes()
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
 	for _, node := range nodes {
 	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
 	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{} {
 func ListModels() []interface{} {
 	return []interface{}{
 	return []interface{}{
 		&Job{},
 		&Job{},
+		&Egress{},
 		&UserAccessToken{},
 		&UserAccessToken{},
+		&Event{},
 	}
 	}
 }
 }