NM-38: User Config Fixes (#3559)

* Build(deps): bump gorm.io/datatypes from 1.2.5 to 1.2.6

Bumps [gorm.io/datatypes](https://github.com/go-gorm/datatypes) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/go-gorm/datatypes/releases)
- [Commits](https://github.com/go-gorm/datatypes/compare/v1.2.5...v1.2.6)

---
updated-dependencies:
- dependency-name: gorm.io/datatypes
  dependency-version: 1.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Build(deps): bump google.golang.org/api from 0.238.0 to 0.240.0 (#3541)

Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.238.0 to 0.240.0.
- [Release notes](https://github.com/googleapis/google-api-go-client/releases)
- [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md)
- [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.238.0...v0.240.0)

---
updated-dependencies:
- dependency-name: google.golang.org/api
  dependency-version: 0.240.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Build(deps): bump github.com/go-playground/validator/v10 (#3539)

Bumps [github.com/go-playground/validator/v10](https://github.com/go-playground/validator) from 10.26.0 to 10.27.0.
- [Release notes](https://github.com/go-playground/validator/releases)
- [Commits](https://github.com/go-playground/validator/compare/v10.26.0...v10.27.0)

---
updated-dependencies:
- dependency-name: github.com/go-playground/validator/v10
  dependency-version: 10.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix(go): prevent idp user from being deleted; (#3538)

* fix(go): use correct method for pro;

fixes: User Config nodes are always reported online.

* fix(go): add device id to extclient;

* fix(go): try match device id;

* fix(go): set device id if not set;

* feat(go): return best match offline extclient;

* fix(go): match device id with owner and gateway;

* fix(go): remove check for rac id;

* fix(go): check status on get node status;

* fix(go): allow offline or unknown extclient;

* feat(go): add count db method;

* feat(go): revert change;

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Abhishek K <abhi281342@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Vishal Dalwadi
2025-07-24 12:09:25 +05:30
committed by GitHub
parent ffe5e0e65a
commit 9d65c62860
13 changed files with 144 additions and 70 deletions
+12 -2
View File
@@ -726,8 +726,17 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
return
}
for _, extclient := range extclients {
if extclient.RemoteAccessClientID != "" &&
extclient.RemoteAccessClientID == customExtClient.RemoteAccessClientID && extclient.OwnerID == caller.UserName && nodeid == extclient.IngressGatewayID {
// if device id is sent, then make sure extclient with the same device id
// does not exist.
if customExtClient.DeviceID != "" && extclient.DeviceID == customExtClient.DeviceID &&
extclient.OwnerID == caller.UserName && nodeid == extclient.IngressGatewayID {
err = errors.New("remote client config already exists on the gateway")
slog.Error("failed to create extclient", "user", userName, "error", err)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
if extclient.OwnerID == caller.UserName && nodeid == extclient.IngressGatewayID {
// extclient on the gw already exists for the remote access client
err = errors.New("remote client config already exists on the gateway")
slog.Error("failed to create extclient", "user", userName, "error", err)
@@ -774,6 +783,7 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
extclient.Enabled = parentNetwork.DefaultACL == "yes"
}
extclient.Os = customExtClient.Os
extclient.DeviceID = customExtClient.DeviceID
extclient.DeviceName = customExtClient.DeviceName
if customExtClient.IsAlreadyConnectedToInetGw {
slog.Warn("RAC/Client is already connected to internet gateway. this may mask their real IP address", "client IP", customExtClient.PublicEndpoint)
+4 -4
View File
@@ -1,8 +1,11 @@
package controller
import (
"context"
"encoding/json"
"errors"
"github.com/gravitl/netmaker/db"
"github.com/gravitl/netmaker/schema"
"github.com/google/go-cmp/cmp"
"net/http"
"os"
@@ -110,10 +113,7 @@ func getUsage(w http.ResponseWriter, _ *http.Request) {
if err == nil {
serverUsage.Ingresses = len(ingresses)
}
egresses, err := logic.GetAllEgresses()
if err == nil {
serverUsage.Egresses = len(egresses)
}
serverUsage.Egresses, _ = (&schema.Egress{}).Count(db.WithContext(context.TODO()))
relays, err := logic.GetRelays()
if err == nil {
serverUsage.Relays = len(relays)
+8
View File
@@ -1324,6 +1324,14 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
return
}
}
if user.AuthType == models.OAuth || user.ExternalIdentityProviderID != "" {
err = fmt.Errorf("cannot delete idp user %s", username)
logger.Log(0, username, "failed to delete user: ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
err = logic.DeleteUser(username)
if err != nil {
logger.Log(0, username,
+3 -3
View File
@@ -7,7 +7,7 @@ toolchain go1.23.7
require (
github.com/blang/semver v3.5.1+incompatible
github.com/eclipse/paho.mqtt.golang v1.5.0
github.com/go-playground/validator/v10 v10.26.0
github.com/go-playground/validator/v10 v10.27.0
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/google/uuid v1.6.0
github.com/gorilla/handlers v1.5.2
@@ -48,9 +48,9 @@ require (
github.com/matryer/is v1.4.1
github.com/pquerna/otp v1.5.0
github.com/spf13/cobra v1.9.1
google.golang.org/api v0.238.0
google.golang.org/api v0.240.0
gopkg.in/mail.v2 v2.3.1
gorm.io/datatypes v1.2.5
gorm.io/datatypes v1.2.6
gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.30.0
+8 -8
View File
@@ -37,8 +37,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
@@ -174,8 +174,8 @@ golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.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/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ=
google.golang.org/api v0.238.0 h1:+EldkglWIg/pWjkq97sd+XxH7PxakNYoe/rkSTbnvOs=
google.golang.org/api v0.238.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
google.golang.org/api v0.240.0 h1:PxG3AA2UIqT1ofIzWV2COM3j3JagKTKSwy7L6RHNXNU=
google.golang.org/api v0.240.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0=
@@ -196,16 +196,16 @@ gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I=
gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4=
gorm.io/datatypes v1.2.6 h1:KafLdXvFUhzNeL2ncm03Gl3eTLONQfNKZ+wJ+9Y4Nck=
gorm.io/datatypes v1.2.6/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g=
gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g=
gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
+4 -1
View File
@@ -433,6 +433,9 @@ func UpdateExtClient(old *models.ExtClient, update *models.CustomExtClient) mode
if update.Country != "" && update.Country != old.Country {
new.Country = update.Country
}
if update.DeviceID != "" && old.DeviceID == "" {
new.DeviceID = update.DeviceID
}
return new
}
@@ -484,7 +487,7 @@ func GetAllExtClientsWithStatus(status models.NodeStatus) ([]models.ExtClient, e
var validExtClients []models.ExtClient
for _, extClient := range extClients {
node := extClient.ConvertToStaticNode()
GetNodeCheckInStatus(&node, false)
GetNodeStatus(&node, false)
if node.Status == status {
validExtClients = append(validExtClients, extClient)
+1 -1
View File
@@ -125,7 +125,7 @@ func GetAllHostsWithStatus(status models.NodeStatus) ([]models.Host, error) {
nodes := GetHostNodes(&host)
for _, node := range nodes {
GetNodeCheckInStatus(&node, false)
getNodeCheckInStatus(&node, false)
if node.Status == status {
validHosts = append(validHosts, host)
break
+1 -1
View File
@@ -471,7 +471,7 @@ func AddStatusToNodes(nodes []models.Node, statusCall bool) (nodesWithStatus []m
if statusCall {
GetNodeStatus(&node, aclDefaultPolicyStatusMap[node.Network])
} else {
GetNodeCheckInStatus(&node, true)
getNodeCheckInStatus(&node, true)
}
nodesWithStatus = append(nodesWithStatus, node)
+2 -2
View File
@@ -6,9 +6,9 @@ import (
"github.com/gravitl/netmaker/models"
)
var GetNodeStatus = GetNodeCheckInStatus
var GetNodeStatus = getNodeCheckInStatus
func GetNodeCheckInStatus(node *models.Node, t bool) {
func getNodeCheckInStatus(node *models.Node, t bool) {
// On CE check only last check-in time
if node.IsStatic {
if !node.StaticNode.Enabled {
+2
View File
@@ -24,6 +24,7 @@ type ExtClient struct {
PostDown string `json:"postdown" bson:"postdown"`
Tags map[TagID]struct{} `json:"tags"`
Os string `json:"os"`
DeviceID string `json:"device_id"`
DeviceName string `json:"device_name"`
PublicEndpoint string `json:"public_endpoint"`
Country string `json:"country"`
@@ -44,6 +45,7 @@ type CustomExtClient struct {
PostDown string `json:"postdown" bson:"postdown" validate:"max=1024"`
Tags map[TagID]struct{} `json:"tags"`
Os string `json:"os"`
DeviceID string `json:"device_id"`
DeviceName string `json:"device_name"`
IsAlreadyConnectedToInetGw bool `json:"is_already_connected_to_inet_gw"`
PublicEndpoint string `json:"public_endpoint"`
+89 -44
View File
@@ -1254,6 +1254,7 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to fetch user %s, error: %v", username, err), "badrequest"))
return
}
deviceID := r.URL.Query().Get("device_id")
remoteAccessClientID := r.URL.Query().Get("remote_access_clientid")
var req models.UserRemoteGwsReq
if remoteAccessClientID == "" {
@@ -1279,58 +1280,102 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
return
}
userGwNodes := proLogic.GetUserRAGNodes(*user)
userExtClients := make(map[string][]models.ExtClient)
// group all extclients of the requesting user by ingress
// gateway.
for _, extClient := range allextClients {
node, ok := userGwNodes[extClient.IngressGatewayID]
// filter our extclients that don't belong to this user.
if extClient.OwnerID != username {
continue
}
_, ok := userExtClients[extClient.IngressGatewayID]
if !ok {
userExtClients[extClient.IngressGatewayID] = []models.ExtClient{}
}
userExtClients[extClient.IngressGatewayID] = append(userExtClients[extClient.IngressGatewayID], extClient)
}
for ingressGatewayID, extClients := range userExtClients {
node, ok := userGwNodes[ingressGatewayID]
if !ok {
continue
}
if extClient.RemoteAccessClientID == req.RemoteAccessClientID && extClient.OwnerID == username {
host, err := logic.GetHost(node.HostID.String())
if err != nil {
continue
var gwClient models.ExtClient
var found bool
if deviceID != "" {
for _, extClient := range extClients {
if extClient.DeviceID == deviceID {
gwClient = extClient
found = true
break
}
}
network, err := logic.GetNetwork(node.Network)
if err != nil {
slog.Error("failed to get node network", "error", err)
continue
}
nodesWithStatus := logic.AddStatusToNodes([]models.Node{node}, false)
if len(nodesWithStatus) > 0 {
node = nodesWithStatus[0]
}
gws := userGws[node.Network]
if extClient.DNS == "" {
extClient.DNS = node.IngressDNS
}
extClient.IngressGatewayEndpoint = utils.GetExtClientEndpoint(
host.EndpointIP,
host.EndpointIPv6,
logic.GetPeerListenPort(host),
)
extClient.AllowedIPs = logic.GetExtclientAllowedIPs(extClient)
gws = append(gws, models.UserRemoteGws{
GwID: node.ID.String(),
GWName: host.Name,
Network: node.Network,
GwClient: extClient,
Connected: true,
IsInternetGateway: node.IsInternetGateway,
GwPeerPublicKey: host.PublicKey.String(),
GwListenPort: logic.GetPeerListenPort(host),
Metadata: node.Metadata,
AllowedEndpoints: getAllowedRagEndpoints(&node, host),
NetworkAddresses: []string{network.AddressRange, network.AddressRange6},
Status: node.Status,
DnsAddress: node.IngressDNS,
Addresses: utils.NoEmptyStringToCsv(node.Address.String(), node.Address6.String()),
})
userGws[node.Network] = gws
delete(userGwNodes, node.ID.String())
}
if !found {
extClientNodes := make([]models.Node, len(extClients))
for i, extClient := range extClients {
extClientNodes[i] = extClient.ConvertToStaticNode()
}
extClientNodesWithStatus := logic.AddStatusToNodes(extClientNodes, true)
for _, node := range extClientNodesWithStatus {
if node.Status == models.OfflineSt || node.Status == models.UnKnown {
gwClient = node.StaticNode
}
}
}
host, err := logic.GetHost(node.HostID.String())
if err != nil {
continue
}
network, err := logic.GetNetwork(node.Network)
if err != nil {
slog.Error("failed to get node network", "error", err)
continue
}
nodesWithStatus := logic.AddStatusToNodes([]models.Node{node}, false)
if len(nodesWithStatus) > 0 {
node = nodesWithStatus[0]
}
gws := userGws[node.Network]
if gwClient.DNS == "" {
gwClient.DNS = node.IngressDNS
}
gwClient.IngressGatewayEndpoint = utils.GetExtClientEndpoint(
host.EndpointIP,
host.EndpointIPv6,
logic.GetPeerListenPort(host),
)
gwClient.AllowedIPs = logic.GetExtclientAllowedIPs(gwClient)
gws = append(gws, models.UserRemoteGws{
GwID: node.ID.String(),
GWName: host.Name,
Network: node.Network,
GwClient: gwClient,
Connected: true,
IsInternetGateway: node.IsInternetGateway,
GwPeerPublicKey: host.PublicKey.String(),
GwListenPort: logic.GetPeerListenPort(host),
Metadata: node.Metadata,
AllowedEndpoints: getAllowedRagEndpoints(&node, host),
NetworkAddresses: []string{network.AddressRange, network.AddressRange6},
Status: node.Status,
DnsAddress: node.IngressDNS,
Addresses: utils.NoEmptyStringToCsv(node.Address.String(), node.Address6.String()),
})
userGws[node.Network] = gws
delete(userGwNodes, node.ID.String())
}
// add remaining gw nodes to resp
for gwID := range userGwNodes {
node, err := logic.GetNodeByID(gwID)
+4 -4
View File
@@ -4,8 +4,11 @@
package pro
import (
"context"
"encoding/base64"
"github.com/gravitl/netmaker/db"
"github.com/gravitl/netmaker/models"
"github.com/gravitl/netmaker/schema"
"github.com/gravitl/netmaker/logic"
)
@@ -48,10 +51,7 @@ func getCurrentServerUsage() (limits Usage) {
if err == nil {
limits.Ingresses = len(ingresses)
}
egresses, err := logic.GetAllEgresses()
if err == nil {
limits.Egresses = len(egresses)
}
limits.Egresses, _ = (&schema.Egress{}).Count(db.WithContext(context.TODO()))
relays, err := logic.GetRelays()
if err == nil {
limits.Relays = len(relays)
+6
View File
@@ -63,6 +63,12 @@ func (e *Egress) ListByNetwork(ctx context.Context) (egs []Egress, err error) {
return
}
func (e *Egress) Count(ctx context.Context) (int, error) {
var count int64
err := db.FromContext(ctx).Model(&Egress{}).Count(&count).Error
return int(count), err
}
func (e *Egress) Delete(ctx context.Context) error {
return db.FromContext(ctx).Table(e.Table()).Where("id = ?", e.ID).Delete(&e).Error
}