瀏覽代碼

resolve merge conflicts

abhishek9686 3 月之前
父節點
當前提交
f8b16aeddf
共有 100 個文件被更改,包括 4873 次插入863 次删除
  1. 2 2
      .github/workflows/deletedroplets.yml
  2. 2 2
      Dockerfile
  3. 2 2
      Dockerfile-quick
  4. 3 3
      auth/host_session.go
  5. 43 0
      cli/cmd/access_token/create.go
  6. 23 0
      cli/cmd/access_token/delete.go
  7. 20 0
      cli/cmd/access_token/get.go
  8. 28 0
      cli/cmd/access_token/root.go
  9. 7 4
      cli/cmd/context/set.go
  10. 3 3
      cli/cmd/host/add_network.go
  11. 3 3
      cli/cmd/host/delete.go
  12. 3 3
      cli/cmd/host/delete_network.go
  13. 2 2
      cli/cmd/host/list.go
  14. 4 4
      cli/cmd/host/refresh_keys.go
  15. 4 3
      cli/cmd/host/root.go
  16. 11 11
      cli/cmd/host/update.go
  17. 26 3
      cli/cmd/node/list.go
  18. 4 1
      cli/cmd/root.go
  19. 58 0
      cli/functions/access_tokens.go
  20. 1 1
      cli/functions/http_client.go
  21. 0 1
      config/config.go
  22. 58 5
      controllers/acls.go
  23. 3 0
      controllers/controller.go
  24. 6 6
      controllers/dns.go
  25. 315 0
      controllers/egress.go
  26. 79 5
      controllers/enrollmentkeys.go
  27. 26 4
      controllers/ext_client.go
  28. 40 2
      controllers/gateway.go
  29. 145 9
      controllers/hosts.go
  30. 5 5
      controllers/migrate.go
  31. 34 5
      controllers/network.go
  32. 23 11
      controllers/node.go
  33. 0 94
      controllers/node_test.go
  34. 91 2
      controllers/server.go
  35. 54 2
      controllers/tags.go
  36. 424 39
      controllers/user.go
  37. 37 90
      database/database.go
  38. 2 2
      database/postgres.go
  39. 2 2
      database/rqlite.go
  40. 2 2
      database/sqlite.go
  41. 0 59
      database/statics.go
  42. 11 0
      database/utils.go
  43. 41 0
      db/connector.go
  44. 112 0
      db/db.go
  45. 117 0
      db/postgres.go
  46. 55 0
      db/sqlite.go
  47. 1 1
      docker/Dockerfile-go-builder
  48. 1 1
      docker/Dockerfile-netclient-multiarch
  49. 2 2
      docker/Dockerfile-netclient-multiarch-userspace
  50. 51 19
      go.mod
  51. 125 80
      go.sum
  52. 347 136
      logic/acls.go
  53. 58 8
      logic/auth.go
  54. 1 2
      logic/dns.go
  55. 366 0
      logic/egress.go
  56. 2 1
      logic/enrollmentkey.go
  57. 13 13
      logic/enrollmentkey_test.go
  58. 16 6
      logic/errors.go
  59. 49 5
      logic/extpeers.go
  60. 28 14
      logic/gateway.go
  61. 15 2
      logic/hosts.go
  62. 59 14
      logic/jwts.go
  63. 1 2
      logic/networks.go
  64. 39 13
      logic/nodes.go
  65. 51 44
      logic/peers.go
  66. 6 4
      logic/relay.go
  67. 14 0
      logic/security.go
  68. 355 0
      logic/settings.go
  69. 1 1
      logic/tags.go
  70. 8 2
      logic/telemetry.go
  71. 4 1
      logic/user_mgmt.go
  72. 27 9
      logic/users.go
  73. 21 0
      logic/util.go
  74. 0 14
      logic/wireguard.go
  75. 50 1
      main.go
  76. 236 4
      migrate/migrate.go
  77. 183 0
      migrate/migrate_schema.go
  78. 55 8
      models/accessToken.go
  79. 1 0
      models/acl.go
  80. 6 15
      models/api_node.go
  81. 14 0
      models/egress.go
  82. 2 0
      models/enrollment_key.go
  83. 78 0
      models/events.go
  84. 1 1
      models/mqtt.go
  85. 14 12
      models/node.go
  86. 46 0
      models/settings.go
  87. 2 0
      models/structs.go
  88. 32 14
      models/user_mgmt.go
  89. 1 1
      mq/publishers.go
  90. 15 3
      pro/auth/auth.go
  91. 27 4
      pro/auth/azure-ad.go
  92. 8 0
      pro/auth/error.go
  93. 26 3
      pro/auth/github.go
  94. 26 1
      pro/auth/google.go
  95. 3 1
      pro/auth/headless_callback.go
  96. 28 3
      pro/auth/oidc.go
  97. 281 0
      pro/auth/sync.go
  98. 114 0
      pro/controllers/events.go
  99. 30 4
      pro/controllers/failover.go
  100. 2 2
      pro/controllers/inet_gws.go

+ 2 - 2
.github/workflows/deletedroplets.yml

@@ -12,7 +12,7 @@ jobs:
     if: ${{ github.event.workflow_run.conclusion == 'success' }}
     if: ${{ github.event.workflow_run.conclusion == 'success' }}
     steps:
     steps:
       - name: get logs
       - name: get logs
-        uses: dawidd6/action-download-artifact@v8
+        uses: dawidd6/action-download-artifact@v9
         with:
         with:
           run_id: ${{ github.event.workflow_run.id}}
           run_id: ${{ github.event.workflow_run.id}}
           if_no_artifact_found: warn
           if_no_artifact_found: warn
@@ -75,7 +75,7 @@ jobs:
     if: ${{ github.event.workflow_run.conclusion == 'failure' }}
     if: ${{ github.event.workflow_run.conclusion == 'failure' }}
     steps:
     steps:
       - name: get logs
       - name: get logs
-        uses: dawidd6/action-download-artifact@v8
+        uses: dawidd6/action-download-artifact@v9
         with:
         with:
           run_id: ${{ github.event.workflow_run.id}}
           run_id: ${{ github.event.workflow_run.id}}
           if_no_artifact_found: warn
           if_no_artifact_found: warn

+ 2 - 2
Dockerfile

@@ -6,12 +6,12 @@ COPY . .
 
 
 RUN GOOS=linux CGO_ENABLED=1 go build -ldflags="-s -w " -tags ${tags} .
 RUN GOOS=linux CGO_ENABLED=1 go build -ldflags="-s -w " -tags ${tags} .
 # RUN go build -tags=ee . -o netmaker main.go
 # RUN go build -tags=ee . -o netmaker main.go
-FROM alpine:3.21.2
+FROM alpine:3.21.3
 
 
 # add a c lib
 # add a c lib
 # set the working directory
 # set the working directory
 WORKDIR /root/
 WORKDIR /root/
-RUN apk update && apk upgrade
+RUN apk upgrade --no-cache
 RUN apk add --no-cache sqlite
 RUN apk add --no-cache sqlite
 RUN mkdir -p /etc/netclient/config
 RUN mkdir -p /etc/netclient/config
 COPY --from=builder /app/netmaker .
 COPY --from=builder /app/netmaker .

+ 2 - 2
Dockerfile-quick

@@ -1,12 +1,12 @@
 #first stage - builder
 #first stage - builder
-FROM alpine:3.21.2
+FROM alpine:3.21.3
 ARG version 
 ARG version 
 WORKDIR /app
 WORKDIR /app
 COPY ./netmaker /root/netmaker
 COPY ./netmaker /root/netmaker
 ENV GO111MODULE=auto
 ENV GO111MODULE=auto
 
 
 # add a c lib
 # add a c lib
-RUN apk add gcompat iptables wireguard-tools
+RUN apk add --no-cache gcompat iptables wireguard-tools
 # set the working directory
 # set the working directory
 WORKDIR /root/
 WORKDIR /root/
 RUN mkdir -p /etc/netclient/config
 RUN mkdir -p /etc/netclient/config

+ 3 - 3
auth/host_session.go

@@ -68,7 +68,7 @@ func SessionHandler(conn *websocket.Conn) {
 	if len(registerMessage.User) > 0 { // handle basic auth
 	if len(registerMessage.User) > 0 { // handle basic auth
 		logger.Log(0, "user registration attempted with host:", registerMessage.RegisterHost.Name, "user:", registerMessage.User)
 		logger.Log(0, "user registration attempted with host:", registerMessage.RegisterHost.Name, "user:", registerMessage.User)
 
 
-		if !servercfg.IsBasicAuthEnabled() {
+		if !logic.IsBasicAuthEnabled() {
 			err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
 			err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
 			if err != nil {
 			if err != nil {
 				logger.Log(0, "error during message writing:", err.Error())
 				logger.Log(0, "error during message writing:", err.Error())
@@ -166,7 +166,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
@@ -208,7 +208,7 @@ func SessionHandler(conn *websocket.Conn) {
 				netsToAdd = append(netsToAdd, newNet)
 				netsToAdd = append(netsToAdd, newNet)
 			}
 			}
 		}
 		}
-		server := servercfg.GetServerInfo()
+		server := logic.GetServerInfo()
 		server.TrafficKey = key
 		server.TrafficKey = key
 		result.Host.HostPass = ""
 		result.Host.HostPass = ""
 		response := models.RegisterResponse{
 		response := models.RegisterResponse{

+ 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")

+ 3 - 3
cli/cmd/host/add_network.go

@@ -6,10 +6,10 @@ import (
 )
 )
 
 
 var addHostNetworkCmd = &cobra.Command{
 var addHostNetworkCmd = &cobra.Command{
-	Use:   "add_network HostID Network",
+	Use:   "add_network DeviceID/HostID Network",
 	Args:  cobra.ExactArgs(2),
 	Args:  cobra.ExactArgs(2),
-	Short: "Add a network to a host",
-	Long:  `Add a network to a host`,
+	Short: "Add a device to a network",
+	Long:  `Add a device to a network`,
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		functions.PrettyPrint(functions.AddHostToNetwork(args[0], args[1]))
 		functions.PrettyPrint(functions.AddHostToNetwork(args[0], args[1]))
 	},
 	},

+ 3 - 3
cli/cmd/host/delete.go

@@ -8,10 +8,10 @@ import (
 var force bool
 var force bool
 
 
 var hostDeleteCmd = &cobra.Command{
 var hostDeleteCmd = &cobra.Command{
-	Use:   "delete HostID",
+	Use:   "delete DeviceID/HostID",
 	Args:  cobra.ExactArgs(1),
 	Args:  cobra.ExactArgs(1),
-	Short: "Delete a host",
-	Long:  `Delete a host`,
+	Short: "Delete a device",
+	Long:  `Delete a device`,
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		functions.PrettyPrint(functions.DeleteHost(args[0], force))
 		functions.PrettyPrint(functions.DeleteHost(args[0], force))
 	},
 	},

+ 3 - 3
cli/cmd/host/delete_network.go

@@ -6,10 +6,10 @@ import (
 )
 )
 
 
 var deleteHostNetworkCmd = &cobra.Command{
 var deleteHostNetworkCmd = &cobra.Command{
-	Use:   "delete_network HostID Network",
+	Use:   "delete_network DeviceID/HostID Network",
 	Args:  cobra.ExactArgs(2),
 	Args:  cobra.ExactArgs(2),
-	Short: "Delete a network from a host",
-	Long:  `Delete a network from a host`,
+	Short: "Remove a device from a network",
+	Long:  `Remove a device from a network`,
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		functions.PrettyPrint(functions.DeleteHostFromNetwork(args[0], args[1]))
 		functions.PrettyPrint(functions.DeleteHostFromNetwork(args[0], args[1]))
 	},
 	},

+ 2 - 2
cli/cmd/host/list.go

@@ -8,8 +8,8 @@ import (
 var hostListCmd = &cobra.Command{
 var hostListCmd = &cobra.Command{
 	Use:   "list",
 	Use:   "list",
 	Args:  cobra.NoArgs,
 	Args:  cobra.NoArgs,
-	Short: "List all hosts",
-	Long:  `List all hosts`,
+	Short: "List all devices",
+	Long:  `List all devices`,
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		functions.PrettyPrint(functions.GetHosts())
 		functions.PrettyPrint(functions.GetHosts())
 	},
 	},

+ 4 - 4
cli/cmd/host/refresh_keys.go

@@ -6,11 +6,11 @@ import (
 )
 )
 
 
 var hostRefreshKeysCmd = &cobra.Command{
 var hostRefreshKeysCmd = &cobra.Command{
-	Use:   "refresh_keys [HOST ID] ",
+	Use:   "refresh_keys [DEVICE ID/HOST ID]",
 	Args:  cobra.MaximumNArgs(1),
 	Args:  cobra.MaximumNArgs(1),
-	Short: "Refresh wireguard keys on host",
-	Long: `Refresh wireguard keys on specified or all hosts
-	If HOSTID is not specified, all hosts will be updated`,
+	Short: "Refresh wireguard keys on device",
+	Long: `Refresh wireguard keys on specified or all devices
+	If DEVICE ID/HOST ID is not specified, all devices will be updated`,
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		functions.PrettyPrint(functions.RefreshKeys(args[0]))
 		functions.PrettyPrint(functions.RefreshKeys(args[0]))
 	},
 	},

+ 4 - 3
cli/cmd/host/root.go

@@ -8,9 +8,10 @@ import (
 
 
 // rootCmd represents the base command when called without any subcommands
 // rootCmd represents the base command when called without any subcommands
 var rootCmd = &cobra.Command{
 var rootCmd = &cobra.Command{
-	Use:   "host",
-	Short: "Manage hosts",
-	Long:  `Manage hosts`,
+	Use:     "device",
+	Aliases: []string{"host"},
+	Short:   "Manage devices",
+	Long:    `Manage devices`,
 }
 }
 
 
 // GetRoot returns the root subcommand
 // GetRoot returns the root subcommand

+ 11 - 11
cli/cmd/host/update.go

@@ -25,10 +25,10 @@ var (
 )
 )
 
 
 var hostUpdateCmd = &cobra.Command{
 var hostUpdateCmd = &cobra.Command{
-	Use:   "update HostID",
+	Use:   "update DeviceID/HostID",
 	Args:  cobra.ExactArgs(1),
 	Args:  cobra.ExactArgs(1),
-	Short: "Update a host",
-	Long:  `Update a host`,
+	Short: "Update a device",
+	Long:  `Update a device`,
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		apiHost := &models.ApiHost{}
 		apiHost := &models.ApiHost{}
 		if apiHostFilePath != "" {
 		if apiHostFilePath != "" {
@@ -57,14 +57,14 @@ var hostUpdateCmd = &cobra.Command{
 
 
 func init() {
 func init() {
 	hostUpdateCmd.Flags().StringVar(&apiHostFilePath, "file", "", "Path to host_definition.json")
 	hostUpdateCmd.Flags().StringVar(&apiHostFilePath, "file", "", "Path to host_definition.json")
-	hostUpdateCmd.Flags().StringVar(&endpoint, "endpoint", "", "Endpoint of the Host")
-	hostUpdateCmd.Flags().StringVar(&endpoint6, "endpoint6", "", "IPv6 Endpoint of the Host")
-	hostUpdateCmd.Flags().StringVar(&name, "name", "", "Host name")
-	hostUpdateCmd.Flags().IntVar(&listenPort, "listen_port", 0, "Listen port of the host")
-	hostUpdateCmd.Flags().IntVar(&mtu, "mtu", 0, "Host MTU size")
+	hostUpdateCmd.Flags().StringVar(&endpoint, "endpoint", "", "Endpoint of the Device")
+	hostUpdateCmd.Flags().StringVar(&endpoint6, "endpoint6", "", "IPv6 Endpoint of the Device")
+	hostUpdateCmd.Flags().StringVar(&name, "name", "", "Device name")
+	hostUpdateCmd.Flags().IntVar(&listenPort, "listen_port", 0, "Listen port of the device")
+	hostUpdateCmd.Flags().IntVar(&mtu, "mtu", 0, "Device MTU size")
 	hostUpdateCmd.Flags().IntVar(&keepAlive, "keep_alive", 0, "Interval (seconds) in which packets are sent to keep connections open with peers")
 	hostUpdateCmd.Flags().IntVar(&keepAlive, "keep_alive", 0, "Interval (seconds) in which packets are sent to keep connections open with peers")
-	hostUpdateCmd.Flags().BoolVar(&isStaticPort, "static_port", false, "Make Host Static Port?")
-	hostUpdateCmd.Flags().BoolVar(&isStatic, "static_endpoint", false, "Make Host Static Endpoint?")
-	hostUpdateCmd.Flags().BoolVar(&isDefault, "default", false, "Make Host Default ?")
+	hostUpdateCmd.Flags().BoolVar(&isStaticPort, "static_port", false, "Make Device Static Port?")
+	hostUpdateCmd.Flags().BoolVar(&isStatic, "static_endpoint", false, "Make Device Static Endpoint?")
+	hostUpdateCmd.Flags().BoolVar(&isDefault, "default", false, "Make Device Default ?")
 	rootCmd.AddCommand(hostUpdateCmd)
 	rootCmd.AddCommand(hostUpdateCmd)
 }
 }

+ 26 - 3
cli/cmd/node/list.go

@@ -29,7 +29,7 @@ var nodeListCmd = &cobra.Command{
 			functions.PrettyPrint(data)
 			functions.PrettyPrint(data)
 		default:
 		default:
 			table := tablewriter.NewWriter(os.Stdout)
 			table := tablewriter.NewWriter(os.Stdout)
-			table.SetHeader([]string{"ID", "Addresses", "Network", "Egress", "Remote Access Gateway", "Relay"})
+			table.SetHeader([]string{"ID", "Addresses", "Network", "Egress", "Remote Access Gateway", "Relay", "Type"})
 			for _, d := range data {
 			for _, d := range data {
 				addresses := ""
 				addresses := ""
 				if d.Address != "" {
 				if d.Address != "" {
@@ -41,8 +41,31 @@ var nodeListCmd = &cobra.Command{
 					}
 					}
 					addresses += d.Address6
 					addresses += d.Address6
 				}
 				}
-				table.Append([]string{d.ID, addresses, d.Network,
-					strconv.FormatBool(d.IsEgressGateway), strconv.FormatBool(d.IsIngressGateway), strconv.FormatBool(d.IsRelay)})
+				network := d.Network
+				id := d.ID
+				nodeType := "Device"
+
+				if d.IsStatic {
+					id = d.StaticNode.ClientID
+					nodeType = "Static"
+				}
+				if d.IsUserNode {
+					id = d.StaticNode.OwnerID
+					nodeType = "User"
+				}
+				if d.IsStatic || d.IsUserNode {
+					addresses = d.StaticNode.Address
+					if d.StaticNode.Address6 != "" {
+						if addresses != "" {
+							addresses += ", "
+						}
+						addresses += d.StaticNode.Address6
+					}
+					network = d.StaticNode.Network
+				}
+
+				table.Append([]string{id, addresses, network,
+					strconv.FormatBool(d.IsEgressGateway), strconv.FormatBool(d.IsIngressGateway), strconv.FormatBool(d.IsRelay), nodeType})
 			}
 			}
 			table.Render()
 			table.Render()
 		}
 		}

+ 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

+ 0 - 1
config/config.go

@@ -75,7 +75,6 @@ type ServerConfig struct {
 	NetmakerTenantID           string        `yaml:"netmaker_tenant_id"`
 	NetmakerTenantID           string        `yaml:"netmaker_tenant_id"`
 	IsPro                      string        `yaml:"is_ee" json:"IsEE"`
 	IsPro                      string        `yaml:"is_ee" json:"IsEE"`
 	StunPort                   int           `yaml:"stun_port"`
 	StunPort                   int           `yaml:"stun_port"`
-	StunList                   string        `yaml:"stun_list"`
 	TurnServer                 string        `yaml:"turn_server"`
 	TurnServer                 string        `yaml:"turn_server"`
 	TurnApiServer              string        `yaml:"turn_api_server"`
 	TurnApiServer              string        `yaml:"turn_api_server"`
 	TurnPort                   int           `yaml:"turn_port"`
 	TurnPort                   int           `yaml:"turn_port"`

+ 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)
 }
 }

+ 3 - 0
controllers/controller.go

@@ -3,6 +3,7 @@ package controller
 import (
 import (
 	"context"
 	"context"
 	"fmt"
 	"fmt"
+	"github.com/gravitl/netmaker/db"
 	"net/http"
 	"net/http"
 	"os"
 	"os"
 	"strings"
 	"strings"
@@ -18,6 +19,7 @@ import (
 
 
 // HttpMiddlewares - middleware functions for REST interactions
 // HttpMiddlewares - middleware functions for REST interactions
 var HttpMiddlewares = []mux.MiddlewareFunc{
 var HttpMiddlewares = []mux.MiddlewareFunc{
+	db.Middleware,
 	userMiddleWare,
 	userMiddleWare,
 }
 }
 
 
@@ -37,6 +39,7 @@ var HttpHandlers = []interface{}{
 	enrollmentKeyHandlers,
 	enrollmentKeyHandlers,
 	tagHandlers,
 	tagHandlers,
 	aclHandlers,
 	aclHandlers,
+	egressHandlers,
 	legacyHandlers,
 	legacyHandlers,
 }
 }
 
 

+ 6 - 6
controllers/dns.go

@@ -164,9 +164,9 @@ func createDNS(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 	// check if default domain is appended if not append
 	// check if default domain is appended if not append
-	if servercfg.GetDefaultDomain() != "" &&
-		!strings.HasSuffix(entry.Name, servercfg.GetDefaultDomain()) {
-		entry.Name += "." + servercfg.GetDefaultDomain()
+	if logic.GetDefaultDomain() != "" &&
+		!strings.HasSuffix(entry.Name, logic.GetDefaultDomain()) {
+		entry.Name += "." + logic.GetDefaultDomain()
 	}
 	}
 	entry, err = logic.CreateDNS(entry)
 	entry, err = logic.CreateDNS(entry)
 	if err != nil {
 	if err != nil {
@@ -185,7 +185,7 @@ func createDNS(w http.ResponseWriter, r *http.Request) {
 		}
 		}
 	}
 	}
 
 
-	if servercfg.GetManageDNS() {
+	if logic.GetManageDNS() {
 		mq.SendDNSSyncByNetwork(netID)
 		mq.SendDNSSyncByNetwork(netID)
 	}
 	}
 
 
@@ -230,7 +230,7 @@ func deleteDNS(w http.ResponseWriter, r *http.Request) {
 		}
 		}
 	}
 	}
 
 
-	if servercfg.GetManageDNS() {
+	if logic.GetManageDNS() {
 		mq.SendDNSSyncByNetwork(netID)
 		mq.SendDNSSyncByNetwork(netID)
 	}
 	}
 
 
@@ -293,7 +293,7 @@ func pushDNS(w http.ResponseWriter, r *http.Request) {
 func syncDNS(w http.ResponseWriter, r *http.Request) {
 func syncDNS(w http.ResponseWriter, r *http.Request) {
 	// Set header
 	// Set header
 	w.Header().Set("Content-Type", "application/json")
 	w.Header().Set("Content-Type", "application/json")
-	if !servercfg.GetManageDNS() {
+	if !logic.GetManageDNS() {
 		logic.ReturnErrorResponse(
 		logic.ReturnErrorResponse(
 			w,
 			w,
 			r,
 			r,

+ 315 - 0
controllers/egress.go

@@ -0,0 +1,315 @@
+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.Get(db.WithContext(r.Context()))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
+		return
+	}
+	err = e.Delete(db.WithContext(r.Context()))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.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")
+}

+ 79 - 5
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)
 }
 }
@@ -160,6 +180,7 @@ func createEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 		enrollmentKeyBody.Unlimited,
 		enrollmentKeyBody.Unlimited,
 		relayId,
 		relayId,
 		false,
 		false,
+		enrollmentKeyBody.AutoEgress,
 	)
 	)
 	if err != nil {
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "failed to create enrollment key:", err.Error())
 		logger.Log(0, r.Header.Get("user"), "failed to create enrollment key:", err.Error())
@@ -172,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)
@@ -207,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 {
@@ -220,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)
@@ -285,7 +340,7 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 	key, keyErr := logic.RetrievePublicTrafficKey()
 	key, keyErr := logic.RetrievePublicTrafficKey()
 	if keyErr != nil {
 	if keyErr != nil {
 		logger.Log(0, "error retrieving key:", keyErr.Error())
 		logger.Log(0, "error retrieving key:", keyErr.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(keyErr, "internal"))
 		return
 		return
 	}
 	}
 	// use the token
 	// use the token
@@ -301,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 {
@@ -348,12 +403,31 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 		}
 		}
 	}
 	}
 	// ready the response
 	// ready the response
-	server := servercfg.GetServerInfo()
+	server := logic.GetServerInfo()
 	server.TrafficKey = key
 	server.TrafficKey = key
 	response := models.RegisterResponse{
 	response := models.RegisterResponse{
 		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)

+ 145 - 9
controllers/hosts.go

@@ -96,7 +96,21 @@ func upgradeHosts(w http.ResponseWriter, r *http.Request) {
 			}(host)
 			}(host)
 		}
 		}
 	}()
 	}()
-
+	logic.LogEvent(&models.Event{
+		Action: models.UpgradeAll,
+		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:   "All Hosts",
+			Name: "All Hosts",
+			Type: models.DeviceSub,
+		},
+		Origin: models.Dashboard,
+	})
 	slog.Info("upgrade all hosts request received", "user", user)
 	slog.Info("upgrade all hosts request received", "user", user)
 	logic.ReturnSuccessResponse(w, r, "upgrade all hosts request received")
 	logic.ReturnSuccessResponse(w, r, "upgrade all hosts request received")
 }
 }
@@ -209,14 +223,14 @@ func pull(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
-	serverConf := servercfg.GetServerInfo()
+	serverConf := logic.GetServerInfo()
 	key, keyErr := logic.RetrievePublicTrafficKey()
 	key, keyErr := logic.RetrievePublicTrafficKey()
 	if keyErr != nil {
 	if keyErr != nil {
 		logger.Log(0, "error retrieving key:", keyErr.Error())
 		logger.Log(0, "error retrieving key:", keyErr.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "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,
@@ -230,7 +244,7 @@ func pull(w http.ResponseWriter, r *http.Request) {
 		ChangeDefaultGw:   hPU.ChangeDefaultGw,
 		ChangeDefaultGw:   hPU.ChangeDefaultGw,
 		DefaultGwIp:       hPU.DefaultGwIp,
 		DefaultGwIp:       hPU.DefaultGwIp,
 		IsInternetGw:      hPU.IsInternetGw,
 		IsInternetGw:      hPU.IsInternetGw,
-		EndpointDetection: servercfg.IsEndpointDetectionEnabled(),
+		EndpointDetection: logic.IsEndpointDetectionEnabled(),
 	}
 	}
 
 
 	logger.Log(1, hostID, "completed a pull")
 	logger.Log(1, hostID, "completed a pull")
@@ -294,7 +308,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 +452,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 +538,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 +685,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"),
@@ -828,6 +906,21 @@ func updateAllKeys(w http.ResponseWriter, r *http.Request) {
 			}
 			}
 		}
 		}
 	}()
 	}()
+	logic.LogEvent(&models.Event{
+		Action: models.RefreshAllKeys,
+		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:   "All Devices",
+			Name: "All Devices",
+			Type: models.DeviceSub,
+		},
+		Origin: models.Dashboard,
+	})
 	logger.Log(2, r.Header.Get("user"), "updated keys for all hosts")
 	logger.Log(2, r.Header.Get("user"), "updated keys for all hosts")
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 }
 }
@@ -863,6 +956,21 @@ func updateKeys(w http.ResponseWriter, r *http.Request) {
 			logger.Log(0, "failed to send host key update", host.ID.String(), err.Error())
 			logger.Log(0, "failed to send host key update", host.ID.String(), err.Error())
 		}
 		}
 	}()
 	}()
+	logic.LogEvent(&models.Event{
+		Action: models.RefreshKey,
+		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,
+	})
 	logger.Log(2, r.Header.Get("user"), "updated key on host", host.Name)
 	logger.Log(2, r.Header.Get("user"), "updated key on host", host.Name)
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 }
 }
@@ -901,7 +1009,21 @@ func syncHosts(w http.ResponseWriter, r *http.Request) {
 			time.Sleep(time.Millisecond * 100)
 			time.Sleep(time.Millisecond * 100)
 		}
 		}
 	}()
 	}()
-
+	logic.LogEvent(&models.Event{
+		Action: models.SyncAll,
+		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:   "All Devices",
+			Name: "All Devices",
+			Type: models.DeviceSub,
+		},
+		Origin: models.Dashboard,
+	})
 	slog.Info("sync all hosts request received", "user", user)
 	slog.Info("sync all hosts request received", "user", user)
 	logic.ReturnSuccessResponse(w, r, "sync all hosts request received")
 	logic.ReturnSuccessResponse(w, r, "sync all hosts request received")
 }
 }
@@ -937,7 +1059,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)
 }
 }

+ 5 - 5
controllers/migrate.go

@@ -70,11 +70,11 @@ func migrate(w http.ResponseWriter, r *http.Request) {
 				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 				return
 				return
 			}
 			}
-			server = servercfg.GetServerInfo()
+			server = logic.GetServerInfo()
 			key, keyErr := logic.RetrievePublicTrafficKey()
 			key, keyErr := logic.RetrievePublicTrafficKey()
 			if keyErr != nil {
 			if keyErr != nil {
-				slog.Error("retrieving traffickey", "error", err)
-				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+				slog.Error("retrieving traffickey", "error", keyErr)
+				logic.ReturnErrorResponse(w, r, logic.FormatError(keyErr, "internal"))
 				return
 				return
 			}
 			}
 			server.TrafficKey = key
 			server.TrafficKey = key
@@ -134,7 +134,7 @@ func convertLegacyHostNode(legacy models.LegacyNode) (models.Host, models.Node)
 	host := models.Host{}
 	host := models.Host{}
 	host.ID = uuid.New()
 	host.ID = uuid.New()
 	host.IPForwarding = models.ParseBool(legacy.IPForwarding)
 	host.IPForwarding = models.ParseBool(legacy.IPForwarding)
-	host.AutoUpdate = servercfg.AutoUpdateEnabled()
+	host.AutoUpdate = logic.AutoUpdateEnabled()
 	host.Interface = "netmaker"
 	host.Interface = "netmaker"
 	host.ListenPort = int(legacy.ListenPort)
 	host.ListenPort = int(legacy.ListenPort)
 	if host.ListenPort == 0 {
 	if host.ListenPort == 0 {
@@ -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

+ 34 - 5
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")
@@ -588,8 +603,7 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
 	logic.CreateDefaultNetworkRolesAndGroups(models.NetworkID(network.NetID))
 	logic.CreateDefaultNetworkRolesAndGroups(models.NetworkID(network.NetID))
 	logic.CreateDefaultAclNetworkPolicies(models.NetworkID(network.NetID))
 	logic.CreateDefaultAclNetworkPolicies(models.NetworkID(network.NetID))
 	logic.CreateDefaultTags(models.NetworkID(network.NetID))
 	logic.CreateDefaultTags(models.NetworkID(network.NetID))
-
-	go logic.AddNetworkToAllocatedIpMap(network.NetID)
+	logic.AddNetworkToAllocatedIpMap(network.NetID)
 
 
 	go func() {
 	go func() {
 		defaultHosts := logic.GetDefaultHosts()
 		defaultHosts := logic.GetDefaultHosts()
@@ -637,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)

+ 23 - 11
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
@@ -477,7 +477,7 @@ func getNode(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
-	server := servercfg.GetServerInfo()
+	server := logic.GetServerInfo()
 	response := models.NodeGet{
 	response := models.NodeGet{
 		Node:         node,
 		Node:         node,
 		Host:         *host,
 		Host:         *host,
@@ -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()

+ 91 - 2
controllers/server.go

@@ -2,6 +2,7 @@ package controller
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
+	"errors"
 	"net/http"
 	"net/http"
 	"os"
 	"os"
 	"strings"
 	"strings"
@@ -12,6 +13,7 @@ import (
 	"golang.org/x/exp/slog"
 	"golang.org/x/exp/slog"
 
 
 	"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"
@@ -41,6 +43,10 @@ func serverHandlers(r *mux.Router) {
 	).Methods(http.MethodPost)
 	).Methods(http.MethodPost)
 	r.HandleFunc("/api/server/getconfig", allowUsers(http.HandlerFunc(getConfig))).
 	r.HandleFunc("/api/server/getconfig", allowUsers(http.HandlerFunc(getConfig))).
 		Methods(http.MethodGet)
 		Methods(http.MethodGet)
+	r.HandleFunc("/api/server/settings", allowUsers(http.HandlerFunc(getSettings))).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/server/settings", logic.SecurityCheck(true, http.HandlerFunc(updateSettings))).
+		Methods(http.MethodPut)
 	r.HandleFunc("/api/server/getserverinfo", logic.SecurityCheck(true, http.HandlerFunc(getServerInfo))).
 	r.HandleFunc("/api/server/getserverinfo", logic.SecurityCheck(true, http.HandlerFunc(getServerInfo))).
 		Methods(http.MethodGet)
 		Methods(http.MethodGet)
 	r.HandleFunc("/api/server/status", getStatus).Methods(http.MethodGet)
 	r.HandleFunc("/api/server/status", getStatus).Methods(http.MethodGet)
@@ -207,7 +213,7 @@ func getServerInfo(w http.ResponseWriter, r *http.Request) {
 
 
 	// get params
 	// get params
 
 
-	json.NewEncoder(w).Encode(servercfg.GetServerInfo())
+	json.NewEncoder(w).Encode(logic.GetServerInfo())
 	// w.WriteHeader(http.StatusOK)
 	// w.WriteHeader(http.StatusOK)
 }
 }
 
 
@@ -222,7 +228,7 @@ func getConfig(w http.ResponseWriter, r *http.Request) {
 
 
 	// get params
 	// get params
 
 
-	scfg := servercfg.GetServerConfig()
+	scfg := logic.GetServerConfig()
 	scfg.IsPro = "no"
 	scfg.IsPro = "no"
 	if servercfg.IsPro {
 	if servercfg.IsPro {
 		scfg.IsPro = "yes"
 		scfg.IsPro = "yes"
@@ -230,3 +236,86 @@ func getConfig(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(scfg)
 	json.NewEncoder(w).Encode(scfg)
 	// w.WriteHeader(http.StatusOK)
 	// w.WriteHeader(http.StatusOK)
 }
 }
+
+// @Summary     Get the server settings
+// @Router      /api/server/settings [get]
+// @Tags        Server
+// @Security    oauth2
+// @Success     200 {object} config.ServerSettings
+func getSettings(w http.ResponseWriter, r *http.Request) {
+	scfg := logic.GetServerSettings()
+	scfg.ClientSecret = logic.Mask()
+	logic.ReturnSuccessResponseWithJson(w, r, scfg, "fetched server settings successfully")
+}
+
+// @Summary     Update the server settings
+// @Router      /api/server/settings [put]
+// @Tags        Server
+// @Security    oauth2
+// @Success     200 {object} config.ServerSettings
+func updateSettings(w http.ResponseWriter, r *http.Request) {
+	var req models.ServerSettings
+	force := r.URL.Query().Get("force")
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		logger.Log(0, r.Header.Get("user"), "error decoding request body: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	if !logic.ValidateNewSettings(req) {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("invalid settings"), "badrequest"))
+		return
+	}
+	currSettings := logic.GetServerSettings()
+	err := logic.UpsertServerSettings(req)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to udpate server settings "+err.Error()), "internal"))
+		return
+	}
+	logic.LogEvent(&models.Event{
+		Action: models.Update,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   models.SettingSub.String(),
+			Name: models.SettingSub.String(),
+			Type: models.SettingSub,
+		},
+		Diff: models.Diff{
+			Old: currSettings,
+			New: req,
+		},
+		Origin: models.Dashboard,
+	})
+	go reInit(currSettings, req, force == "true")
+	logic.ReturnSuccessResponseWithJson(w, r, req, "updated server settings successfully")
+}
+
+func reInit(curr, new models.ServerSettings, force bool) {
+	logic.SettingsMutex.Lock()
+	defer logic.SettingsMutex.Unlock()
+	logic.ResetAuthProvider()
+	logic.EmailInit()
+	logic.SetVerbosity(int(logic.GetServerSettings().Verbosity))
+	logic.ResetIDPSyncHook()
+	// check if auto update is changed
+	if force {
+		if curr.NetclientAutoUpdate != new.NetclientAutoUpdate {
+			// update all hosts
+			hosts, _ := logic.GetAllHosts()
+			for _, host := range hosts {
+				host.AutoUpdate = new.NetclientAutoUpdate
+				logic.UpsertHost(&host)
+				mq.HostUpdate(&models.HostUpdate{
+					Action: models.UpdateHost,
+					Host:   host,
+				})
+			}
+		}
+	}
+	go mq.PublishPeerUpdate(false)
+
+}

+ 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)
 }
 }

+ 424 - 39
controllers/user.go

@@ -6,7 +6,9 @@ import (
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
 	"reflect"
 	"reflect"
+	"time"
 
 
+	"github.com/google/uuid"
 	"github.com/gorilla/mux"
 	"github.com/gorilla/mux"
 	"github.com/gorilla/websocket"
 	"github.com/gorilla/websocket"
 	"github.com/gravitl/netmaker/auth"
 	"github.com/gravitl/netmaker/auth"
@@ -14,6 +16,7 @@ import (
 	"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"
+	"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"
 )
 )
@@ -34,14 +37,19 @@ func userHandlers(r *mux.Router) {
 	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceUsers, http.HandlerFunc(createUser)))).Methods(http.MethodPost)
 	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceUsers, http.HandlerFunc(createUser)))).Methods(http.MethodPost)
 	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, http.HandlerFunc(deleteUser))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, http.HandlerFunc(deleteUser))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUser)))).Methods(http.MethodGet)
 	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUser)))).Methods(http.MethodGet)
+	r.HandleFunc("/api/users/{username}/enable", logic.SecurityCheck(true, http.HandlerFunc(enableUserAccount))).Methods(http.MethodPost)
+	r.HandleFunc("/api/users/{username}/disable", logic.SecurityCheck(true, http.HandlerFunc(disableUserAccount))).Methods(http.MethodPost)
 	r.HandleFunc("/api/v1/users", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUserV1)))).Methods(http.MethodGet)
 	r.HandleFunc("/api/v1/users", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUserV1)))).Methods(http.MethodGet)
 	r.HandleFunc("/api/users", logic.SecurityCheck(true, http.HandlerFunc(getUsers))).Methods(http.MethodGet)
 	r.HandleFunc("/api/users", logic.SecurityCheck(true, http.HandlerFunc(getUsers))).Methods(http.MethodGet)
 	r.HandleFunc("/api/v1/users/roles", logic.SecurityCheck(true, http.HandlerFunc(ListRoles))).Methods(http.MethodGet)
 	r.HandleFunc("/api/v1/users/roles", logic.SecurityCheck(true, http.HandlerFunc(ListRoles))).Methods(http.MethodGet)
-
+	r.HandleFunc("/api/v1/users/access_token", logic.SecurityCheck(true, http.HandlerFunc(createUserAccessToken))).Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/users/access_token", logic.SecurityCheck(true, http.HandlerFunc(getUserAccessTokens))).Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/users/access_token", logic.SecurityCheck(true, http.HandlerFunc(deleteUserAccessTokens))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/v1/users/logout", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(logout)))).Methods(http.MethodPost)
 }
 }
 
 
 // @Summary     Authenticate a user to retrieve an authorization token
 // @Summary     Authenticate a user to retrieve an authorization token
-// @Router      /api/users/adm/authenticate [post]
+// @Router      /api/v1/users/{username}/access_token [post]
 // @Tags        Auth
 // @Tags        Auth
 // @Accept      json
 // @Accept      json
 // @Param       body body models.UserAuthParams true "Authentication parameters"
 // @Param       body body models.UserAuthParams true "Authentication parameters"
@@ -49,24 +57,201 @@ func userHandlers(r *mux.Router) {
 // @Failure     400 {object} models.ErrorResponse
 // @Failure     400 {object} models.ErrorResponse
 // @Failure     401 {object} models.ErrorResponse
 // @Failure     401 {object} models.ErrorResponse
 // @Failure     500 {object} models.ErrorResponse
 // @Failure     500 {object} models.ErrorResponse
-func authenticateUser(response http.ResponseWriter, request *http.Request) {
+func createUserAccessToken(w http.ResponseWriter, r *http.Request) {
 
 
 	// Auth request consists of Mac Address and Password (from node that is authorizing
 	// Auth request consists of Mac Address and Password (from node that is authorizing
 	// in case of Master, auth is ignored and mac is set to "mastermac"
 	// in case of Master, auth is ignored and mac is set to "mastermac"
-	var authRequest models.UserAuthParams
-	var errorResponse = models.ErrorResponse{
-		Code: http.StatusInternalServerError, Message: "W1R3: It's not you it's me.",
+	var req schema.UserAccessToken
+
+	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, logic.BadReq))
+		return
+	}
+	if req.Name == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("name is required"), logic.BadReq))
+		return
+	}
+	if req.UserName == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("username is required"), logic.BadReq))
+		return
+	}
+	caller, err := logic.GetUser(r.Header.Get("user"))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.UnAuthorized))
+		return
+	}
+	user, err := logic.GetUser(req.UserName)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.UnAuthorized))
+		return
+	}
+	if caller.UserName != user.UserName && caller.PlatformRoleID != models.SuperAdminRole {
+		if caller.PlatformRoleID == models.AdminRole {
+			if user.PlatformRoleID == models.SuperAdminRole || user.PlatformRoleID == models.AdminRole {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("not enough permissions to create token for user "+user.UserName), logic.Forbidden_Msg))
+				return
+			}
+		} else {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("not enough permissions to create token for user "+user.UserName), logic.Forbidden_Msg))
+			return
+		}
 	}
 	}
 
 
-	if !servercfg.IsBasicAuthEnabled() {
+	req.ID = uuid.New().String()
+	req.CreatedBy = r.Header.Get("user")
+	req.CreatedAt = time.Now()
+	jwt, err := logic.CreateUserAccessJwtToken(user.UserName, user.PlatformRoleID, req.ExpiresAt, req.ID)
+	if jwt == "" {
+		// very unlikely that err is !nil and no jwt returned, but handle it anyways.
 		logic.ReturnErrorResponse(
 		logic.ReturnErrorResponse(
-			response,
-			request,
-			logic.FormatError(fmt.Errorf("basic auth is disabled"), "badrequest"),
+			w,
+			r,
+			logic.FormatError(errors.New("error creating access token "+err.Error()), logic.Internal),
+		)
+		return
+	}
+	err = req.Create(r.Context())
+	if err != nil {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			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{
+		AuthToken: jwt,
+		UserName:  req.UserName,
+	}, "api access token has generated for user "+req.UserName)
+}
 
 
+// @Summary     Authenticate a user to retrieve an authorization token
+// @Router      /api/v1/users/{username}/access_token [post]
+// @Tags        Auth
+// @Accept      json
+// @Param       body body models.UserAuthParams true "Authentication parameters"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     401 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func getUserAccessTokens(w http.ResponseWriter, r *http.Request) {
+	username := r.URL.Query().Get("username")
+	if username == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("username is required"), "badrequest"))
+		return
+	}
+	logic.ReturnSuccessResponseWithJson(w, r, (&schema.UserAccessToken{UserName: username}).ListByUser(r.Context()), "fetched api access tokens for user "+username)
+}
+
+// @Summary     Authenticate a user to retrieve an authorization token
+// @Router      /api/v1/users/{username}/access_token [post]
+// @Tags        Auth
+// @Accept      json
+// @Param       body body models.UserAuthParams true "Authentication parameters"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     401 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func deleteUserAccessTokens(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
+	}
+	a := schema.UserAccessToken{
+		ID: id,
+	}
+	err := a.Get(r.Context())
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("token does not exist"), "badrequest"))
+		return
+	}
+	caller, err := logic.GetUser(r.Header.Get("user"))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
+		return
+	}
+	user, err := logic.GetUser(a.UserName)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
+		return
+	}
+	if caller.UserName != user.UserName && caller.PlatformRoleID != models.SuperAdminRole {
+		if caller.PlatformRoleID == models.AdminRole {
+			if user.PlatformRoleID == models.SuperAdminRole || user.PlatformRoleID == models.AdminRole {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("not enough permissions to delete token of user "+user.UserName), logic.Forbidden_Msg))
+				return
+			}
+		} else {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("not enough permissions to delete token of user "+user.UserName), logic.Forbidden_Msg))
+			return
+		}
+	}
+
+	err = (&schema.UserAccessToken{ID: id}).Delete(r.Context())
+	if err != nil {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("error deleting access token "+err.Error()), "internal"),
+		)
+		return
+	}
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		Source: models.Subject{
+			ID:   caller.UserName,
+			Name: caller.UserName,
+			Type: models.UserSub,
+		},
+		TriggeredBy: caller.UserName,
+		Target: models.Subject{
+			ID:   a.ID,
+			Name: a.Name,
+			Type: models.UserAccessTokenSub,
+			Info: a,
+		},
+		Origin: models.Dashboard,
+	})
+	logic.ReturnSuccessResponseWithJson(w, r, nil, "revoked access token")
+}
+
+// @Summary     Authenticate a user to retrieve an authorization token
+// @Router      /api/users/adm/authenticate [post]
+// @Tags        Auth
+// @Accept      json
+// @Param       body body models.UserAuthParams true "Authentication parameters"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     401 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func authenticateUser(response http.ResponseWriter, request *http.Request) {
+
+	// Auth request consists of Mac Address and Password (from node that is authorizing
+	// in case of Master, auth is ignored and mac is set to "mastermac"
+	var authRequest models.UserAuthParams
+	var errorResponse = models.ErrorResponse{
+		Code: http.StatusInternalServerError, Message: "W1R3: It's not you it's me.",
+	}
 	decoder := json.NewDecoder(request.Body)
 	decoder := json.NewDecoder(request.Body)
 	decoderErr := decoder.Decode(&authRequest)
 	decoderErr := decoder.Decode(&authRequest)
 	defer request.Body.Close()
 	defer request.Body.Close()
@@ -76,15 +261,36 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 		logic.ReturnErrorResponse(response, request, errorResponse)
 		logic.ReturnErrorResponse(response, request, errorResponse)
 		return
 		return
 	}
 	}
+	user, err := logic.GetUser(authRequest.UserName)
+	if err != nil {
+		logger.Log(0, authRequest.UserName, "user validation failed: ",
+			err.Error())
+		logic.ReturnErrorResponse(response, request, logic.FormatError(err, "unauthorized"))
+		return
+	}
+	if logic.IsOauthUser(user) == nil {
+		logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("user is registered via SSO"), "badrequest"))
+		return
+	}
+
+	if user.AccountDisabled {
+		err = errors.New("user account disabled")
+		logic.ReturnErrorResponse(response, request, logic.FormatError(err, "unauthorized"))
+		return
+	}
+
+	if !user.IsSuperAdmin && !logic.IsBasicAuthEnabled() {
+		logic.ReturnErrorResponse(
+			response,
+			request,
+			logic.FormatError(fmt.Errorf("basic auth is disabled"), "badrequest"),
+		)
+		return
+	}
+
 	if val := request.Header.Get("From-Ui"); val == "true" {
 	if val := request.Header.Get("From-Ui"); val == "true" {
 		// request came from UI, if normal user block Login
 		// request came from UI, if normal user block Login
-		user, err := logic.GetUser(authRequest.UserName)
-		if err != nil {
-			logger.Log(0, authRequest.UserName, "user validation failed: ",
-				err.Error())
-			logic.ReturnErrorResponse(response, request, logic.FormatError(err, "unauthorized"))
-			return
-		}
+
 		role, err := logic.GetRole(user.PlatformRoleID)
 		role, err := logic.GetRole(user.PlatformRoleID)
 		if err != nil {
 		if err != nil {
 			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"))
@@ -94,16 +300,40 @@ 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,
+		})
 	}
 	}
-	user, err := logic.GetUser(authRequest.UserName)
-	if err != nil {
-		logic.ReturnErrorResponse(response, request, logic.FormatError(err, "unauthorized"))
-		return
-	}
-	if logic.IsOauthUser(user) == nil {
-		logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("user is registered via SSO"), "badrequest"))
-		return
-	}
+
 	username := authRequest.UserName
 	username := authRequest.UserName
 	jwt, err := logic.VerifyAuthRequest(authRequest)
 	jwt, err := logic.VerifyAuthRequest(authRequest)
 	if err != nil {
 	if err != nil {
@@ -145,7 +375,7 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 	response.Write(successJSONResponse)
 	response.Write(successJSONResponse)
 
 
 	go func() {
 	go func() {
-		if servercfg.IsPro && servercfg.GetRacAutoDisable() {
+		if servercfg.IsPro && logic.GetRacAutoDisable() {
 			// enable all associeated clients for the user
 			// enable all associeated clients for the user
 			clients, err := logic.GetAllExtClients()
 			clients, err := logic.GetAllExtClients()
 			if err != nil {
 			if err != nil {
@@ -225,6 +455,65 @@ func getUser(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(user)
 	json.NewEncoder(w).Encode(user)
 }
 }
 
 
+// @Summary     Enable a user's account
+// @Router      /api/users/{username}/enable [post]
+// @Tags        Users
+// @Param       username path string true "Username of the user to enable"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func enableUserAccount(w http.ResponseWriter, r *http.Request) {
+	username := mux.Vars(r)["username"]
+	user, err := logic.GetUser(username)
+	if err != nil {
+		logger.Log(0, "failed to fetch user: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+
+	user.AccountDisabled = false
+	err = logic.UpsertUser(*user)
+	if err != nil {
+		logger.Log(0, "failed to enable user account: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+	}
+
+	logic.ReturnSuccessResponse(w, r, "user account enabled")
+}
+
+// @Summary     Disable a user's account
+// @Router      /api/users/{username}/disable [post]
+// @Tags        Users
+// @Param       username path string true "Username of the user to disable"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func disableUserAccount(w http.ResponseWriter, r *http.Request) {
+	username := mux.Vars(r)["username"]
+	user, err := logic.GetUser(username)
+	if err != nil {
+		logger.Log(0, "failed to fetch user: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+
+	if user.PlatformRoleID == models.SuperAdminRole {
+		err = errors.New("cannot disable super-admin user account")
+		logger.Log(0, err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	user.AccountDisabled = true
+	err = logic.UpsertUser(*user)
+	if err != nil {
+		logger.Log(0, "failed to disable user account: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+	}
+
+	logic.ReturnSuccessResponse(w, r, "user account disabled")
+}
+
 // swagger:route GET /api/v1/users user getUserV1
 // swagger:route GET /api/v1/users user getUserV1
 //
 //
 // Get an individual user with role info.
 // Get an individual user with role info.
@@ -319,7 +608,7 @@ func createSuperAdmin(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
-	if !servercfg.IsBasicAuthEnabled() {
+	if !logic.IsBasicAuthEnabled() {
 		logic.ReturnErrorResponse(
 		logic.ReturnErrorResponse(
 			w,
 			w,
 			r,
 			r,
@@ -367,7 +656,7 @@ func transferSuperAdmin(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only admins can be promoted to superadmin role"), "forbidden"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only admins can be promoted to superadmin role"), "forbidden"))
 		return
 		return
 	}
 	}
-	if !servercfg.IsBasicAuthEnabled() {
+	if !logic.IsBasicAuthEnabled() {
 		logic.ReturnErrorResponse(
 		logic.ReturnErrorResponse(
 			w,
 			w,
 			r,
 			r,
@@ -458,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)
@@ -534,12 +838,12 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
 			return
 			return
 		}
 		}
 		if caller.PlatformRoleID == models.AdminRole && user.PlatformRoleID == models.AdminRole {
 		if caller.PlatformRoleID == models.AdminRole && user.PlatformRoleID == models.AdminRole {
-			slog.Error("admin user cannot update another admin", "caller", caller.UserName, "attempted to update admin user", username)
-			logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("admin user cannot update another admin"), "forbidden"))
+			slog.Error("an admin user does not have permissions to update another admin user", "caller", caller.UserName, "attempted to update admin user", username)
+			logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("an admin user does not have permissions to update another admin user"), "forbidden"))
 			return
 			return
 		}
 		}
 		if caller.PlatformRoleID == models.AdminRole && userchange.PlatformRoleID == models.AdminRole {
 		if caller.PlatformRoleID == models.AdminRole && userchange.PlatformRoleID == models.AdminRole {
-			err = errors.New("admin user cannot update role of an another user to admin")
+			err = errors.New("an admin user does not have permissions to assign the admin role to another user")
 			slog.Error(
 			slog.Error(
 				"failed to update user",
 				"failed to update user",
 				"caller",
 				"caller",
@@ -592,7 +896,30 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
 		return
 		return
 	}
 	}
-
+	logic.AddGlobalNetRolesToAdmins(&userchange)
+	if userchange.PlatformRoleID != user.PlatformRoleID || !logic.CompareMaps(user.UserGroups, userchange.UserGroups) {
+		(&schema.UserAccessToken{UserName: user.UserName}).DeleteAllUserTokens(r.Context())
+	}
+	oldUser := *user
+	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: oldUser,
+			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,
@@ -600,6 +927,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))
@@ -671,18 +999,28 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
 			return
 			return
 		}
 		}
 	}
 	}
-	success, err := logic.DeleteUser(username)
+	err = logic.DeleteUser(username)
 	if err != nil {
 	if err != nil {
 		logger.Log(0, username,
 		logger.Log(0, username,
 			"failed to delete user: ", err.Error())
 			"failed to delete user: ", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
-	} else if !success {
-		err := errors.New("delete unsuccessful")
-		logger.Log(0, username, err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-		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()
@@ -748,3 +1086,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")
+}

+ 37 - 90
database/database.go

@@ -1,18 +1,12 @@
 package database
 package database
 
 
 import (
 import (
-	"crypto/rand"
-	"encoding/json"
 	"errors"
 	"errors"
 	"sync"
 	"sync"
 	"time"
 	"time"
 
 
-	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
-	"github.com/gravitl/netmaker/models"
-	"github.com/gravitl/netmaker/netclient/ncutils"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
-	"golang.org/x/crypto/nacl/box"
 )
 )
 
 
 const (
 const (
@@ -73,6 +67,8 @@ const (
 	TAG_TABLE_NAME = "tags"
 	TAG_TABLE_NAME = "tags"
 	// PEER_ACK_TABLE - table for failover peer ack
 	// PEER_ACK_TABLE - table for failover peer ack
 	PEER_ACK_TABLE = "peer_ack"
 	PEER_ACK_TABLE = "peer_ack"
+	// SERVER_SETTINGS - table for server settings
+	SERVER_SETTINGS = "server_settings"
 	// == ERROR CONSTS ==
 	// == ERROR CONSTS ==
 	// NO_RECORD - no singular result found
 	// NO_RECORD - no singular result found
 	NO_RECORD = "no result found"
 	NO_RECORD = "no result found"
@@ -102,6 +98,36 @@ const (
 
 
 var dbMutex sync.RWMutex
 var dbMutex sync.RWMutex
 
 
+var Tables = []string{
+	NETWORKS_TABLE_NAME,
+	NODES_TABLE_NAME,
+	CERTS_TABLE_NAME,
+	DELETED_NODES_TABLE_NAME,
+	USERS_TABLE_NAME,
+	DNS_TABLE_NAME,
+	EXT_CLIENT_TABLE_NAME,
+	PEERS_TABLE_NAME,
+	SERVERCONF_TABLE_NAME,
+	SERVER_UUID_TABLE_NAME,
+	GENERATED_TABLE_NAME,
+	NODE_ACLS_TABLE_NAME,
+	SSO_STATE_CACHE,
+	METRICS_TABLE_NAME,
+	NETWORK_USER_TABLE_NAME,
+	USER_GROUPS_TABLE_NAME,
+	CACHE_TABLE_NAME,
+	HOSTS_TABLE_NAME,
+	ENROLLMENT_KEYS_TABLE_NAME,
+	HOST_ACTIONS_TABLE_NAME,
+	PENDING_USERS_TABLE_NAME,
+	USER_PERMISSIONS_TABLE_NAME,
+	USER_INVITES_TABLE_NAME,
+	TAG_TABLE_NAME,
+	ACLS_TABLE_NAME,
+	PEER_ACK_TABLE,
+	SERVER_SETTINGS,
+}
+
 func getCurrentDB() map[string]interface{} {
 func getCurrentDB() map[string]interface{} {
 	switch servercfg.GetDB() {
 	switch servercfg.GetDB() {
 	case "rqlite":
 	case "rqlite":
@@ -131,71 +157,30 @@ func InitializeDatabase() error {
 		time.Sleep(2 * time.Second)
 		time.Sleep(2 * time.Second)
 	}
 	}
 	createTables()
 	createTables()
-	return initializeUUID()
+	return nil
 }
 }
 
 
 func createTables() {
 func createTables() {
-	CreateTable(NETWORKS_TABLE_NAME)
-	CreateTable(NODES_TABLE_NAME)
-	CreateTable(CERTS_TABLE_NAME)
-	CreateTable(DELETED_NODES_TABLE_NAME)
-	CreateTable(USERS_TABLE_NAME)
-	CreateTable(DNS_TABLE_NAME)
-	CreateTable(EXT_CLIENT_TABLE_NAME)
-	CreateTable(PEERS_TABLE_NAME)
-	CreateTable(SERVERCONF_TABLE_NAME)
-	CreateTable(SERVER_UUID_TABLE_NAME)
-	CreateTable(GENERATED_TABLE_NAME)
-	CreateTable(NODE_ACLS_TABLE_NAME)
-	CreateTable(SSO_STATE_CACHE)
-	CreateTable(METRICS_TABLE_NAME)
-	CreateTable(NETWORK_USER_TABLE_NAME)
-	CreateTable(USER_GROUPS_TABLE_NAME)
-	CreateTable(CACHE_TABLE_NAME)
-	CreateTable(HOSTS_TABLE_NAME)
-	CreateTable(ENROLLMENT_KEYS_TABLE_NAME)
-	CreateTable(HOST_ACTIONS_TABLE_NAME)
-	CreateTable(PENDING_USERS_TABLE_NAME)
-	CreateTable(USER_PERMISSIONS_TABLE_NAME)
-	CreateTable(USER_INVITES_TABLE_NAME)
-	CreateTable(TAG_TABLE_NAME)
-	CreateTable(ACLS_TABLE_NAME)
-	CreateTable(PEER_ACK_TABLE)
+	for _, table := range Tables {
+		_ = CreateTable(table)
+	}
 }
 }
 
 
 func CreateTable(tableName string) error {
 func CreateTable(tableName string) error {
 	return getCurrentDB()[CREATE_TABLE].(func(string) error)(tableName)
 	return getCurrentDB()[CREATE_TABLE].(func(string) error)(tableName)
 }
 }
 
 
-// IsJSONString - checks if valid json
-func IsJSONString(value string) bool {
-	var jsonInt interface{}
-	var nodeInt models.Node
-	return json.Unmarshal([]byte(value), &jsonInt) == nil || json.Unmarshal([]byte(value), &nodeInt) == nil
-}
-
 // Insert - inserts object into db
 // Insert - inserts object into db
 func Insert(key string, value string, tableName string) error {
 func Insert(key string, value string, tableName string) error {
 	dbMutex.Lock()
 	dbMutex.Lock()
 	defer dbMutex.Unlock()
 	defer dbMutex.Unlock()
-	if key != "" && value != "" && IsJSONString(value) {
+	if key != "" && value != "" {
 		return getCurrentDB()[INSERT].(func(string, string, string) error)(key, value, tableName)
 		return getCurrentDB()[INSERT].(func(string, string, string) error)(key, value, tableName)
 	} else {
 	} else {
 		return errors.New("invalid insert " + key + " : " + value)
 		return errors.New("invalid insert " + key + " : " + value)
 	}
 	}
 }
 }
 
 
-// InsertPeer - inserts peer into db
-func InsertPeer(key string, value string) error {
-	dbMutex.Lock()
-	defer dbMutex.Unlock()
-	if key != "" && value != "" && IsJSONString(value) {
-		return getCurrentDB()[INSERT_PEER].(func(string, string) error)(key, value)
-	} else {
-		return errors.New("invalid peer insert " + key + " : " + value)
-	}
-}
-
 // DeleteRecord - deletes a record from db
 // DeleteRecord - deletes a record from db
 func DeleteRecord(tableName string, key string) error {
 func DeleteRecord(tableName string, key string) error {
 	dbMutex.Lock()
 	dbMutex.Lock()
@@ -237,44 +222,6 @@ func FetchRecords(tableName string) (map[string]string, error) {
 	return getCurrentDB()[FETCH_ALL].(func(string) (map[string]string, error))(tableName)
 	return getCurrentDB()[FETCH_ALL].(func(string) (map[string]string, error))(tableName)
 }
 }
 
 
-// initializeUUID - create a UUID record for server if none exists
-func initializeUUID() error {
-	records, err := FetchRecords(SERVER_UUID_TABLE_NAME)
-	if err != nil {
-		if !IsEmptyRecord(err) {
-			return err
-		}
-	} else if len(records) > 0 {
-		return nil
-	}
-	// setup encryption keys
-	var trafficPubKey, trafficPrivKey, errT = box.GenerateKey(rand.Reader) // generate traffic keys
-	if errT != nil {
-		return errT
-	}
-	tPriv, err := ncutils.ConvertKeyToBytes(trafficPrivKey)
-	if err != nil {
-		return err
-	}
-
-	tPub, err := ncutils.ConvertKeyToBytes(trafficPubKey)
-	if err != nil {
-		return err
-	}
-
-	telemetry := models.Telemetry{
-		UUID:           uuid.NewString(),
-		TrafficKeyPriv: tPriv,
-		TrafficKeyPub:  tPub,
-	}
-	telJSON, err := json.Marshal(&telemetry)
-	if err != nil {
-		return err
-	}
-
-	return Insert(SERVER_UUID_RECORD_KEY, string(telJSON), SERVER_UUID_TABLE_NAME)
-}
-
 // CloseDB - closes a database gracefully
 // CloseDB - closes a database gracefully
 func CloseDB() {
 func CloseDB() {
 	getCurrentDB()[CLOSE_DB].(func())()
 	getCurrentDB()[CLOSE_DB].(func())()

+ 2 - 2
database/postgres.go

@@ -59,7 +59,7 @@ func pgCreateTable(tableName string) error {
 }
 }
 
 
 func pgInsert(key string, value string, tableName string) error {
 func pgInsert(key string, value string, tableName string) error {
-	if key != "" && value != "" && IsJSONString(value) {
+	if key != "" && value != "" {
 		insertSQL := "INSERT INTO " + tableName + " (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = $3;"
 		insertSQL := "INSERT INTO " + tableName + " (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = $3;"
 		statement, err := PGDB.Prepare(insertSQL)
 		statement, err := PGDB.Prepare(insertSQL)
 		if err != nil {
 		if err != nil {
@@ -77,7 +77,7 @@ func pgInsert(key string, value string, tableName string) error {
 }
 }
 
 
 func pgInsertPeer(key string, value string) error {
 func pgInsertPeer(key string, value string) error {
-	if key != "" && value != "" && IsJSONString(value) {
+	if key != "" && value != "" {
 		err := pgInsert(key, value, PEERS_TABLE_NAME)
 		err := pgInsert(key, value, PEERS_TABLE_NAME)
 		if err != nil {
 		if err != nil {
 			return err
 			return err

+ 2 - 2
database/rqlite.go

@@ -43,7 +43,7 @@ func rqliteCreateTable(tableName string) error {
 }
 }
 
 
 func rqliteInsert(key string, value string, tableName string) error {
 func rqliteInsert(key string, value string, tableName string) error {
-	if key != "" && value != "" && IsJSONString(value) {
+	if key != "" && value != "" {
 		_, err := RQliteDatabase.WriteOne("INSERT OR REPLACE INTO " + tableName + " (key, value) VALUES ('" + key + "', '" + value + "')")
 		_, err := RQliteDatabase.WriteOne("INSERT OR REPLACE INTO " + tableName + " (key, value) VALUES ('" + key + "', '" + value + "')")
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -54,7 +54,7 @@ func rqliteInsert(key string, value string, tableName string) error {
 }
 }
 
 
 func rqliteInsertPeer(key string, value string) error {
 func rqliteInsertPeer(key string, value string) error {
-	if key != "" && value != "" && IsJSONString(value) {
+	if key != "" && value != "" {
 		_, err := RQliteDatabase.WriteOne("INSERT OR REPLACE INTO " + PEERS_TABLE_NAME + " (key, value) VALUES ('" + key + "', '" + value + "')")
 		_, err := RQliteDatabase.WriteOne("INSERT OR REPLACE INTO " + PEERS_TABLE_NAME + " (key, value) VALUES ('" + key + "', '" + value + "')")
 		if err != nil {
 		if err != nil {
 			return err
 			return err

+ 2 - 2
database/sqlite.go

@@ -61,7 +61,7 @@ func sqliteCreateTable(tableName string) error {
 }
 }
 
 
 func sqliteInsert(key string, value string, tableName string) error {
 func sqliteInsert(key string, value string, tableName string) error {
-	if key != "" && value != "" && IsJSONString(value) {
+	if key != "" && value != "" {
 		insertSQL := "INSERT OR REPLACE INTO " + tableName + " (key, value) VALUES (?, ?)"
 		insertSQL := "INSERT OR REPLACE INTO " + tableName + " (key, value) VALUES (?, ?)"
 		statement, err := SqliteDB.Prepare(insertSQL)
 		statement, err := SqliteDB.Prepare(insertSQL)
 		if err != nil {
 		if err != nil {
@@ -78,7 +78,7 @@ func sqliteInsert(key string, value string, tableName string) error {
 }
 }
 
 
 func sqliteInsertPeer(key string, value string) error {
 func sqliteInsertPeer(key string, value string) error {
-	if key != "" && value != "" && IsJSONString(value) {
+	if key != "" && value != "" {
 		err := sqliteInsert(key, value, PEERS_TABLE_NAME)
 		err := sqliteInsert(key, value, PEERS_TABLE_NAME)
 		if err != nil {
 		if err != nil {
 			return err
 			return err

+ 0 - 59
database/statics.go

@@ -1,59 +0,0 @@
-package database
-
-import (
-	"encoding/json"
-	"strings"
-)
-
-// SetPeers - sets peers for a network
-func SetPeers(newPeers map[string]string, networkName string) bool {
-	areEqual := PeersAreEqual(newPeers, networkName)
-	if !areEqual {
-		jsonData, err := json.Marshal(newPeers)
-		if err != nil {
-			return false
-		}
-		InsertPeer(networkName, string(jsonData))
-		return true
-	}
-	return !areEqual
-}
-
-// GetPeers - gets peers for a given network
-func GetPeers(networkName string) (map[string]string, error) {
-	record, err := FetchRecord(PEERS_TABLE_NAME, networkName)
-	if err != nil && !IsEmptyRecord(err) {
-		return nil, err
-	}
-	currentDataMap := make(map[string]string)
-	if IsEmptyRecord(err) {
-		return currentDataMap, nil
-	}
-	err = json.Unmarshal([]byte(record), &currentDataMap)
-	return currentDataMap, err
-}
-
-// PeersAreEqual - checks if peers are the same
-func PeersAreEqual(toCompare map[string]string, networkName string) bool {
-	currentDataMap, err := GetPeers(networkName)
-	if err != nil {
-		return false
-	}
-	if len(currentDataMap) != len(toCompare) {
-		return false
-	}
-	for k := range currentDataMap {
-		if toCompare[k] != currentDataMap[k] {
-			return false
-		}
-	}
-	return true
-}
-
-// IsEmptyRecord - checks for if it's an empty record error or not
-func IsEmptyRecord(err error) bool {
-	if err == nil {
-		return false
-	}
-	return strings.Contains(err.Error(), NO_RECORD) || strings.Contains(err.Error(), NO_RECORDS)
-}

+ 11 - 0
database/utils.go

@@ -0,0 +1,11 @@
+package database
+
+import "strings"
+
+// IsEmptyRecord - checks for if it's an empty record error or not
+func IsEmptyRecord(err error) bool {
+	if err == nil {
+		return false
+	}
+	return strings.Contains(err.Error(), NO_RECORD) || strings.Contains(err.Error(), NO_RECORDS)
+}

+ 41 - 0
db/connector.go

@@ -0,0 +1,41 @@
+package db
+
+import (
+	"errors"
+	"os"
+
+	"github.com/gravitl/netmaker/config"
+	"gorm.io/gorm"
+)
+
+var ErrUnsupportedDB = errors.New("unsupported db type")
+
+// connector helps connect to a database,
+// along with any initializations required.
+type connector interface {
+	connect() (*gorm.DB, error)
+}
+
+// GetDB - gets the database type
+func GetDB() string {
+	database := "sqlite"
+	if os.Getenv("DATABASE") != "" {
+		database = os.Getenv("DATABASE")
+	} else if config.Config.Server.Database != "" {
+		database = config.Config.Server.Database
+	}
+	return database
+}
+
+// newConnector detects the database being
+// used and returns the corresponding connector.
+func newConnector() (connector, error) {
+	switch GetDB() {
+	case "sqlite":
+		return &sqliteConnector{}, nil
+	case "postgres":
+		return &postgresConnector{}, nil
+	default:
+		return nil, ErrUnsupportedDB
+	}
+}

+ 112 - 0
db/db.go

@@ -0,0 +1,112 @@
+package db
+
+import (
+	"context"
+	"errors"
+	"net/http"
+	"time"
+
+	"gorm.io/gorm"
+)
+
+type ctxKey string
+
+const dbCtxKey ctxKey = "db"
+
+var db *gorm.DB
+
+var ErrDBNotFound = errors.New("no db instance in context")
+
+// InitializeDB initializes a connection to the
+// database (if not already done) and ensures it
+// has the latest schema.
+func InitializeDB(models ...interface{}) error {
+	if db != nil {
+		return nil
+	}
+
+	connector, err := newConnector()
+	if err != nil {
+		return err
+	}
+
+	// DB / LIFE ADVICE: try 5 times before giving up.
+	for i := 0; i < 5; i++ {
+		db, err = connector.connect()
+		if err == nil {
+			break
+		}
+
+		// wait 2s if you have the time.
+		time.Sleep(2 * time.Second)
+	}
+	if err != nil {
+		return err
+	}
+
+	return db.AutoMigrate(models...)
+}
+
+// WithContext returns a new context with the db
+// connection instance.
+//
+// Ensure InitializeDB has been called before using
+// this function.
+//
+// To extract the db connection use the FromContext
+// function.
+func WithContext(ctx context.Context) context.Context {
+	return context.WithValue(ctx, dbCtxKey, db)
+}
+
+// Middleware to auto-inject the db connection instance
+// in a request's context.
+//
+// Ensure InitializeDB has been called before using this
+// middleware.
+func Middleware(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		next.ServeHTTP(w, r.WithContext(WithContext(r.Context())))
+	})
+}
+
+// FromContext extracts the db connection instance from
+// the given context.
+//
+// The function panics, if a connection does not exist.
+func FromContext(ctx context.Context) *gorm.DB {
+	db, ok := ctx.Value(dbCtxKey).(*gorm.DB)
+	if !ok {
+		panic(ErrDBNotFound)
+	}
+
+	return db
+}
+
+func SetPagination(ctx context.Context, page, pageSize int) context.Context {
+	if page < 1 {
+		page = 1
+	}
+	if pageSize < 1 || pageSize > 100 {
+		pageSize = 10
+	}
+	db := FromContext(ctx)
+	offset := (page - 1) * pageSize
+	return context.WithValue(ctx, dbCtxKey, db.Offset(offset).Limit(pageSize))
+}
+
+// BeginTx returns a context with a new transaction.
+// If the context already has a db connection instance,
+// it uses that instance. Otherwise, it uses the
+// connection initialized in the package.
+//
+// Ensure InitializeDB has been called before using
+// this function.
+func BeginTx(ctx context.Context) context.Context {
+	dbInCtx, ok := ctx.Value(dbCtxKey).(*gorm.DB)
+	if !ok {
+		return context.WithValue(ctx, dbCtxKey, db.Begin())
+	}
+
+	return context.WithValue(ctx, dbCtxKey, dbInCtx.Begin())
+}

+ 117 - 0
db/postgres.go

@@ -0,0 +1,117 @@
+package db
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+
+	"github.com/gravitl/netmaker/config"
+	"gorm.io/driver/postgres"
+	"gorm.io/gorm"
+	"gorm.io/gorm/logger"
+)
+
+// postgresConnector for initializing and
+// connecting to a postgres database.
+type postgresConnector struct{}
+
+// postgresConnector.connect connects and
+// initializes a connection to postgres.
+func (pg *postgresConnector) connect() (*gorm.DB, error) {
+	pgConf := GetSQLConf()
+	dsn := fmt.Sprintf(
+		"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s connect_timeout=5",
+		pgConf.Host,
+		pgConf.Port,
+		pgConf.Username,
+		pgConf.Password,
+		pgConf.DB,
+		pgConf.SSLMode,
+	)
+
+	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
+		Logger: logger.Default.LogMode(logger.Silent),
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	// ensure netmaker_v1 schema exists.
+	err = db.Exec("CREATE SCHEMA IF NOT EXISTS netmaker_v1").Error
+	if err != nil {
+		return nil, err
+	}
+
+	// set the netmaker_v1 schema as the default schema.
+	err = db.Exec("SET search_path TO netmaker_v1").Error
+	if err != nil {
+		return nil, err
+	}
+
+	return db, nil
+}
+func GetSQLConf() config.SQLConfig {
+	var cfg config.SQLConfig
+	cfg.Host = GetSQLHost()
+	cfg.Port = GetSQLPort()
+	cfg.Username = GetSQLUser()
+	cfg.Password = GetSQLPass()
+	cfg.DB = GetSQLDB()
+	cfg.SSLMode = GetSQLSSLMode()
+	return cfg
+}
+func GetSQLHost() string {
+	host := "localhost"
+	if os.Getenv("SQL_HOST") != "" {
+		host = os.Getenv("SQL_HOST")
+	} else if config.Config.SQL.Host != "" {
+		host = config.Config.SQL.Host
+	}
+	return host
+}
+func GetSQLPort() int32 {
+	port := int32(5432)
+	envport, err := strconv.Atoi(os.Getenv("SQL_PORT"))
+	if err == nil && envport != 0 {
+		port = int32(envport)
+	} else if config.Config.SQL.Port != 0 {
+		port = config.Config.SQL.Port
+	}
+	return port
+}
+func GetSQLUser() string {
+	user := "postgres"
+	if os.Getenv("SQL_USER") != "" {
+		user = os.Getenv("SQL_USER")
+	} else if config.Config.SQL.Username != "" {
+		user = config.Config.SQL.Username
+	}
+	return user
+}
+func GetSQLPass() string {
+	pass := "nopass"
+	if os.Getenv("SQL_PASS") != "" {
+		pass = os.Getenv("SQL_PASS")
+	} else if config.Config.SQL.Password != "" {
+		pass = config.Config.SQL.Password
+	}
+	return pass
+}
+func GetSQLDB() string {
+	db := "netmaker"
+	if os.Getenv("SQL_DB") != "" {
+		db = os.Getenv("SQL_DB")
+	} else if config.Config.SQL.DB != "" {
+		db = config.Config.SQL.DB
+	}
+	return db
+}
+func GetSQLSSLMode() string {
+	sslmode := "disable"
+	if os.Getenv("SQL_SSL_MODE") != "" {
+		sslmode = os.Getenv("SQL_SSL_MODE")
+	} else if config.Config.SQL.SSLMode != "" {
+		sslmode = config.Config.SQL.SSLMode
+	}
+	return sslmode
+}

+ 55 - 0
db/sqlite.go

@@ -0,0 +1,55 @@
+package db
+
+import (
+	"os"
+	"path/filepath"
+
+	"gorm.io/driver/sqlite"
+	"gorm.io/gorm"
+	"gorm.io/gorm/logger"
+)
+
+// sqliteConnector for initializing and
+// connecting to a sqlite database.
+type sqliteConnector struct{}
+
+// sqliteConnector.connect connects and
+// initializes a connection to sqlite.
+func (s *sqliteConnector) connect() (*gorm.DB, error) {
+	// ensure data dir exists.
+	_, err := os.Stat("data")
+	if err != nil {
+		if os.IsNotExist(err) {
+			err = os.Mkdir("data", 0700)
+			if err != nil {
+				return nil, err
+			}
+		} else {
+			return nil, err
+		}
+	}
+
+	dbFilePath := filepath.Join("data", "netmaker.db")
+
+	// ensure netmaker_v1.db exists.
+	_, err = os.Stat(dbFilePath)
+	if err != nil {
+		if os.IsNotExist(err) {
+			file, err := os.Create(dbFilePath)
+			if err != nil {
+				return nil, err
+			}
+
+			err = file.Close()
+			if err != nil {
+				return nil, err
+			}
+		} else {
+			return nil, err
+		}
+	}
+
+	return gorm.Open(sqlite.Open(dbFilePath), &gorm.Config{
+		Logger: logger.Default.LogMode(logger.Silent),
+	})
+}

+ 1 - 1
docker/Dockerfile-go-builder

@@ -1,6 +1,6 @@
 FROM golang:1.23.0-alpine3.20
 FROM golang:1.23.0-alpine3.20
 ARG version 
 ARG version 
-RUN apk add build-base
+RUN apk add --no-cache build-base
 WORKDIR /app
 WORKDIR /app
 COPY go.* ./ 
 COPY go.* ./ 
 RUN go mod download
 RUN go mod download

+ 1 - 1
docker/Dockerfile-netclient-multiarch

@@ -13,7 +13,7 @@ FROM alpine:3.16.2
 
 
 WORKDIR /root/
 WORKDIR /root/
 
 
-RUN apk add --no-cache --update bash libmnl gcompat iptables openresolv iproute2 wireguard-tools 
+RUN apk add --no-cache bash libmnl gcompat iptables openresolv iproute2 wireguard-tools 
 COPY --from=builder /app/netclient-app ./netclient
 COPY --from=builder /app/netclient-app ./netclient
 COPY --from=builder /app/scripts/netclient.sh .
 COPY --from=builder /app/scripts/netclient.sh .
 RUN chmod 0755 netclient && chmod 0755 netclient.sh
 RUN chmod 0755 netclient && chmod 0755 netclient.sh

+ 2 - 2
docker/Dockerfile-netclient-multiarch-userspace

@@ -10,7 +10,7 @@ RUN GOOS=linux CGO_ENABLED=0 /usr/local/go/bin/go build -ldflags="-w -s" -o netc
 
 
 WORKDIR /root/
 WORKDIR /root/
 
 
-RUN apk add --update git build-base libmnl-dev iptables
+RUN apk add --no-cache git build-base libmnl-dev iptables
 
 
 RUN git clone https://git.zx2c4.com/wireguard-go && \
 RUN git clone https://git.zx2c4.com/wireguard-go && \
     cd wireguard-go && \
     cd wireguard-go && \
@@ -28,7 +28,7 @@ FROM alpine:3.16.2
 
 
 WORKDIR /root/
 WORKDIR /root/
 
 
-RUN apk add --no-cache --update bash libmnl gcompat iptables openresolv iproute2
+RUN apk add --no-cache bash libmnl gcompat iptables openresolv iproute2
 COPY --from=builder /usr/bin/wireguard-go /usr/bin/wg* /usr/bin/
 COPY --from=builder /usr/bin/wireguard-go /usr/bin/wg* /usr/bin/
 COPY --from=builder /app/netclient-app ./netclient
 COPY --from=builder /app/netclient-app ./netclient
 COPY --from=builder /app/scripts/netclient.sh .
 COPY --from=builder /app/scripts/netclient.sh .

+ 51 - 19
go.mod

@@ -1,28 +1,30 @@
 module github.com/gravitl/netmaker
 module github.com/gravitl/netmaker
 
 
-go 1.23
+go 1.23.0
+
+toolchain go1.23.7
 
 
 require (
 require (
 	github.com/blang/semver v3.5.1+incompatible
 	github.com/blang/semver v3.5.1+incompatible
-	github.com/eclipse/paho.mqtt.golang v1.4.3
-	github.com/go-playground/validator/v10 v10.24.0
-	github.com/golang-jwt/jwt/v4 v4.5.1
+	github.com/eclipse/paho.mqtt.golang v1.5.0
+	github.com/go-playground/validator/v10 v10.26.0
+	github.com/golang-jwt/jwt/v4 v4.5.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.32.0
-	golang.org/x/net v0.34.0 // indirect
-	golang.org/x/oauth2 v0.24.0
-	golang.org/x/sys v0.29.0 // indirect
-	golang.org/x/text v0.21.0 // indirect
+	golang.org/x/crypto v0.38.0
+	golang.org/x/net v0.39.0 // indirect
+	golang.org/x/oauth2 v0.29.0
+	golang.org/x/sys v0.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
 )
 )
@@ -30,44 +32,74 @@ 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
 )
 )
 
 
 require (
 require (
+	github.com/google/go-cmp v0.7.0
 	github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e
 	github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e
 	github.com/guumaster/tablewriter v0.0.10
 	github.com/guumaster/tablewriter v0.0.10
 	github.com/matryer/is v1.4.1
 	github.com/matryer/is v1.4.1
 	github.com/olekukonko/tablewriter v0.0.5
 	github.com/olekukonko/tablewriter v0.0.5
-	github.com/spf13/cobra v1.8.1
+	github.com/spf13/cobra v1.9.1
+	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/sqlite v1.5.7
+	gorm.io/gorm v1.26.1
 )
 )
 
 
 require (
 require (
-	cloud.google.com/go/compute/metadata v0.3.0 // indirect
+	cloud.google.com/go/auth v0.16.0 // indirect
+	cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
+	cloud.google.com/go/compute/metadata v0.6.0 // indirect
 	github.com/gabriel-vasile/mimetype v1.4.8 // indirect
 	github.com/gabriel-vasile/mimetype v1.4.8 // indirect
-	github.com/go-jose/go-jose/v3 v3.0.3 // indirect
+	github.com/go-jose/go-jose/v4 v4.0.5 // indirect
+	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/go-logr/stdr v1.2.2 // indirect
+	github.com/go-sql-driver/mysql v1.8.1 // indirect
+	github.com/google/s2a-go v0.1.9 // indirect
+	github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
+	github.com/googleapis/gax-go/v2 v2.14.1 // indirect
+	github.com/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/kr/text v0.2.0 // indirect
+	github.com/jackc/pgpassfile v1.0.0 // indirect
+	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
+	github.com/jackc/pgx/v5 v5.7.2 // indirect
+	github.com/jackc/puddle/v2 v2.2.2 // indirect
+	github.com/jinzhu/inflection v1.0.0 // indirect
+	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
+	github.com/rogpeppe/go-internal v1.14.1 // indirect
 	github.com/seancfoley/bintree v1.3.1 // indirect
 	github.com/seancfoley/bintree v1.3.1 // indirect
-	github.com/spf13/pflag v1.0.5 // indirect
+	github.com/spf13/pflag v1.0.6 // indirect
+	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
+	go.opentelemetry.io/otel v1.35.0 // indirect
+	go.opentelemetry.io/otel/metric v1.35.0 // indirect
+	go.opentelemetry.io/otel/trace v1.35.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect
+	google.golang.org/grpc v1.71.1 // indirect
+	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
+	gorm.io/driver/mysql v1.5.6 // indirect
 )
 )
 
 
 require (
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/felixge/httpsnoop v1.0.3 // indirect
+	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/hashicorp/go-version v1.7.0
 	github.com/hashicorp/go-version v1.7.0
 	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.10.0 // indirect
+	golang.org/x/sync v0.14.0 // indirect
 )
 )

+ 125 - 80
go.sum

@@ -1,40 +1,63 @@
-cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
-cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
+cloud.google.com/go/auth v0.16.0 h1:Pd8P1s9WkcrBE2n/PhAwKsdrR35V3Sg2II9B+ndM3CU=
+cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
+cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
+cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
+cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
+cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/c-robinson/iplib v1.0.8 h1:exDRViDyL9UBLcfmlxxkY5odWX5092nPsQIykHXhIn4=
 github.com/c-robinson/iplib v1.0.8 h1:exDRViDyL9UBLcfmlxxkY5odWX5092nPsQIykHXhIn4=
 github.com/c-robinson/iplib v1.0.8/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo=
 github.com/c-robinson/iplib v1.0.8/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo=
-github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
-github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
-github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
+github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik=
-github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE=
-github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
-github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o=
+github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
 github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
 github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
 github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
-github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
-github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
+github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
+github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
 github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
 github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
 github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
-github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
-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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+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-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
+github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
+github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
+github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
+github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
+github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
+github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
 github.com/google/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/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
+github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
+github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
 github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e h1:XmA6L9IPRdUr28a+SK/oMchGgQy159wvzXA5tJ7l+40=
 github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e h1:XmA6L9IPRdUr28a+SK/oMchGgQy159wvzXA5tJ7l+40=
 github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e/go.mod h1:AFIo+02s+12CEg8Gzz9kzhCbmbq6JcKNrhHffCGA9z4=
 github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e/go.mod h1:AFIo+02s+12CEg8Gzz9kzhCbmbq6JcKNrhHffCGA9z4=
 github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
 github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
@@ -47,10 +70,24 @@ 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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
+github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
+github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
+github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
 github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
@@ -63,19 +100,23 @@ 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=
 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa h1:hxMLFbj+F444JAS5nUQxTDZwUxwCRqg3WkNqhiDzXrM=
 github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa h1:hxMLFbj+F444JAS5nUQxTDZwUxwCRqg3WkNqhiDzXrM=
 github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg=
 github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -85,79 +126,83 @@ github.com/seancfoley/ipaddress-go v1.7.0 h1:vWp3SR3k+HkV3aKiNO2vEe6xbVxS0x/Ixw6
 github.com/seancfoley/ipaddress-go v1.7.0/go.mod h1:TQRZgv+9jdvzHmKoPGBMxyiaVmoI0rYpfEk8Q/sL/Iw=
 github.com/seancfoley/ipaddress-go v1.7.0/go.mod h1:TQRZgv+9jdvzHmKoPGBMxyiaVmoI0rYpfEk8Q/sL/Iw=
 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
-github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
-github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
-github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
+github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/txn2/txeh v1.5.5 h1:UN4e/lCK5HGw/gGAi2GCVrNKg0GTCUWs7gs5riaZlz4=
 github.com/txn2/txeh v1.5.5 h1:UN4e/lCK5HGw/gGAi2GCVrNKg0GTCUWs7gs5riaZlz4=
 github.com/txn2/txeh v1.5.5/go.mod h1:qYzGG9kCzeVEI12geK4IlanHWY8X4uy/I3NcW7mk8g4=
 github.com/txn2/txeh v1.5.5/go.mod h1:qYzGG9kCzeVEI12geK4IlanHWY8X4uy/I3NcW7mk8g4=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
+go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
+go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
 go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
 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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
-golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
+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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
-golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
-golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
-golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
-golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
-golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
-golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
-golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
+golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
+golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
+golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
+golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+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.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
+golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
+golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
+golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb h1:9aqVcYEDHmSNb0uOWukxV5lHV09WqiSiCuhEgWNETLY=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb h1:9aqVcYEDHmSNb0uOWukxV5lHV09WqiSiCuhEgWNETLY=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ=
+google.golang.org/api v0.229.0 h1:p98ymMtqeJ5i3lIBMj5MpR9kzIIgzpHHh8vQ+vgAzx8=
+google.golang.org/api v0.229.0/go.mod h1:wyDfmq5g1wYJWn29O22FDWN48P7Xcz0xz+LBpptYvB0=
+google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
+google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1:ztQaXfzEXTmCBvbtWYRhJxW+0iJcz2qXfd38/e9l7bA=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
+google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
 gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
 gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
 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/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
+gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
+gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
+gorm.io/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=

+ 347 - 136
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
 				}
 				}
@@ -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,6 @@ 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)
 	}
 	}
-
 	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 +1656,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 +1694,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 +1731,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 +1767,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 +1869,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 +1895,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 +2038,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 +2075,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

+ 58 - 8
logic/auth.go

@@ -1,12 +1,16 @@
 package logic
 package logic
 
 
 import (
 import (
+	"context"
 	"encoding/base64"
 	"encoding/base64"
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"time"
 	"time"
 
 
+	"github.com/gravitl/netmaker/db"
+	"github.com/gravitl/netmaker/schema"
+
 	"github.com/go-playground/validator/v10"
 	"github.com/go-playground/validator/v10"
 	"golang.org/x/crypto/bcrypt"
 	"golang.org/x/crypto/bcrypt"
 	"golang.org/x/exp/slog"
 	"golang.org/x/exp/slog"
@@ -28,6 +32,9 @@ func ClearSuperUserCache() {
 	superUser = models.User{}
 	superUser = models.User{}
 }
 }
 
 
+var ResetAuthProvider = func() {}
+var ResetIDPSyncHook = func() {}
+
 // HasSuperAdmin - checks if server has an superadmin/owner
 // HasSuperAdmin - checks if server has an superadmin/owner
 func HasSuperAdmin() (bool, error) {
 func HasSuperAdmin() (bool, error) {
 
 
@@ -169,6 +176,7 @@ func CreateUser(user *models.User) error {
 	if IsOauthUser(user) == nil {
 	if IsOauthUser(user) == nil {
 		user.AuthType = models.OAuth
 		user.AuthType = models.OAuth
 	}
 	}
+	AddGlobalNetRolesToAdmins(user)
 	_, err = CreateUserJWT(user.UserName, user.PlatformRoleID)
 	_, err = CreateUserJWT(user.UserName, user.PlatformRoleID)
 	if err != nil {
 	if err != nil {
 		logger.Log(0, "failed to generate token", err.Error())
 		logger.Log(0, "failed to generate token", err.Error())
@@ -186,7 +194,6 @@ func CreateUser(user *models.User) error {
 		logger.Log(0, "failed to insert user", err.Error())
 		logger.Log(0, "failed to insert user", err.Error())
 		return err
 		return err
 	}
 	}
-	AddGlobalNetRolesToAdmins(*user)
 	return nil
 	return nil
 }
 }
 
 
@@ -235,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)
@@ -298,14 +305,58 @@ func UpdateUser(userchange, user *models.User) (*models.User, error) {
 	if err := IsNetworkRolesValid(userchange.NetworkRoles); err != nil {
 	if err := IsNetworkRolesValid(userchange.NetworkRoles); err != nil {
 		return userchange, errors.New("invalid network roles: " + err.Error())
 		return userchange, errors.New("invalid network roles: " + err.Error())
 	}
 	}
+
+	if userchange.DisplayName != "" {
+		if user.ExternalIdentityProviderID != "" &&
+			user.DisplayName != userchange.DisplayName {
+			return userchange, errors.New("display name cannot be updated for external user")
+		}
+
+		user.DisplayName = userchange.DisplayName
+	}
+
+	if user.ExternalIdentityProviderID != "" &&
+		userchange.AccountDisabled != user.AccountDisabled {
+		return userchange, errors.New("account status cannot be updated for external user")
+	}
+
 	// Reset Gw Access for service users
 	// Reset Gw Access for service users
 	go UpdateUserGwAccess(*user, *userchange)
 	go UpdateUserGwAccess(*user, *userchange)
 	if userchange.PlatformRoleID != "" {
 	if userchange.PlatformRoleID != "" {
 		user.PlatformRoleID = userchange.PlatformRoleID
 		user.PlatformRoleID = userchange.PlatformRoleID
 	}
 	}
+
+	for groupID := range userchange.UserGroups {
+		_, ok := user.UserGroups[groupID]
+		if !ok {
+			group, err := GetUserGroup(groupID)
+			if err != nil {
+				return userchange, err
+			}
+
+			if group.ExternalIdentityProviderID != "" {
+				return userchange, errors.New("cannot modify membership of external groups")
+			}
+		}
+	}
+
+	for groupID := range user.UserGroups {
+		_, ok := userchange.UserGroups[groupID]
+		if !ok {
+			group, err := GetUserGroup(groupID)
+			if err != nil {
+				return userchange, err
+			}
+
+			if group.ExternalIdentityProviderID != "" {
+				return userchange, errors.New("cannot modify membership of external groups")
+			}
+		}
+	}
+
 	user.UserGroups = userchange.UserGroups
 	user.UserGroups = userchange.UserGroups
 	user.NetworkRoles = userchange.NetworkRoles
 	user.NetworkRoles = userchange.NetworkRoles
-	AddGlobalNetRolesToAdmins(*user)
+	AddGlobalNetRolesToAdmins(user)
 	err := ValidateUser(user)
 	err := ValidateUser(user)
 	if err != nil {
 	if err != nil {
 		return &models.User{}, err
 		return &models.User{}, err
@@ -349,19 +400,18 @@ func ValidateUser(user *models.User) error {
 }
 }
 
 
 // DeleteUser - deletes a given user
 // DeleteUser - deletes a given user
-func DeleteUser(user string) (bool, error) {
+func DeleteUser(user string) error {
 
 
 	if userRecord, err := database.FetchRecord(database.USERS_TABLE_NAME, user); err != nil || len(userRecord) == 0 {
 	if userRecord, err := database.FetchRecord(database.USERS_TABLE_NAME, user); err != nil || len(userRecord) == 0 {
-		return false, errors.New("user does not exist")
+		return errors.New("user does not exist")
 	}
 	}
 
 
 	err := database.DeleteRecord(database.USERS_TABLE_NAME, user)
 	err := database.DeleteRecord(database.USERS_TABLE_NAME, user)
 	if err != nil {
 	if err != nil {
-		return false, err
+		return err
 	}
 	}
 	go RemoveUserFromAclPolicy(user)
 	go RemoveUserFromAclPolicy(user)
-
-	return true, nil
+	return (&schema.UserAccessToken{UserName: user}).DeleteAllUserTokens(db.WithContext(context.TODO()))
 }
 }
 
 
 func SetAuthSecret(secret string) error {
 func SetAuthSecret(secret string) error {

+ 1 - 2
logic/dns.go

@@ -12,7 +12,6 @@ import (
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
-	"github.com/gravitl/netmaker/servercfg"
 	"github.com/txn2/txeh"
 	"github.com/txn2/txeh"
 )
 )
 
 
@@ -106,7 +105,7 @@ func GetNodeDNS(network string) ([]models.DNSEntry, error) {
 	if err != nil {
 	if err != nil {
 		return dns, err
 		return dns, err
 	}
 	}
-	defaultDomain := servercfg.GetDefaultDomain()
+	defaultDomain := GetDefaultDomain()
 	for _, node := range nodes {
 	for _, node := range nodes {
 		if node.Network != network {
 		if node.Network != network {
 			continue
 			continue

+ 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()))
+		}
+	}
+
+}

+ 2 - 1
logic/enrollmentkey.go

@@ -38,7 +38,7 @@ var (
 )
 )
 
 
 // CreateEnrollmentKey - creates a new enrollment key in db
 // CreateEnrollmentKey - creates a new enrollment key in db
-func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string, groups []models.TagID, unlimited bool, relay uuid.UUID, defaultKey bool) (*models.EnrollmentKey, error) {
+func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string, groups []models.TagID, unlimited bool, relay uuid.UUID, defaultKey, autoEgress bool) (*models.EnrollmentKey, error) {
 	newKeyID, err := getUniqueEnrollmentID()
 	newKeyID, err := getUniqueEnrollmentID()
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -54,6 +54,7 @@ func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string
 		Relay:         relay,
 		Relay:         relay,
 		Groups:        groups,
 		Groups:        groups,
 		Default:       defaultKey,
 		Default:       defaultKey,
+		AutoEgress:    autoEgress,
 	}
 	}
 	if uses > 0 {
 	if uses > 0 {
 		k.UsesRemaining = uses
 		k.UsesRemaining = uses

+ 13 - 13
logic/enrollmentkey_test.go

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

+ 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 = ""

+ 15 - 2
logic/hosts.go

@@ -228,7 +228,7 @@ func CreateHost(h *models.Host) error {
 		return err
 		return err
 	}
 	}
 	h.HostPass = string(hash)
 	h.HostPass = string(hash)
-	h.AutoUpdate = servercfg.AutoUpdateEnabled()
+	h.AutoUpdate = AutoUpdateEnabled()
 	checkForZombieHosts(h)
 	checkForZombieHosts(h)
 	return UpsertHost(h)
 	return UpsertHost(h)
 }
 }
@@ -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
 		}
 		}
@@ -575,6 +587,7 @@ func CheckHostPorts(h *models.Host) {
 			h.ListenPort = minPort
 			h.ListenPort = minPort
 		}
 		}
 	}
 	}
+	return
 }
 }
 
 
 // HostExists - checks if given host already exists
 // HostExists - checks if given host already exists

+ 59 - 14
logic/jwts.go

@@ -1,6 +1,7 @@
 package logic
 package logic
 
 
 import (
 import (
+	"context"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"strings"
 	"strings"
@@ -8,8 +9,10 @@ import (
 
 
 	"github.com/golang-jwt/jwt/v4"
 	"github.com/golang-jwt/jwt/v4"
 
 
+	"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"
 )
 )
 
 
@@ -53,17 +56,19 @@ func CreateJWT(uuid string, macAddress string, network string) (response string,
 }
 }
 
 
 // CreateUserJWT - creates a user jwt token
 // CreateUserJWT - creates a user jwt token
-func CreateUserJWT(username string, role models.UserRoleID) (response string, err error) {
-	expirationTime := time.Now().Add(servercfg.GetServerConfig().JwtValidityDuration)
+func CreateUserAccessJwtToken(username string, role models.UserRoleID, d time.Time, tokenID string) (response string, err error) {
 	claims := &models.UserClaims{
 	claims := &models.UserClaims{
 		UserName:       username,
 		UserName:       username,
 		Role:           role,
 		Role:           role,
-		RacAutoDisable: servercfg.GetRacAutoDisable() && (role != models.SuperAdminRole && role != models.AdminRole),
+		TokenType:      models.AccessTokenType,
+		Api:            servercfg.GetAPIHost(),
+		RacAutoDisable: GetRacAutoDisable() && (role != models.SuperAdminRole && role != models.AdminRole),
 		RegisteredClaims: jwt.RegisteredClaims{
 		RegisteredClaims: jwt.RegisteredClaims{
 			Issuer:    "Netmaker",
 			Issuer:    "Netmaker",
 			Subject:   fmt.Sprintf("user|%s", username),
 			Subject:   fmt.Sprintf("user|%s", username),
 			IssuedAt:  jwt.NewNumericDate(time.Now()),
 			IssuedAt:  jwt.NewNumericDate(time.Now()),
-			ExpiresAt: jwt.NewNumericDate(expirationTime),
+			ExpiresAt: jwt.NewNumericDate(d),
+			ID:        tokenID,
 		},
 		},
 	}
 	}
 
 
@@ -75,16 +80,29 @@ func CreateUserJWT(username string, role models.UserRoleID) (response string, er
 	return "", err
 	return "", err
 }
 }
 
 
-// VerifyJWT verifies Auth Header
-func VerifyJWT(bearerToken string) (username string, issuperadmin, isadmin bool, err error) {
-	token := ""
-	tokenSplit := strings.Split(bearerToken, " ")
-	if len(tokenSplit) > 1 {
-		token = tokenSplit[1]
-	} else {
-		return "", false, false, errors.New("invalid auth header")
+// CreateUserJWT - creates a user jwt token
+func CreateUserJWT(username string, role models.UserRoleID) (response string, err error) {
+	settings := GetServerSettings()
+	expirationTime := time.Now().Add(time.Duration(settings.JwtValidityDuration) * time.Minute)
+	claims := &models.UserClaims{
+		UserName:       username,
+		Role:           role,
+		TokenType:      models.UserIDTokenType,
+		RacAutoDisable: settings.RacAutoDisable && (role != models.SuperAdminRole && role != models.AdminRole),
+		RegisteredClaims: jwt.RegisteredClaims{
+			Issuer:    "Netmaker",
+			Subject:   fmt.Sprintf("user|%s", username),
+			IssuedAt:  jwt.NewNumericDate(time.Now()),
+			ExpiresAt: jwt.NewNumericDate(expirationTime),
+		},
+	}
+
+	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+	tokenString, err := token.SignedString(jwtSecretKey)
+	if err == nil {
+		return tokenString, nil
 	}
 	}
-	return VerifyUserToken(token)
+	return "", err
 }
 }
 
 
 func GetUserNameFromToken(authtoken string) (username string, err error) {
 func GetUserNameFromToken(authtoken string) (username string, err error) {
@@ -107,6 +125,20 @@ func GetUserNameFromToken(authtoken string) (username string, err error) {
 	if err != nil {
 	if err != nil {
 		return "", Unauthorized_Err
 		return "", Unauthorized_Err
 	}
 	}
+	if claims.TokenType == models.AccessTokenType {
+		jti := claims.ID
+		if jti != "" {
+			a := schema.UserAccessToken{ID: jti}
+			// check if access token is active
+			err := a.Get(db.WithContext(context.TODO()))
+			if err != nil {
+				err = errors.New("token revoked")
+				return "", err
+			}
+			a.LastUsed = time.Now().UTC()
+			a.Update(db.WithContext(context.TODO()))
+		}
+	}
 
 
 	if token != nil && token.Valid {
 	if token != nil && token.Valid {
 		var user *models.User
 		var user *models.User
@@ -139,7 +171,20 @@ func VerifyUserToken(tokenString string) (username string, issuperadmin, isadmin
 	token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
 	token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
 		return jwtSecretKey, nil
 		return jwtSecretKey, nil
 	})
 	})
-
+	if claims.TokenType == models.AccessTokenType {
+		jti := claims.ID
+		if jti != "" {
+			a := schema.UserAccessToken{ID: jti}
+			// check if access token is active
+			err := a.Get(db.WithContext(context.TODO()))
+			if err != nil {
+				err = errors.New("token revoked")
+				return "", false, false, err
+			}
+			a.LastUsed = time.Now().UTC()
+			a.Update(db.WithContext(context.TODO()))
+		}
+	}
 	if token != nil && token.Valid {
 	if token != nil && token.Valid {
 		var user *models.User
 		var user *models.User
 		// check that user exists
 		// check that user exists

+ 1 - 2
logic/networks.go

@@ -302,6 +302,7 @@ func CreateNetwork(network models.Network) (models.Network, error) {
 		true,
 		true,
 		uuid.Nil,
 		uuid.Nil,
 		true,
 		true,
+		false,
 	)
 	)
 
 
 	return network, nil
 	return network, nil
@@ -521,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" {
@@ -566,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)

+ 51 - 44
logic/peers.go

@@ -7,6 +7,7 @@ import (
 	"net/netip"
 	"net/netip"
 	"time"
 	"time"
 
 
+	"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"
@@ -48,16 +49,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
@@ -158,30 +162,20 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 		Peers:           []wgtypes.PeerConfig{},
 		Peers:           []wgtypes.PeerConfig{},
 		NodePeers:       []wgtypes.PeerConfig{},
 		NodePeers:       []wgtypes.PeerConfig{},
 		HostNetworkInfo: models.HostInfoMap{},
 		HostNetworkInfo: models.HostInfoMap{},
-		ServerConfig:    servercfg.ServerInfo,
+		ServerConfig:    GetServerInfo(),
 	}
 	}
 	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
 			}
 			}
+
 		}
 		}
 	}()
 	}()
 
 
@@ -190,6 +184,9 @@ 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
@@ -199,6 +196,7 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 			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)
@@ -206,13 +204,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
@@ -249,8 +256,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
@@ -437,7 +445,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(),
@@ -451,12 +459,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 {
@@ -494,7 +502,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
@@ -551,11 +558,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-- {
@@ -569,11 +576,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-- {
@@ -590,13 +597,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
 		}
 		}
 	}
 	}
@@ -625,11 +632,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)
@@ -671,13 +678,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)

+ 14 - 0
logic/security.go

@@ -1,6 +1,7 @@
 package logic
 package logic
 
 
 import (
 import (
+	"errors"
 	"net/http"
 	"net/http"
 	"strings"
 	"strings"
 
 
@@ -32,6 +33,19 @@ func SecurityCheck(reqAdmin bool, next http.Handler) http.HandlerFunc {
 			ReturnErrorResponse(w, r, FormatError(err, "unauthorized"))
 			ReturnErrorResponse(w, r, FormatError(err, "unauthorized"))
 			return
 			return
 		}
 		}
+
+		user, err := GetUser(username)
+		if err != nil {
+			ReturnErrorResponse(w, r, FormatError(err, "unauthorized"))
+			return
+		}
+
+		if user.AccountDisabled {
+			err = errors.New("user account disabled")
+			ReturnErrorResponse(w, r, FormatError(err, "unauthorized"))
+			return
+		}
+
 		// detect masteradmin
 		// detect masteradmin
 		if username == MasterUser {
 		if username == MasterUser {
 			r.Header.Set("ismaster", "yes")
 			r.Header.Set("ismaster", "yes")

+ 355 - 0
logic/settings.go

@@ -0,0 +1,355 @@
+package logic
+
+import (
+	"encoding/json"
+	"os"
+	"regexp"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/gravitl/netmaker/config"
+	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/servercfg"
+)
+
+var serverSettingsDBKey = "server_cfg"
+var SettingsMutex = &sync.RWMutex{}
+
+func GetServerSettings() (s models.ServerSettings) {
+	data, err := database.FetchRecord(database.SERVER_SETTINGS, serverSettingsDBKey)
+	if err != nil {
+		return
+	}
+	json.Unmarshal([]byte(data), &s)
+	return
+}
+
+func UpsertServerSettings(s models.ServerSettings) error {
+	// get curr settings
+	currSettings := GetServerSettings()
+	if s.ClientSecret == Mask() {
+		s.ClientSecret = currSettings.ClientSecret
+	}
+	data, err := json.Marshal(s)
+	if err != nil {
+		return err
+	}
+	err = database.Insert(serverSettingsDBKey, string(data), database.SERVER_SETTINGS)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func ValidateNewSettings(req models.ServerSettings) bool {
+	// TODO: add checks for different fields
+	return true
+}
+
+func GetServerSettingsFromEnv() (s models.ServerSettings) {
+
+	s = models.ServerSettings{
+		NetclientAutoUpdate:        servercfg.AutoUpdateEnabled(),
+		Verbosity:                  servercfg.GetVerbosity(),
+		AuthProvider:               os.Getenv("AUTH_PROVIDER"),
+		OIDCIssuer:                 os.Getenv("OIDC_ISSUER"),
+		ClientID:                   os.Getenv("CLIENT_ID"),
+		ClientSecret:               os.Getenv("CLIENT_SECRET"),
+		AzureTenant:                servercfg.GetAzureTenant(),
+		Telemetry:                  servercfg.Telemetry(),
+		BasicAuth:                  servercfg.IsBasicAuthEnabled(),
+		JwtValidityDuration:        servercfg.GetJwtValidityDurationFromEnv() / 60,
+		RacAutoDisable:             servercfg.GetRacAutoDisable(),
+		RacRestrictToSingleNetwork: servercfg.GetRacRestrictToSingleNetwork(),
+		EndpointDetection:          servercfg.IsEndpointDetectionEnabled(),
+		AllowedEmailDomains:        servercfg.GetAllowedEmailDomains(),
+		EmailSenderAddr:            servercfg.GetSenderEmail(),
+		EmailSenderUser:            servercfg.GetSenderUser(),
+		EmailSenderPassword:        servercfg.GetEmaiSenderPassword(),
+		SmtpHost:                   servercfg.GetSmtpHost(),
+		SmtpPort:                   servercfg.GetSmtpPort(),
+		MetricInterval:             servercfg.GetMetricInterval(),
+		MetricsPort:                servercfg.GetMetricsPort(),
+		ManageDNS:                  servercfg.GetManageDNS(),
+		DefaultDomain:              servercfg.GetDefaultDomain(),
+		Stun:                       servercfg.IsStunEnabled(),
+		StunServers:                servercfg.GetStunServers(),
+		TextSize:                   "16",
+		Theme:                      models.Dark,
+		ReducedMotion:              false,
+	}
+
+	return
+}
+
+// GetServerConfig - gets the server config into memory from file or env
+func GetServerConfig() config.ServerConfig {
+	var cfg config.ServerConfig
+	settings := GetServerSettings()
+	cfg.APIConnString = servercfg.GetAPIConnString()
+	cfg.CoreDNSAddr = servercfg.GetCoreDNSAddr()
+	cfg.APIHost = servercfg.GetAPIHost()
+	cfg.APIPort = servercfg.GetAPIPort()
+	cfg.MasterKey = "(hidden)"
+	cfg.DNSKey = "(hidden)"
+	cfg.AllowedOrigin = servercfg.GetAllowedOrigin()
+	cfg.RestBackend = "off"
+	cfg.NodeID = servercfg.GetNodeID()
+	cfg.BrokerType = servercfg.GetBrokerType()
+	cfg.EmqxRestEndpoint = servercfg.GetEmqxRestEndpoint()
+	if settings.NetclientAutoUpdate {
+		cfg.NetclientAutoUpdate = "enabled"
+	} else {
+		cfg.NetclientAutoUpdate = "disabled"
+	}
+	if servercfg.IsRestBackend() {
+		cfg.RestBackend = "on"
+	}
+	cfg.DNSMode = "off"
+	if servercfg.IsDNSMode() {
+		cfg.DNSMode = "on"
+	}
+	cfg.DisplayKeys = "off"
+	if servercfg.IsDisplayKeys() {
+		cfg.DisplayKeys = "on"
+	}
+	cfg.DisableRemoteIPCheck = "off"
+	if servercfg.DisableRemoteIPCheck() {
+		cfg.DisableRemoteIPCheck = "on"
+	}
+	cfg.Database = servercfg.GetDB()
+	cfg.Platform = servercfg.GetPlatform()
+	cfg.Version = servercfg.GetVersion()
+	cfg.PublicIp = servercfg.GetServerHostIP()
+
+	// == auth config ==
+	var authInfo = GetAuthProviderInfo(settings)
+	cfg.AuthProvider = authInfo[0]
+	cfg.ClientID = authInfo[1]
+	cfg.ClientSecret = authInfo[2]
+	cfg.FrontendURL = servercfg.GetFrontendURL()
+	cfg.AzureTenant = settings.AzureTenant
+	cfg.Telemetry = settings.Telemetry
+	cfg.Server = servercfg.GetServer()
+	cfg.Verbosity = settings.Verbosity
+	cfg.IsPro = "no"
+	if servercfg.IsPro {
+		cfg.IsPro = "yes"
+	}
+	cfg.JwtValidityDuration = time.Duration(settings.JwtValidityDuration) * time.Minute
+	cfg.RacAutoDisable = settings.RacAutoDisable
+	cfg.RacRestrictToSingleNetwork = settings.RacRestrictToSingleNetwork
+	cfg.MetricInterval = settings.MetricInterval
+	cfg.ManageDNS = settings.ManageDNS
+	cfg.Stun = settings.Stun
+	cfg.StunServers = settings.StunServers
+	cfg.DefaultDomain = settings.DefaultDomain
+	return cfg
+}
+
+// GetServerInfo - gets the server config into memory from file or env
+func GetServerInfo() models.ServerConfig {
+	var cfg models.ServerConfig
+	serverSettings := GetServerSettings()
+	cfg.Server = servercfg.GetServer()
+	if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
+		cfg.MQUserName = "HOST_ID"
+		cfg.MQPassword = "HOST_PASS"
+	} else {
+		cfg.MQUserName = servercfg.GetMqUserName()
+		cfg.MQPassword = servercfg.GetMqPassword()
+	}
+	cfg.API = servercfg.GetAPIConnString()
+	cfg.CoreDNSAddr = servercfg.GetCoreDNSAddr()
+	cfg.APIPort = servercfg.GetAPIPort()
+	cfg.DNSMode = "off"
+	cfg.Broker = servercfg.GetPublicBrokerEndpoint()
+	cfg.BrokerType = servercfg.GetBrokerType()
+	if servercfg.IsDNSMode() {
+		cfg.DNSMode = "on"
+	}
+	cfg.Version = servercfg.GetVersion()
+	cfg.IsPro = servercfg.IsPro
+	cfg.MetricInterval = serverSettings.MetricInterval
+	cfg.MetricsPort = serverSettings.MetricsPort
+	cfg.ManageDNS = serverSettings.ManageDNS
+	cfg.Stun = serverSettings.Stun
+	cfg.StunServers = serverSettings.StunServers
+	cfg.DefaultDomain = serverSettings.DefaultDomain
+	cfg.EndpointDetection = serverSettings.EndpointDetection
+	return cfg
+}
+
+// GetDefaultDomain - get the default domain
+func GetDefaultDomain() string {
+	return GetServerSettings().DefaultDomain
+}
+
+func ValidateDomain(domain string) bool {
+	domainPattern := `[a-zA-Z0-9][a-zA-Z0-9_-]{0,62}(\.[a-zA-Z0-9][a-zA-Z0-9_-]{0,62})*(\.[a-zA-Z][a-zA-Z0-9]{0,10}){1}`
+
+	exp := regexp.MustCompile("^" + domainPattern + "$")
+
+	return exp.MatchString(domain)
+}
+
+// Telemetry - checks if telemetry data should be sent
+func Telemetry() string {
+	return GetServerSettings().Telemetry
+}
+
+// GetJwtValidityDuration - returns the JWT validity duration in minutes
+func GetJwtValidityDuration() time.Duration {
+	return GetServerConfig().JwtValidityDuration
+}
+
+// GetRacAutoDisable - returns whether the feature to autodisable RAC is enabled
+func GetRacAutoDisable() bool {
+	return GetServerSettings().RacAutoDisable
+}
+
+// GetRacRestrictToSingleNetwork - returns whether the feature to allow simultaneous network connections via RAC is enabled
+func GetRacRestrictToSingleNetwork() bool {
+	return GetServerSettings().RacRestrictToSingleNetwork
+}
+
+func GetSmtpHost() string {
+	return GetServerSettings().SmtpHost
+}
+
+func GetSmtpPort() int {
+	return GetServerSettings().SmtpPort
+}
+
+func GetSenderEmail() string {
+	return GetServerSettings().EmailSenderAddr
+}
+
+func GetSenderUser() string {
+	return GetServerSettings().EmailSenderUser
+}
+
+func GetEmaiSenderPassword() string {
+	return GetServerSettings().EmailSenderPassword
+}
+
+// AutoUpdateEnabled returns a boolean indicating whether netclient auto update is enabled or disabled
+// default is enabled
+func AutoUpdateEnabled() bool {
+	return GetServerSettings().NetclientAutoUpdate
+}
+
+// GetAuthProviderInfo = gets the oauth provider info
+func GetAuthProviderInfo(settings models.ServerSettings) (pi []string) {
+	var authProvider = ""
+
+	defer func() {
+		if authProvider == "oidc" {
+			if settings.OIDCIssuer != "" {
+				pi = append(pi, settings.OIDCIssuer)
+			} else {
+				pi = []string{"", "", ""}
+			}
+		}
+	}()
+
+	if settings.AuthProvider != "" && settings.ClientID != "" && settings.ClientSecret != "" {
+		authProvider = strings.ToLower(settings.AuthProvider)
+		if authProvider == "google" || authProvider == "azure-ad" || authProvider == "github" || authProvider == "oidc" {
+			return []string{authProvider, settings.ClientID, settings.ClientSecret}
+		} else {
+			authProvider = ""
+		}
+	}
+	return []string{"", "", ""}
+}
+
+// GetAzureTenant - retrieve the azure tenant ID from env variable or config file
+func GetAzureTenant() string {
+	return GetServerSettings().AzureTenant
+}
+
+// IsSyncEnabled returns whether auth provider sync is enabled.
+func IsSyncEnabled() bool {
+	return GetServerSettings().SyncEnabled
+}
+
+// GetIDPSyncInterval returns the interval at which the netmaker should sync
+// data from IDP.
+func GetIDPSyncInterval() time.Duration {
+	syncInterval, err := time.ParseDuration(GetServerSettings().IDPSyncInterval)
+	if err != nil {
+		return 24 * time.Hour
+	}
+
+	if syncInterval == 0 {
+		return 24 * time.Hour
+	}
+
+	return syncInterval
+}
+
+// GetMetricsPort - get metrics port
+func GetMetricsPort() int {
+	return GetServerSettings().MetricsPort
+}
+
+// GetMetricInterval - get the publish metric interval
+func GetMetricIntervalInMinutes() time.Duration {
+	//default 15 minutes
+	mi := "15"
+	if os.Getenv("PUBLISH_METRIC_INTERVAL") != "" {
+		mi = os.Getenv("PUBLISH_METRIC_INTERVAL")
+	}
+	interval, err := strconv.Atoi(mi)
+	if err != nil {
+		interval = 15
+	}
+
+	return time.Duration(interval) * time.Minute
+}
+
+// GetMetricInterval - get the publish metric interval
+func GetMetricInterval() string {
+	return GetServerSettings().MetricInterval
+}
+
+// GetManageDNS - if manage DNS enabled or not
+func GetManageDNS() bool {
+	return GetServerSettings().ManageDNS
+}
+
+// IsBasicAuthEnabled - checks if basic auth has been configured to be turned off
+func IsBasicAuthEnabled() bool {
+	return GetServerSettings().BasicAuth
+}
+
+// IsEndpointDetectionEnabled - returns true if endpoint detection enabled
+func IsEndpointDetectionEnabled() bool {
+	return GetServerSettings().EndpointDetection
+}
+
+// IsStunEnabled - returns true if STUN set to on
+func IsStunEnabled() bool {
+	return GetServerSettings().Stun
+}
+
+func GetStunServers() string {
+	return GetServerSettings().StunServers
+}
+
+// GetAllowedEmailDomains - gets the allowed email domains for oauth signup
+func GetAllowedEmailDomains() string {
+	return GetServerSettings().AllowedEmailDomains
+}
+
+func GetVerbosity() int32 {
+	return GetServerSettings().Verbosity
+}
+
+func Mask() string {
+	return ("..................")
+}

+ 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 {

+ 8 - 2
logic/telemetry.go

@@ -7,6 +7,7 @@ import (
 
 
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
+
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/posthog/posthog-go"
 	"github.com/posthog/posthog-go"
@@ -19,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"
 
 
@@ -33,7 +36,7 @@ func SetFreeTierForTelemetry(freeTierFlag bool) {
 
 
 // sendTelemetry - gathers telemetry data and sends to posthog
 // sendTelemetry - gathers telemetry data and sends to posthog
 func sendTelemetry() error {
 func sendTelemetry() error {
-	if servercfg.Telemetry() == "off" {
+	if Telemetry() == "off" {
 		return nil
 		return nil
 	}
 	}
 
 
@@ -78,7 +81,8 @@ func sendTelemetry() error {
 			Set("pro_trial_end_date", d.ProTrialEndDate.In(time.UTC).Format("2006-01-02")).
 			Set("pro_trial_end_date", d.ProTrialEndDate.In(time.UTC).Format("2006-01-02")).
 			Set("admin_email", adminEmail).
 			Set("admin_email", adminEmail).
 			Set("email", adminEmail). // needed for posthog intgration with hubspot. "admin_email" can only be removed if not used in posthog
 			Set("email", adminEmail). // needed for posthog intgration with hubspot. "admin_email" can only be removed if not used in posthog
-			Set("is_saas_tenant", d.IsSaasTenant),
+			Set("is_saas_tenant", d.IsSaasTenant).
+			Set("domain", d.Domain),
 	})
 	})
 }
 }
 
 
@@ -102,6 +106,7 @@ func FetchTelemetryData() telemetryData {
 		data.IsProTrial = true
 		data.IsProTrial = true
 	}
 	}
 	data.IsSaasTenant = servercfg.DeployedByOperator()
 	data.IsSaasTenant = servercfg.DeployedByOperator()
+	data.Domain = servercfg.GetNmBaseDomain()
 	return data
 	return data
 }
 }
 
 
@@ -199,6 +204,7 @@ type telemetryData struct {
 	IsProTrial      bool
 	IsProTrial      bool
 	ProTrialEndDate time.Time
 	ProTrialEndDate time.Time
 	IsSaasTenant    bool
 	IsSaasTenant    bool
+	Domain          string
 }
 }
 
 
 // clientCount - What types of netclients we're tallying
 // clientCount - What types of netclients we're tallying

+ 4 - 1
logic/user_mgmt.go

@@ -50,6 +50,8 @@ var MigrateUserRoleAndGroups = func(u models.User) {
 
 
 }
 }
 
 
+var MigrateGroups = func() {}
+
 var UpdateUserGwAccess = func(currentUser, changeUser models.User) {}
 var UpdateUserGwAccess = func(currentUser, changeUser models.User) {}
 
 
 var UpdateRole = func(r models.UserRolePermissionTemplate) error { return nil }
 var UpdateRole = func(r models.UserRolePermissionTemplate) error { return nil }
@@ -61,7 +63,8 @@ var CreateDefaultNetworkRolesAndGroups = func(netID models.NetworkID) {}
 var CreateDefaultUserPolicies = func(netID models.NetworkID) {}
 var CreateDefaultUserPolicies = func(netID models.NetworkID) {}
 var GetUserGroupsInNetwork = func(netID models.NetworkID) (networkGrps map[models.UserGroupID]models.UserGroup) { return }
 var GetUserGroupsInNetwork = func(netID models.NetworkID) (networkGrps map[models.UserGroupID]models.UserGroup) { return }
 var GetUserGroup = func(groupId models.UserGroupID) (userGrps models.UserGroup, err error) { return }
 var GetUserGroup = func(groupId models.UserGroupID) (userGrps models.UserGroup, err error) { return }
-var AddGlobalNetRolesToAdmins = func(u models.User) {}
+var AddGlobalNetRolesToAdmins = func(u *models.User) {}
+var EmailInit = func() {}
 
 
 // GetRole - fetches role template by id
 // GetRole - fetches role template by id
 func GetRole(roleID models.UserRoleID) (models.UserRolePermissionTemplate, error) {
 func GetRole(roleID models.UserRoleID) (models.UserRolePermissionTemplate, error) {

+ 27 - 9
logic/users.go

@@ -41,13 +41,15 @@ func GetReturnUser(username string) (models.ReturnUser, error) {
 // ToReturnUser - gets a user as a return user
 // ToReturnUser - gets a user as a return user
 func ToReturnUser(user models.User) models.ReturnUser {
 func ToReturnUser(user models.User) models.ReturnUser {
 	return models.ReturnUser{
 	return models.ReturnUser{
-		UserName:       user.UserName,
-		PlatformRoleID: user.PlatformRoleID,
-		AuthType:       user.AuthType,
-		UserGroups:     user.UserGroups,
-		NetworkRoles:   user.NetworkRoles,
-		RemoteGwIDs:    user.RemoteGwIDs,
-		LastLoginTime:  user.LastLoginTime,
+		UserName:        user.UserName,
+		DisplayName:     user.DisplayName,
+		AccountDisabled: user.AccountDisabled,
+		AuthType:        user.AuthType,
+		RemoteGwIDs:     user.RemoteGwIDs,
+		UserGroups:      user.UserGroups,
+		PlatformRoleID:  user.PlatformRoleID,
+		NetworkRoles:    user.NetworkRoles,
+		LastLoginTime:   user.LastLoginTime,
 	}
 	}
 }
 }
 
 
@@ -78,7 +80,7 @@ func GetSuperAdmin() (models.ReturnUser, error) {
 		return models.ReturnUser{}, err
 		return models.ReturnUser{}, err
 	}
 	}
 	for _, user := range users {
 	for _, user := range users {
-		if user.IsSuperAdmin {
+		if user.IsSuperAdmin || user.PlatformRoleID == models.SuperAdminRole {
 			return user, nil
 			return user, nil
 		}
 		}
 	}
 	}
@@ -113,7 +115,7 @@ func IsPendingUser(username string) bool {
 	return false
 	return false
 }
 }
 
 
-func ListPendingUsers() ([]models.ReturnUser, error) {
+func ListPendingReturnUsers() ([]models.ReturnUser, error) {
 	pendingUsers := []models.ReturnUser{}
 	pendingUsers := []models.ReturnUser{}
 	records, err := database.FetchRecords(database.PENDING_USERS_TABLE_NAME)
 	records, err := database.FetchRecords(database.PENDING_USERS_TABLE_NAME)
 	if err != nil && !database.IsEmptyRecord(err) {
 	if err != nil && !database.IsEmptyRecord(err) {
@@ -129,6 +131,22 @@ func ListPendingUsers() ([]models.ReturnUser, error) {
 	return pendingUsers, nil
 	return pendingUsers, nil
 }
 }
 
 
+func ListPendingUsers() ([]models.User, error) {
+	var pendingUsers []models.User
+	records, err := database.FetchRecords(database.PENDING_USERS_TABLE_NAME)
+	if err != nil && !database.IsEmptyRecord(err) {
+		return pendingUsers, err
+	}
+	for _, record := range records {
+		var u models.User
+		err = json.Unmarshal([]byte(record), &u)
+		if err == nil {
+			pendingUsers = append(pendingUsers, u)
+		}
+	}
+	return pendingUsers, nil
+}
+
 func GetUserMap() (map[string]models.User, error) {
 func GetUserMap() (map[string]models.User, error) {
 	userMap := make(map[string]models.User)
 	userMap := make(map[string]models.User)
 	records, err := database.FetchRecords(database.USERS_TABLE_NAME)
 	records, err := database.FetchRecords(database.USERS_TABLE_NAME)

+ 21 - 0
logic/util.go

@@ -10,6 +10,7 @@ import (
 	"log/slog"
 	"log/slog"
 	"net"
 	"net"
 	"os"
 	"os"
+	"reflect"
 	"strings"
 	"strings"
 	"time"
 	"time"
 	"unicode"
 	"unicode"
@@ -201,3 +202,23 @@ func VersionLessThan(v1, v2 string) (bool, error) {
 	}
 	}
 	return sv1.LT(sv2), nil
 	return sv1.LT(sv2), nil
 }
 }
+
+// Compare any two maps with any key and value types
+func CompareMaps[K comparable, V any](a, b map[K]V) bool {
+	if len(a) != len(b) {
+		return false
+	}
+
+	for key, valA := range a {
+		valB, ok := b[key]
+		if !ok {
+			return false
+		}
+
+		if !reflect.DeepEqual(valA, valB) {
+			return false
+		}
+	}
+
+	return true
+}

+ 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

+ 50 - 1
main.go

@@ -3,6 +3,8 @@ package main
 
 
 import (
 import (
 	"context"
 	"context"
+	"crypto/rand"
+	"encoding/json"
 	"flag"
 	"flag"
 	"fmt"
 	"fmt"
 	"os"
 	"os"
@@ -12,9 +14,11 @@ import (
 	"sync"
 	"sync"
 	"syscall"
 	"syscall"
 
 
+	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/config"
 	"github.com/gravitl/netmaker/config"
 	controller "github.com/gravitl/netmaker/controllers"
 	controller "github.com/gravitl/netmaker/controllers"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/functions"
 	"github.com/gravitl/netmaker/functions"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
@@ -22,9 +26,11 @@ import (
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/netclient/ncutils"
 	"github.com/gravitl/netmaker/netclient/ncutils"
+	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/serverctl"
 	"github.com/gravitl/netmaker/serverctl"
 	_ "go.uber.org/automaxprocs"
 	_ "go.uber.org/automaxprocs"
+	"golang.org/x/crypto/nacl/box"
 	"golang.org/x/exp/slog"
 	"golang.org/x/exp/slog"
 )
 )
 
 
@@ -99,8 +105,13 @@ func initialize() { // Client Mode Prereq Check
 	if err = database.InitializeDatabase(); err != nil {
 	if err = database.InitializeDatabase(); err != nil {
 		logger.FatalLog("Error connecting to database: ", err.Error())
 		logger.FatalLog("Error connecting to database: ", err.Error())
 	}
 	}
+	// initialize sql schema db.
+	err = db.InitializeDB(schema.ListModels()...)
+	if err != nil {
+		logger.FatalLog("Error connecting to v1 database: ", err.Error())
+	}
 	logger.Log(0, "database successfully connected")
 	logger.Log(0, "database successfully connected")
-
+	initializeUUID()
 	//initialize cache
 	//initialize cache
 	_, _ = logic.GetNetworks()
 	_, _ = logic.GetNetworks()
 	_, _ = logic.GetAllNodes()
 	_, _ = logic.GetAllNodes()
@@ -247,3 +258,41 @@ func setGarbageCollection() {
 		debug.SetGCPercent(ncutils.DEFAULT_GC_PERCENT)
 		debug.SetGCPercent(ncutils.DEFAULT_GC_PERCENT)
 	}
 	}
 }
 }
+
+// initializeUUID - create a UUID record for server if none exists
+func initializeUUID() error {
+	records, err := database.FetchRecords(database.SERVER_UUID_TABLE_NAME)
+	if err != nil {
+		if !database.IsEmptyRecord(err) {
+			return err
+		}
+	} else if len(records) > 0 {
+		return nil
+	}
+	// setup encryption keys
+	var trafficPubKey, trafficPrivKey, errT = box.GenerateKey(rand.Reader) // generate traffic keys
+	if errT != nil {
+		return errT
+	}
+	tPriv, err := ncutils.ConvertKeyToBytes(trafficPrivKey)
+	if err != nil {
+		return err
+	}
+
+	tPub, err := ncutils.ConvertKeyToBytes(trafficPubKey)
+	if err != nil {
+		return err
+	}
+
+	telemetry := models.Telemetry{
+		UUID:           uuid.NewString(),
+		TrafficKeyPriv: tPriv,
+		TrafficKeyPub:  tPub,
+	}
+	telJSON, err := json.Marshal(&telemetry)
+	if err != nil {
+		return err
+	}
+
+	return database.Insert(database.SERVER_UUID_RECORD_KEY, string(telJSON), database.SERVER_UUID_TABLE_NAME)
+}

+ 236 - 4
migrate/migrate.go

@@ -1,34 +1,41 @@
 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"
 )
 )
 
 
 // Run - runs all migrations
 // Run - runs all migrations
 func Run() {
 func Run() {
+	settings()
 	updateEnrollmentKeys()
 	updateEnrollmentKeys()
 	assignSuperAdmin()
 	assignSuperAdmin()
 	createDefaultTagsAndPolicies()
 	createDefaultTagsAndPolicies()
 	removeOldUserGrps()
 	removeOldUserGrps()
+	syncGroups()
 	syncUsers()
 	syncUsers()
 	updateHosts()
 	updateHosts()
 	updateNodes()
 	updateNodes()
 	updateAcls()
 	updateAcls()
 	migrateToGws()
 	migrateToGws()
+	migrateToEgressV1()
 }
 }
 
 
 func assignSuperAdmin() {
 func assignSuperAdmin() {
@@ -151,6 +158,7 @@ func updateEnrollmentKeys() {
 			true,
 			true,
 			uuid.Nil,
 			uuid.Nil,
 			true,
 			true,
+			false,
 		)
 		)
 
 
 	}
 	}
@@ -386,6 +394,10 @@ func MigrateEmqx() {
 
 
 }
 }
 
 
+func syncGroups() {
+	logic.MigrateGroups()
+}
+
 func syncUsers() {
 func syncUsers() {
 	// create default network user roles for existing networks
 	// create default network user roles for existing networks
 	if servercfg.IsPro {
 	if servercfg.IsPro {
@@ -405,11 +417,12 @@ func syncUsers() {
 			}
 			}
 			if user.PlatformRoleID == models.SuperAdminRole && !user.IsSuperAdmin {
 			if user.PlatformRoleID == models.SuperAdminRole && !user.IsSuperAdmin {
 				user.IsSuperAdmin = true
 				user.IsSuperAdmin = true
-				logic.UpsertUser(user)
+
 			}
 			}
 			if user.PlatformRoleID.String() != "" {
 			if user.PlatformRoleID.String() != "" {
 				logic.MigrateUserRoleAndGroups(user)
 				logic.MigrateUserRoleAndGroups(user)
-				logic.AddGlobalNetRolesToAdmins(user)
+				logic.AddGlobalNetRolesToAdmins(&user)
+				logic.UpsertUser(user)
 				continue
 				continue
 			}
 			}
 			user.AuthType = models.BasicAuth
 			user.AuthType = models.BasicAuth
@@ -430,9 +443,9 @@ func syncUsers() {
 			} else {
 			} else {
 				user.PlatformRoleID = models.ServiceUser
 				user.PlatformRoleID = models.ServiceUser
 			}
 			}
-			logic.UpsertUser(user)
-			logic.AddGlobalNetRolesToAdmins(user)
+			logic.AddGlobalNetRolesToAdmins(&user)
 			logic.MigrateUserRoleAndGroups(user)
 			logic.MigrateUserRoleAndGroups(user)
+			logic.UpsertUser(user)
 		}
 		}
 	}
 	}
 
 
@@ -496,3 +509,222 @@ func migrateToGws() {
 		logic.DeleteTag(models.TagID(fmt.Sprintf("%s.%s", netI.NetID, models.OldRemoteAccessTagName)), true)
 		logic.DeleteTag(models.TagID(fmt.Sprintf("%s.%s", netI.NetID, models.OldRemoteAccessTagName)), true)
 	}
 	}
 }
 }
+
+func migrateToEgressV1() {
+	nodes, _ := logic.GetAllNodes()
+	user, err := logic.GetSuperAdmin()
+	if err != nil {
+		return
+	}
+	for _, node := range nodes {
+		if node.IsEgressGateway {
+			egressHost, err := logic.GetHost(node.HostID.String())
+			if err != nil {
+				continue
+			}
+			for _, rangeI := range node.EgressGatewayRequest.Ranges {
+				e := schema.Egress{
+					ID:          uuid.New().String(),
+					Name:        fmt.Sprintf("%s egress", egressHost.Name),
+					Description: "",
+					Network:     node.Network,
+					Nodes: datatypes.JSONMap{
+						node.ID.String(): 256,
+					},
+					Tags:      make(datatypes.JSONMap),
+					Range:     rangeI,
+					Nat:       node.EgressGatewayRequest.NatEnabled == "yes",
+					Status:    true,
+					CreatedBy: user.UserName,
+					CreatedAt: time.Now().UTC(),
+				}
+				err = e.Create(db.WithContext(context.TODO()))
+				if err == nil {
+					node.IsEgressGateway = false
+					node.EgressGatewayRequest = models.EgressGatewayRequest{}
+					node.EgressGatewayNatEnabled = false
+					node.EgressGatewayRanges = []string{}
+					logic.UpsertNode(&node)
+					acl := models.Acl{
+						ID:          uuid.New().String(),
+						Name:        "egress node policy",
+						MetaData:    "",
+						Default:     false,
+						ServiceType: models.Any,
+						NetworkID:   models.NetworkID(node.Network),
+						Proto:       models.ALL,
+						RuleType:    models.DevicePolicy,
+						Src: []models.AclPolicyTag{
+
+							{
+								ID:    models.NodeTagID,
+								Value: "*",
+							},
+						},
+						Dst: []models.AclPolicyTag{
+							{
+								ID:    models.EgressID,
+								Value: e.ID,
+							},
+						},
+
+						AllowedDirection: models.TrafficDirectionUni,
+						Enabled:          true,
+						CreatedBy:        "auto",
+						CreatedAt:        time.Now().UTC(),
+					}
+					logic.InsertAcl(acl)
+					acl = models.Acl{
+						ID:          uuid.New().String(),
+						Name:        "egress node policy",
+						MetaData:    "",
+						Default:     false,
+						ServiceType: models.Any,
+						NetworkID:   models.NetworkID(node.Network),
+						Proto:       models.ALL,
+						RuleType:    models.UserPolicy,
+						Src: []models.AclPolicyTag{
+
+							{
+								ID:    models.UserGroupAclID,
+								Value: "*",
+							},
+						},
+						Dst: []models.AclPolicyTag{
+							{
+								ID:    models.EgressID,
+								Value: e.ID,
+							},
+						},
+
+						AllowedDirection: models.TrafficDirectionUni,
+						Enabled:          true,
+						CreatedBy:        "auto",
+						CreatedAt:        time.Now().UTC(),
+					}
+					logic.InsertAcl(acl)
+				}
+
+			}
+
+		}
+
+		if node.IsInternetGateway {
+			inetHost, err := logic.GetHost(node.HostID.String())
+			if err != nil {
+				continue
+			}
+			e := schema.Egress{
+				ID:          uuid.New().String(),
+				Name:        fmt.Sprintf("%s inet gw", inetHost.Name),
+				Description: "add description",
+				Network:     node.Network,
+				Nodes: datatypes.JSONMap{
+					node.ID.String(): 256,
+				},
+				Tags:      make(datatypes.JSONMap),
+				Range:     "",
+				IsInetGw:  true,
+				Nat:       node.EgressGatewayRequest.NatEnabled == "yes",
+				Status:    true,
+				CreatedBy: user.UserName,
+				CreatedAt: time.Now().UTC(),
+			}
+			err = e.Create(db.WithContext(context.TODO()))
+			if err == nil {
+				node.IsEgressGateway = false
+				node.EgressGatewayRequest = models.EgressGatewayRequest{}
+				node.EgressGatewayNatEnabled = false
+				node.EgressGatewayRanges = []string{}
+				node.IsInternetGateway = false
+				src := []models.AclPolicyTag{}
+				for _, inetClientID := range node.InetNodeReq.InetNodeClientIDs {
+					_, err := logic.GetNodeByID(inetClientID)
+					if err == nil {
+						src = append(src, models.AclPolicyTag{
+							ID:    models.NodeID,
+							Value: inetClientID,
+						})
+					}
+				}
+				acl := models.Acl{
+					ID:          uuid.New().String(),
+					Name:        "exit node policy",
+					MetaData:    "all traffic on source nodes will pass through the destination node in the policy",
+					Default:     false,
+					ServiceType: models.Any,
+					NetworkID:   models.NetworkID(node.Network),
+					Proto:       models.ALL,
+					RuleType:    models.DevicePolicy,
+					Src:         src,
+					Dst: []models.AclPolicyTag{
+						{
+							ID:    models.EgressID,
+							Value: e.ID,
+						},
+					},
+
+					AllowedDirection: models.TrafficDirectionBi,
+					Enabled:          true,
+					CreatedBy:        "auto",
+					CreatedAt:        time.Now().UTC(),
+				}
+				logic.InsertAcl(acl)
+
+				acl = models.Acl{
+					ID:          uuid.New().String(),
+					Name:        "exit node policy",
+					MetaData:    "all traffic on source nodes will pass through the destination node in the policy",
+					Default:     false,
+					ServiceType: models.Any,
+					NetworkID:   models.NetworkID(node.Network),
+					Proto:       models.ALL,
+					RuleType:    models.UserPolicy,
+					Src: []models.AclPolicyTag{
+						{
+							ID:    models.UserGroupAclID,
+							Value: fmt.Sprintf("%s-%s-grp", node.Network, models.NetworkAdmin),
+						},
+						{
+							ID:    models.UserGroupAclID,
+							Value: fmt.Sprintf("global-%s-grp", models.NetworkAdmin),
+						},
+						{
+							ID:    models.UserGroupAclID,
+							Value: fmt.Sprintf("%s-%s-grp", node.Network, models.NetworkUser),
+						},
+						{
+							ID:    models.UserGroupAclID,
+							Value: fmt.Sprintf("global-%s-grp", models.NetworkUser),
+						},
+					},
+					Dst: []models.AclPolicyTag{
+						{
+							ID:    models.EgressID,
+							Value: e.ID,
+						},
+					},
+
+					AllowedDirection: models.TrafficDirectionBi,
+					Enabled:          true,
+					CreatedBy:        "auto",
+					CreatedAt:        time.Now().UTC(),
+				}
+				logic.InsertAcl(acl)
+				node.InetNodeReq = models.InetNodeReq{}
+				logic.UpsertNode(&node)
+			}
+		}
+		if node.InternetGwID != "" {
+			node.InternetGwID = ""
+			logic.UpsertNode(&node)
+		}
+	}
+}
+
+func settings() {
+	_, err := database.FetchRecords(database.SERVER_SETTINGS)
+	if database.IsEmptyRecord(err) {
+		logic.UpsertServerSettings(logic.GetServerSettingsFromEnv())
+	}
+}

+ 183 - 0
migrate/migrate_schema.go

@@ -0,0 +1,183 @@
+package migrate
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/db"
+	"github.com/gravitl/netmaker/schema"
+	"github.com/gravitl/netmaker/servercfg"
+	"gorm.io/gorm"
+	"os"
+	"path/filepath"
+)
+
+// ToSQLSchema migrates the data from key-value
+// db to sql db.
+//
+// This function archives the old data and does not
+// delete it.
+//
+// Based on the db server, the archival is done in the
+// following way:
+//
+// 1. Sqlite: Moves the old data to a
+// netmaker_archive.db file.
+//
+// 2. Postgres: Moves the data to a netmaker_archive
+// schema within the same database.
+func ToSQLSchema() error {
+	// initialize sql schema db.
+	err := db.InitializeDB(schema.ListModels()...)
+	if err != nil {
+		return err
+	}
+
+	// migrate, if not done already.
+	err = migrate()
+	if err != nil {
+		return err
+	}
+
+	// archive key-value schema db, if not done already.
+	// ignore errors.
+	_ = archive()
+
+	return nil
+}
+
+func migrate() error {
+	// begin a new transaction.
+	dbctx := db.BeginTx(context.TODO())
+	commit := false
+	defer func() {
+		if commit {
+			db.FromContext(dbctx).Commit()
+		} else {
+			db.FromContext(dbctx).Rollback()
+		}
+	}()
+
+	// check if migrated already.
+	migrationJob := &schema.Job{
+		ID: "migration-v1.0.0",
+	}
+	err := migrationJob.Get(dbctx)
+	if err != nil {
+		if !errors.Is(err, gorm.ErrRecordNotFound) {
+			return err
+		}
+
+		// initialize key-value schema db.
+		err := database.InitializeDatabase()
+		if err != nil {
+			return err
+		}
+		defer database.CloseDB()
+
+		// migrate.
+		// TODO: add migration code.
+
+		// mark migration job completed.
+		err = migrationJob.Create(dbctx)
+		if err != nil {
+			return err
+		}
+
+		commit = true
+	}
+
+	return nil
+}
+
+func archive() error {
+	dbServer := servercfg.GetDB()
+	if dbServer != "sqlite" && dbServer != "postgres" {
+		return nil
+	}
+
+	// begin a new transaction.
+	dbctx := db.BeginTx(context.TODO())
+	commit := false
+	defer func() {
+		if commit {
+			db.FromContext(dbctx).Commit()
+		} else {
+			db.FromContext(dbctx).Rollback()
+		}
+	}()
+
+	// check if key-value schema db archived already.
+	archivalJob := &schema.Job{
+		ID: "archival-v1.0.0",
+	}
+	err := archivalJob.Get(dbctx)
+	if err != nil {
+		if !errors.Is(err, gorm.ErrRecordNotFound) {
+			return err
+		}
+
+		// archive.
+		switch dbServer {
+		case "sqlite":
+			err = sqliteArchiveOldData()
+		default:
+			err = pgArchiveOldData()
+		}
+		if err != nil {
+			return err
+		}
+
+		// mark archival job completed.
+		err = archivalJob.Create(dbctx)
+		if err != nil {
+			return err
+		}
+
+		commit = true
+	} else {
+		// remove the residual
+		if dbServer == "sqlite" {
+			_ = os.Remove(filepath.Join("data", "netmaker.db"))
+		}
+	}
+
+	return nil
+}
+
+func sqliteArchiveOldData() error {
+	oldDBFilePath := filepath.Join("data", "netmaker.db")
+	archiveDBFilePath := filepath.Join("data", "netmaker_archive.db")
+
+	// check if netmaker_archive.db exist.
+	_, err := os.Stat(archiveDBFilePath)
+	if err == nil {
+		return nil
+	} else if !os.IsNotExist(err) {
+		return err
+	}
+
+	// rename old db file to netmaker_archive.db.
+	return os.Rename(oldDBFilePath, archiveDBFilePath)
+}
+
+func pgArchiveOldData() error {
+	_, err := database.PGDB.Exec("CREATE SCHEMA IF NOT EXISTS netmaker_archive")
+	if err != nil {
+		return err
+	}
+
+	for _, table := range database.Tables {
+		_, err := database.PGDB.Exec(
+			fmt.Sprintf(
+				"ALTER TABLE public.%s SET SCHEMA netmaker_archive",
+				table,
+			),
+		)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}

+ 55 - 8
models/accessToken.go

@@ -1,13 +1,60 @@
 package models
 package models
 
 
-// AccessToken - token used to access netmaker
-type AccessToken struct {
-	APIConnString string `json:"apiconnstring"`
-	ClientConfig
+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
 }
 }
 
 
-// ClientConfig - the config of the client
-type ClientConfig struct {
-	Network string `json:"network"`
-	Key     string `json:"key"`
+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"`
+}

+ 2 - 0
models/enrollment_key.go

@@ -54,6 +54,7 @@ type EnrollmentKey struct {
 	Relay         uuid.UUID `json:"relay"`
 	Relay         uuid.UUID `json:"relay"`
 	Groups        []TagID   `json:"groups"`
 	Groups        []TagID   `json:"groups"`
 	Default       bool      `json:"default"`
 	Default       bool      `json:"default"`
+	AutoEgress    bool      `json:"auto_egress"`
 }
 }
 
 
 // APIEnrollmentKey - used to create enrollment keys via API
 // APIEnrollmentKey - used to create enrollment keys via API
@@ -66,6 +67,7 @@ type APIEnrollmentKey struct {
 	Type          KeyType  `json:"type"`
 	Type          KeyType  `json:"type"`
 	Relay         string   `json:"relay"`
 	Relay         string   `json:"relay"`
 	Groups        []TagID  `json:"groups"`
 	Groups        []TagID  `json:"groups"`
+	AutoEgress    bool     `json:"auto_egress"`
 }
 }
 
 
 // RegisterResponse - the response to a successful enrollment register
 // RegisterResponse - the response to a successful enrollment register

+ 78 - 0
models/events.go

@@ -0,0 +1,78 @@
+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"
+	RefreshKey        Action = "REFRESH_KEY"
+	RefreshAllKeys    Action = "REFRESH_ALL_KEYS"
+	SyncAll           Action = "SYNC_ALL"
+	UpgradeAll        Action = "UPGRADE_ALL"
+	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

+ 46 - 0
models/settings.go

@@ -0,0 +1,46 @@
+package models
+
+type Theme string
+
+const (
+	Dark   Theme = "dark"
+	Light  Theme = "light"
+	System Theme = "system"
+)
+
+type ServerSettings struct {
+	NetclientAutoUpdate        bool   `json:"netclientautoupdate"`
+	Verbosity                  int32  `json:"verbosity"`
+	AuthProvider               string `json:"authprovider"`
+	OIDCIssuer                 string `json:"oidcissuer"`
+	ClientID                   string `json:"client_id"`
+	ClientSecret               string `json:"client_secret"`
+	SyncEnabled                bool     `json:"sync_enabled"`
+	GoogleAdminEmail           string   `json:"google_admin_email"`
+	GoogleSACredsJson          string   `json:"google_sa_creds_json"`
+	AzureTenant                string `json:"azure_tenant"`
+	UserFilters                []string `json:"user_filters"`
+	GroupFilters               []string `json:"group_filters"`
+	IDPSyncInterval            string   `json:"idp_sync_interval"`
+	Telemetry                  string `json:"telemetry"`
+	BasicAuth                  bool   `json:"basic_auth"`
+	JwtValidityDuration        int    `json:"jwt_validity_duration"`
+	RacAutoDisable             bool   `json:"rac_auto_disable"`
+	RacRestrictToSingleNetwork bool   `json:"rac_restrict_to_single_network"`
+	EndpointDetection          bool   `json:"endpoint_detection"`
+	AllowedEmailDomains        string `json:"allowed_email_domains"`
+	EmailSenderAddr            string `json:"email_sender_addr"`
+	EmailSenderUser            string `json:"email_sender_user"`
+	EmailSenderPassword        string `json:"email_sender_password"`
+	SmtpHost                   string `json:"smtp_host"`
+	SmtpPort                   int    `json:"smtp_port"`
+	MetricInterval             string `json:"metric_interval"`
+	MetricsPort                int    `json:"metrics_port"`
+	ManageDNS                  bool   `json:"manage_dns"`
+	DefaultDomain              string `json:"default_domain"`
+	Stun                       bool   `json:"stun"`
+	StunServers                string `json:"stun_servers"`
+	Theme                      Theme  `json:"theme"`
+	TextSize                   string `json:"text_size"`
+	ReducedMotion              bool   `json:"reduced_motion"`
+}

+ 2 - 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
@@ -263,6 +264,7 @@ type NodeJoinResponse struct {
 type ServerConfig struct {
 type ServerConfig struct {
 	CoreDNSAddr       string `yaml:"corednsaddr"`
 	CoreDNSAddr       string `yaml:"corednsaddr"`
 	API               string `yaml:"api"`
 	API               string `yaml:"api"`
+	APIHost           string `yaml:"apihost"`
 	APIPort           string `yaml:"apiport"`
 	APIPort           string `yaml:"apiport"`
 	DNSMode           string `yaml:"dnsmode"`
 	DNSMode           string `yaml:"dnsmode"`
 	Version           string `yaml:"version"`
 	Version           string `yaml:"version"`

+ 32 - 14
models/user_mgmt.go

@@ -13,6 +13,7 @@ type RsrcID string
 type UserRoleID string
 type UserRoleID string
 type UserGroupID string
 type UserGroupID string
 type AuthType string
 type AuthType string
+type TokenType string
 
 
 var (
 var (
 	BasicAuth AuthType = "basic_auth"
 	BasicAuth AuthType = "basic_auth"
@@ -35,6 +36,15 @@ func GetRAGRoleID(netID, hostID string) UserRoleID {
 	return UserRoleID(fmt.Sprintf("netID-%s-rag-%s", netID, hostID))
 	return UserRoleID(fmt.Sprintf("netID-%s-rag-%s", netID, hostID))
 }
 }
 
 
+func (t TokenType) String() string {
+	return string(t)
+}
+
+var (
+	UserIDTokenType TokenType = "user_id_token"
+	AccessTokenType TokenType = "access_token"
+)
+
 var RsrcTypeMap = map[RsrcType]struct{}{
 var RsrcTypeMap = map[RsrcType]struct{}{
 	HostRsrc:           {},
 	HostRsrc:           {},
 	RelayRsrc:          {},
 	RelayRsrc:          {},
@@ -134,17 +144,20 @@ type CreateGroupReq struct {
 }
 }
 
 
 type UserGroup struct {
 type UserGroup struct {
-	ID           UserGroupID                           `json:"id"`
-	Default      bool                                  `json:"default"`
-	Name         string                                `json:"name"`
-	NetworkRoles map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
-	MetaData     string                                `json:"meta_data"`
+	ID                         UserGroupID                           `json:"id"`
+	ExternalIdentityProviderID string                                `json:"external_identity_provider_id"`
+	Default                    bool                                  `json:"default"`
+	Name                       string                                `json:"name"`
+	NetworkRoles               map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
+	MetaData                   string                                `json:"meta_data"`
 }
 }
 
 
 // User struct - struct for Users
 // User struct - struct for Users
 type User struct {
 type User struct {
 	UserName                   string                                `json:"username" bson:"username" validate:"min=3,in_charset|email"`
 	UserName                   string                                `json:"username" bson:"username" validate:"min=3,in_charset|email"`
 	ExternalIdentityProviderID string                                `json:"external_identity_provider_id"`
 	ExternalIdentityProviderID string                                `json:"external_identity_provider_id"`
+	DisplayName                string                                `json:"display_name"`
+	AccountDisabled            bool                                  `json:"account_disabled"`
 	Password                   string                                `json:"password" bson:"password" validate:"required,min=5"`
 	Password                   string                                `json:"password" bson:"password" validate:"required,min=5"`
 	IsAdmin                    bool                                  `json:"isadmin" bson:"isadmin"` // deprecated
 	IsAdmin                    bool                                  `json:"isadmin" bson:"isadmin"` // deprecated
 	IsSuperAdmin               bool                                  `json:"issuperadmin"`           // deprecated
 	IsSuperAdmin               bool                                  `json:"issuperadmin"`           // deprecated
@@ -164,15 +177,18 @@ type ReturnUserWithRolesAndGroups struct {
 
 
 // ReturnUser - return user struct
 // ReturnUser - return user struct
 type ReturnUser struct {
 type ReturnUser struct {
-	UserName       string                                `json:"username"`
-	IsAdmin        bool                                  `json:"isadmin"`
-	IsSuperAdmin   bool                                  `json:"issuperadmin"`
-	AuthType       AuthType                              `json:"auth_type"`
-	RemoteGwIDs    map[string]struct{}                   `json:"remote_gw_ids"` // deprecated
-	UserGroups     map[UserGroupID]struct{}              `json:"user_group_ids"`
-	PlatformRoleID UserRoleID                            `json:"platform_role_id"`
-	NetworkRoles   map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
-	LastLoginTime  time.Time                             `json:"last_login_time"`
+	UserName                   string                                `json:"username"`
+	ExternalIdentityProviderID string                                `json:"external_identity_provider_id"`
+	DisplayName                string                                `json:"display_name"`
+	AccountDisabled            bool                                  `json:"account_disabled"`
+	IsAdmin                    bool                                  `json:"isadmin"`
+	IsSuperAdmin               bool                                  `json:"issuperadmin"`
+	AuthType                   AuthType                              `json:"auth_type"`
+	RemoteGwIDs                map[string]struct{}                   `json:"remote_gw_ids"` // deprecated
+	UserGroups                 map[UserGroupID]struct{}              `json:"user_group_ids"`
+	PlatformRoleID             UserRoleID                            `json:"platform_role_id"`
+	NetworkRoles               map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
+	LastLoginTime              time.Time                             `json:"last_login_time"`
 }
 }
 
 
 // UserAuthParams - user auth params struct
 // UserAuthParams - user auth params struct
@@ -185,6 +201,8 @@ type UserAuthParams struct {
 type UserClaims struct {
 type UserClaims struct {
 	Role           UserRoleID
 	Role           UserRoleID
 	UserName       string
 	UserName       string
+	Api            string
+	TokenType      TokenType
 	RacAutoDisable bool
 	RacAutoDisable bool
 	jwt.RegisteredClaims
 	jwt.RegisteredClaims
 }
 }

+ 1 - 1
mq/publishers.go

@@ -21,7 +21,7 @@ func PublishPeerUpdate(replacePeers bool) error {
 		return nil
 		return nil
 	}
 	}
 
 
-	if servercfg.GetManageDNS() {
+	if logic.GetManageDNS() {
 		sendDNSSync()
 		sendDNSSync()
 	}
 	}
 
 

+ 15 - 3
pro/auth/auth.go

@@ -34,6 +34,7 @@ const (
 
 
 // OAuthUser - generic OAuth strategy user
 // OAuthUser - generic OAuth strategy user
 type OAuthUser struct {
 type OAuthUser struct {
+	ID                string `json:"id" bson:"id"`
 	Name              string `json:"name" bson:"name"`
 	Name              string `json:"name" bson:"name"`
 	Email             string `json:"email" bson:"email"`
 	Email             string `json:"email" bson:"email"`
 	Login             string `json:"login" bson:"login"`
 	Login             string `json:"login" bson:"login"`
@@ -47,7 +48,7 @@ var (
 )
 )
 
 
 func getCurrentAuthFunctions() map[string]interface{} {
 func getCurrentAuthFunctions() map[string]interface{} {
-	var authInfo = servercfg.GetAuthProviderInfo()
+	var authInfo = logic.GetAuthProviderInfo(logic.GetServerSettings())
 	var authProvider = authInfo[0]
 	var authProvider = authInfo[0]
 	switch authProvider {
 	switch authProvider {
 	case google_provider_name:
 	case google_provider_name:
@@ -63,6 +64,17 @@ func getCurrentAuthFunctions() map[string]interface{} {
 	}
 	}
 }
 }
 
 
+// ResetAuthProvider resets the auth provider configuration.
+func ResetAuthProvider() {
+	settings := logic.GetServerSettings()
+
+	if settings.AuthProvider == "" {
+		auth_provider = nil
+	}
+
+	InitializeAuthProvider()
+}
+
 // InitializeAuthProvider - initializes the auth provider if any is present
 // InitializeAuthProvider - initializes the auth provider if any is present
 func InitializeAuthProvider() string {
 func InitializeAuthProvider() string {
 	var functions = getCurrentAuthFunctions()
 	var functions = getCurrentAuthFunctions()
@@ -74,7 +86,7 @@ func InitializeAuthProvider() string {
 	if err != nil {
 	if err != nil {
 		logger.FatalLog("failed to set auth_secret", err.Error())
 		logger.FatalLog("failed to set auth_secret", err.Error())
 	}
 	}
-	var authInfo = servercfg.GetAuthProviderInfo()
+	var authInfo = logic.GetAuthProviderInfo(logic.GetServerSettings())
 	var serverConn = servercfg.GetAPIHost()
 	var serverConn = servercfg.GetAPIHost()
 	if strings.Contains(serverConn, "localhost") || strings.Contains(serverConn, "127.0.0.1") {
 	if strings.Contains(serverConn, "localhost") || strings.Contains(serverConn, "127.0.0.1") {
 		serverConn = "http://" + serverConn
 		serverConn = "http://" + serverConn
@@ -275,7 +287,7 @@ func isStateCached(state string) bool {
 
 
 // isEmailAllowed - checks if email is allowed to signup
 // isEmailAllowed - checks if email is allowed to signup
 func isEmailAllowed(email string) bool {
 func isEmailAllowed(email string) bool {
-	allowedDomains := servercfg.GetAllowedEmailDomains()
+	allowedDomains := logic.GetAllowedEmailDomains()
 	domains := strings.Split(allowedDomains, ",")
 	domains := strings.Split(allowedDomains, ",")
 	if len(domains) == 1 && domains[0] == "*" {
 	if len(domains) == 1 && domains[0] == "*" {
 		return true
 		return true

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

@@ -35,7 +35,7 @@ func initAzureAD(redirectURL string, clientID string, clientSecret string) {
 		ClientID:     clientID,
 		ClientID:     clientID,
 		ClientSecret: clientSecret,
 		ClientSecret: clientSecret,
 		Scopes:       []string{"User.Read", "email", "profile", "openid"},
 		Scopes:       []string{"User.Read", "email", "profile", "openid"},
-		Endpoint:     microsoft.AzureADEndpoint(servercfg.GetAzureTenant()),
+		Endpoint:     microsoft.AzureADEndpoint(logic.GetAzureTenant()),
 	}
 	}
 }
 }
 
 
@@ -111,7 +111,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 					return
 					return
 				}
 				}
-				user.ExternalIdentityProviderID = content.UserPrincipalName
+				user.ExternalIdentityProviderID = content.ID
 				if err = logic.CreateUser(&user); err != nil {
 				if err = logic.CreateUser(&user); err != nil {
 					handleSomethingWentWrong(w)
 					handleSomethingWentWrong(w)
 					return
 					return
@@ -124,7 +124,9 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 					return
 					return
 				}
 				}
 				err = logic.InsertPendingUser(&models.User{
 				err = logic.InsertPendingUser(&models.User{
-					UserName: content.Email,
+					UserName:                   content.Email,
+					ExternalIdentityProviderID: content.ID,
+					AuthType:                   models.OAuth,
 				})
 				})
 				if err != nil {
 				if err != nil {
 					handleSomethingWentWrong(w)
 					handleSomethingWentWrong(w)
@@ -152,6 +154,12 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotFound(w)
 		handleOauthUserNotFound(w)
 		return
 		return
 	}
 	}
+
+	if user.AccountDisabled {
+		handleUserAccountDisabled(w)
+		return
+	}
+
 	userRole, err := logic.GetRole(user.PlatformRoleID)
 	userRole, err := logic.GetRole(user.PlatformRoleID)
 	if err != nil {
 	if err != nil {
 		handleSomethingWentWrong(w)
 		handleSomethingWentWrong(w)
@@ -176,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)
 }
 }

+ 8 - 0
pro/auth/error.go

@@ -113,6 +113,8 @@ var notallowedtosignup = fmt.Sprintf(htmlBaseTemplate, `<h2>Your email is not al
 var authTypeMismatch = fmt.Sprintf(htmlBaseTemplate, `<h2>It looks like you already have an account with us using Basic Authentication.</h2>
 var authTypeMismatch = fmt.Sprintf(htmlBaseTemplate, `<h2>It looks like you already have an account with us using Basic Authentication.</h2>
 <p>To continue, please log in with your existing credentials or reset your password if needed.</p>`)
 <p>To continue, please log in with your existing credentials or reset your password if needed.</p>`)
 
 
+var userAccountDisabled = fmt.Sprintf(htmlBaseTemplate, `<h2>Your account has been disabled. Please contact your administrator for more information about your account.</h2>`)
+
 func handleOauthUserNotFound(response http.ResponseWriter) {
 func handleOauthUserNotFound(response http.ResponseWriter) {
 	response.Header().Set("Content-Type", "text/html; charset=utf-8")
 	response.Header().Set("Content-Type", "text/html; charset=utf-8")
 	response.WriteHeader(http.StatusNotFound)
 	response.WriteHeader(http.StatusNotFound)
@@ -166,3 +168,9 @@ func handleAuthTypeMismatch(response http.ResponseWriter) {
 	response.WriteHeader(http.StatusBadRequest)
 	response.WriteHeader(http.StatusBadRequest)
 	response.Write([]byte(authTypeMismatch))
 	response.Write([]byte(authTypeMismatch))
 }
 }
+
+func handleUserAccountDisabled(response http.ResponseWriter) {
+	response.Header().Set("Content-Type", "text/html; charset=utf-8")
+	response.WriteHeader(http.StatusUnauthorized)
+	response.Write([]byte(userAccountDisabled))
+}

+ 26 - 3
pro/auth/github.go

@@ -111,7 +111,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 					return
 					return
 				}
 				}
-				user.ExternalIdentityProviderID = content.Login
+				user.ExternalIdentityProviderID = content.ID
 				if err = logic.CreateUser(&user); err != nil {
 				if err = logic.CreateUser(&user); err != nil {
 					handleSomethingWentWrong(w)
 					handleSomethingWentWrong(w)
 					return
 					return
@@ -124,7 +124,9 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 					return
 					return
 				}
 				}
 				err = logic.InsertPendingUser(&models.User{
 				err = logic.InsertPendingUser(&models.User{
-					UserName: content.Email,
+					UserName:                   content.Email,
+					ExternalIdentityProviderID: content.ID,
+					AuthType:                   models.OAuth,
 				})
 				})
 				if err != nil {
 				if err != nil {
 					handleSomethingWentWrong(w)
 					handleSomethingWentWrong(w)
@@ -143,6 +145,12 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotFound(w)
 		handleOauthUserNotFound(w)
 		return
 		return
 	}
 	}
+
+	if user.AccountDisabled {
+		handleUserAccountDisabled(w)
+		return
+	}
+
 	userRole, err := logic.GetRole(user.PlatformRoleID)
 	userRole, err := logic.GetRole(user.PlatformRoleID)
 	if err != nil {
 	if err != nil {
 		handleSomethingWentWrong(w)
 		handleSomethingWentWrong(w)
@@ -167,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)
 }
 }

+ 26 - 1
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)
@@ -104,7 +105,9 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 					return
 					return
 				}
 				}
 				err = logic.InsertPendingUser(&models.User{
 				err = logic.InsertPendingUser(&models.User{
-					UserName: content.Email,
+					UserName:                   content.Email,
+					ExternalIdentityProviderID: content.ID,
+					AuthType:                   models.OAuth,
 				})
 				})
 				if err != nil {
 				if err != nil {
 					handleSomethingWentWrong(w)
 					handleSomethingWentWrong(w)
@@ -135,6 +138,11 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
+	if user.AccountDisabled {
+		handleUserAccountDisabled(w)
+		return
+	}
+
 	userRole, err := logic.GetRole(user.PlatformRoleID)
 	userRole, err := logic.GetRole(user.PlatformRoleID)
 	if err != nil {
 	if err != nil {
 		handleSomethingWentWrong(w)
 		handleSomethingWentWrong(w)
@@ -160,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)
 }
 }

+ 3 - 1
pro/auth/headless_callback.go

@@ -64,7 +64,9 @@ func HandleHeadlessSSOCallback(w http.ResponseWriter, r *http.Request) {
 	if err != nil {
 	if err != nil {
 		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
 		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
 			err = logic.InsertPendingUser(&models.User{
 			err = logic.InsertPendingUser(&models.User{
-				UserName: userClaims.getUserName(),
+				UserName:                   userClaims.getUserName(),
+				ExternalIdentityProviderID: userClaims.ID,
+				AuthType:                   models.OAuth,
 			})
 			})
 			if err != nil {
 			if err != nil {
 				handleSomethingWentWrong(w)
 				handleSomethingWentWrong(w)

+ 28 - 3
pro/auth/oidc.go

@@ -102,7 +102,7 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 					return
 					return
 				}
 				}
-				user.ExternalIdentityProviderID = content.Email
+				user.ExternalIdentityProviderID = content.ID
 				if err = logic.CreateUser(&user); err != nil {
 				if err = logic.CreateUser(&user); err != nil {
 					handleSomethingWentWrong(w)
 					handleSomethingWentWrong(w)
 					return
 					return
@@ -115,7 +115,9 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 					return
 					return
 				}
 				}
 				err = logic.InsertPendingUser(&models.User{
 				err = logic.InsertPendingUser(&models.User{
-					UserName: content.Email,
+					UserName:                   content.Email,
+					ExternalIdentityProviderID: content.ID,
+					AuthType:                   models.OAuth,
 				})
 				})
 				if err != nil {
 				if err != nil {
 					handleSomethingWentWrong(w)
 					handleSomethingWentWrong(w)
@@ -143,6 +145,12 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotFound(w)
 		handleOauthUserNotFound(w)
 		return
 		return
 	}
 	}
+
+	if user.AccountDisabled {
+		handleUserAccountDisabled(w)
+		return
+	}
+
 	userRole, err := logic.GetRole(user.PlatformRoleID)
 	userRole, err := logic.GetRole(user.PlatformRoleID)
 	if err != nil {
 	if err != nil {
 		handleSomethingWentWrong(w)
 		handleSomethingWentWrong(w)
@@ -167,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)
 }
 }
@@ -209,6 +232,8 @@ func getOIDCUserInfo(state string, code string) (u *OAuthUser, e error) {
 		e = fmt.Errorf("error when claiming OIDCUser: \"%s\"", err.Error())
 		e = fmt.Errorf("error when claiming OIDCUser: \"%s\"", err.Error())
 	}
 	}
 
 
+	u.ID = idToken.Subject
+
 	return
 	return
 }
 }
 
 

+ 281 - 0
pro/auth/sync.go

@@ -0,0 +1,281 @@
+package auth
+
+import (
+	"fmt"
+	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/pro/idp"
+	"github.com/gravitl/netmaker/pro/idp/azure"
+	"github.com/gravitl/netmaker/pro/idp/google"
+	proLogic "github.com/gravitl/netmaker/pro/logic"
+	"strings"
+	"time"
+)
+
+var syncTicker *time.Ticker
+
+func StartSyncHook() {
+	syncTicker = time.NewTicker(logic.GetIDPSyncInterval())
+
+	for range syncTicker.C {
+		err := SyncFromIDP()
+		if err != nil {
+			logger.Log(0, "failed to sync from idp: ", err.Error())
+		} else {
+			logger.Log(0, "sync from idp complete")
+		}
+	}
+}
+
+func ResetIDPSyncHook() {
+	if syncTicker != nil {
+		syncTicker.Stop()
+		if logic.IsSyncEnabled() {
+			go StartSyncHook()
+		}
+	}
+}
+
+func SyncFromIDP() error {
+	settings := logic.GetServerSettings()
+
+	var idpClient idp.Client
+	var idpUsers []idp.User
+	var idpGroups []idp.Group
+	var err error
+
+	switch settings.AuthProvider {
+	case "google":
+		idpClient, err = google.NewGoogleWorkspaceClient()
+		if err != nil {
+			return err
+		}
+	case "azure-ad":
+		idpClient = azure.NewAzureEntraIDClient()
+	default:
+		if settings.AuthProvider != "" {
+			return fmt.Errorf("invalid auth provider: %s", settings.AuthProvider)
+		}
+	}
+
+	if settings.AuthProvider != "" && idpClient != nil {
+		idpUsers, err = idpClient.GetUsers()
+		if err != nil {
+			return err
+		}
+
+		idpGroups, err = idpClient.GetGroups()
+		if err != nil {
+			return err
+		}
+	}
+
+	err = syncUsers(idpUsers)
+	if err != nil {
+		return err
+	}
+
+	return syncGroups(idpGroups)
+}
+
+func syncUsers(idpUsers []idp.User) error {
+	dbUsers, err := logic.GetUsersDB()
+	if err != nil && !database.IsEmptyRecord(err) {
+		return err
+	}
+
+	password, err := logic.FetchPassValue("")
+	if err != nil {
+		return err
+	}
+
+	idpUsersMap := make(map[string]struct{})
+	for _, user := range idpUsers {
+		idpUsersMap[user.Username] = struct{}{}
+	}
+
+	dbUsersMap := make(map[string]models.User)
+	for _, user := range dbUsers {
+		dbUsersMap[user.UserName] = user
+	}
+
+	filters := logic.GetServerSettings().UserFilters
+
+	for _, user := range idpUsers {
+		var found bool
+		for _, filter := range filters {
+			if strings.HasPrefix(user.Username, filter) {
+				found = true
+				break
+			}
+		}
+
+		// if there are filters but none of them match, then skip this user.
+		if len(filters) > 0 && !found {
+			continue
+		}
+
+		dbUser, ok := dbUsersMap[user.Username]
+		if !ok {
+			// create the user only if it doesn't exist.
+			err = logic.CreateUser(&models.User{
+				UserName:                   user.Username,
+				ExternalIdentityProviderID: user.ID,
+				DisplayName:                user.DisplayName,
+				AccountDisabled:            user.AccountDisabled,
+				Password:                   password,
+				AuthType:                   models.OAuth,
+				PlatformRoleID:             models.ServiceUser,
+			})
+			if err != nil {
+				return err
+			}
+		} else if dbUser.AuthType == models.OAuth {
+			if dbUser.AccountDisabled != user.AccountDisabled ||
+				dbUser.DisplayName != user.DisplayName ||
+				dbUser.ExternalIdentityProviderID != user.ID {
+
+				dbUser.AccountDisabled = user.AccountDisabled
+				dbUser.DisplayName = user.DisplayName
+				dbUser.ExternalIdentityProviderID = user.ID
+
+				err = logic.UpsertUser(dbUser)
+				if err != nil {
+					return err
+				}
+			}
+		} else {
+			logger.Log(0, "user with username "+user.Username+" already exists, skipping creation")
+			continue
+		}
+	}
+
+	for _, user := range dbUsersMap {
+		if user.ExternalIdentityProviderID == "" {
+			continue
+		}
+		if _, ok := idpUsersMap[user.UserName]; !ok {
+			// delete the user if it has been deleted on idp.
+			err = logic.DeleteUser(user.UserName)
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+func syncGroups(idpGroups []idp.Group) error {
+	dbGroups, err := proLogic.ListUserGroups()
+	if err != nil && !database.IsEmptyRecord(err) {
+		return err
+	}
+
+	dbUsers, err := logic.GetUsersDB()
+	if err != nil && !database.IsEmptyRecord(err) {
+		return err
+	}
+
+	idpGroupsMap := make(map[string]struct{})
+	for _, group := range idpGroups {
+		idpGroupsMap[group.ID] = struct{}{}
+	}
+
+	dbGroupsMap := make(map[string]models.UserGroup)
+	for _, group := range dbGroups {
+		if group.ExternalIdentityProviderID != "" {
+			dbGroupsMap[group.ExternalIdentityProviderID] = group
+		}
+	}
+
+	dbUsersMap := make(map[string]models.User)
+	for _, user := range dbUsers {
+		if user.ExternalIdentityProviderID != "" {
+			dbUsersMap[user.ExternalIdentityProviderID] = user
+		}
+	}
+
+	modifiedUsers := make(map[string]struct{})
+
+	filters := logic.GetServerSettings().GroupFilters
+
+	for _, group := range idpGroups {
+		var found bool
+		for _, filter := range filters {
+			if strings.HasPrefix(group.Name, filter) {
+				found = true
+				break
+			}
+		}
+
+		// if there are filters but none of them match, then skip this group.
+		if len(filters) > 0 && !found {
+			continue
+		}
+
+		dbGroup, ok := dbGroupsMap[group.ID]
+		if !ok {
+			err := proLogic.CreateUserGroup(models.UserGroup{
+				ExternalIdentityProviderID: group.ID,
+				Default:                    false,
+				Name:                       group.Name,
+			})
+			if err != nil {
+				return err
+			}
+		} else {
+			dbGroup.Name = group.Name
+			err = proLogic.UpdateUserGroup(dbGroup)
+			if err != nil {
+				return err
+			}
+		}
+
+		groupMembersMap := make(map[string]struct{})
+		for _, member := range group.Members {
+			groupMembersMap[member] = struct{}{}
+		}
+
+		for _, user := range dbUsers {
+			// use dbGroup.Name because the group name may have been changed on idp.
+			_, inNetmakerGroup := user.UserGroups[models.UserGroupID(dbGroup.Name)]
+			_, inIDPGroup := groupMembersMap[user.ExternalIdentityProviderID]
+
+			if inNetmakerGroup && !inIDPGroup {
+				// use dbGroup.Name because the group name may have been changed on idp.
+				delete(dbUsersMap[user.ExternalIdentityProviderID].UserGroups, models.UserGroupID(dbGroup.Name))
+				modifiedUsers[user.ExternalIdentityProviderID] = struct{}{}
+			}
+
+			if !inNetmakerGroup && inIDPGroup {
+				// use dbGroup.Name because the group name may have been changed on idp.
+				dbUsersMap[user.ExternalIdentityProviderID].UserGroups[models.UserGroupID(dbGroup.Name)] = struct{}{}
+				modifiedUsers[user.ExternalIdentityProviderID] = struct{}{}
+			}
+		}
+	}
+
+	for userID := range modifiedUsers {
+		err = logic.UpsertUser(dbUsersMap[userID])
+		if err != nil {
+			return err
+		}
+	}
+
+	for _, group := range dbGroups {
+		if group.ExternalIdentityProviderID != "" {
+			if _, ok := idpGroupsMap[group.ExternalIdentityProviderID]; !ok {
+				// delete the group if it has been deleted on idp.
+				err = proLogic.DeleteUserGroup(group.ID)
+				if err != nil {
+					return err
+				}
+			}
+		}
+	}
+
+	return nil
+}

+ 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,

部分文件因文件數量過多而無法顯示