diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml
index 742486234..da1db5c03 100644
--- a/.github/workflows/golang-test-linux.yml
+++ b/.github/workflows/golang-test-linux.yml
@@ -142,7 +142,7 @@ jobs:
fail-fast: false
matrix:
arch: [ '386','amd64' ]
- store: [ 'sqlite', 'postgres']
+ store: [ 'sqlite', 'postgres', 'mysql' ]
runs-on: ubuntu-22.04
steps:
- name: Install Go
@@ -182,6 +182,17 @@ jobs:
- name: check git status
run: git --no-pager diff --exit-code
+ - name: Login to Docker hub
+ if: matrix.store == 'mysql'
+ uses: docker/login-action@v1
+ with:
+ username: ${{ secrets.DOCKER_USER }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: download mysql image
+ if: matrix.store == 'mysql'
+ run: docker pull mlsmaycon/warmed-mysql:8
+
- name: Test
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -p 1 -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 10m $(go list ./... | grep /management)
@@ -191,7 +202,7 @@ jobs:
fail-fast: false
matrix:
arch: [ '386','amd64' ]
- store: [ 'sqlite', 'postgres' ]
+ store: [ 'sqlite', 'postgres', 'mysql' ]
runs-on: ubuntu-22.04
steps:
- name: Install Go
@@ -231,6 +242,17 @@ jobs:
- name: check git status
run: git --no-pager diff --exit-code
+ - name: Login to Docker hub
+ if: matrix.store == 'mysql'
+ uses: docker/login-action@v1
+ with:
+ username: ${{ secrets.DOCKER_USER }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: download mysql image
+ if: matrix.store == 'mysql'
+ run: docker pull mlsmaycon/warmed-mysql:8
+
- name: Test
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -run=^$ -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 20m ./...
@@ -240,7 +262,7 @@ jobs:
fail-fast: false
matrix:
arch: [ '386','amd64' ]
- store: [ 'sqlite', 'postgres' ]
+ store: [ 'sqlite', 'postgres', 'mysql' ]
runs-on: ubuntu-22.04
steps:
- name: Install Go
@@ -280,6 +302,17 @@ jobs:
- name: check git status
run: git --no-pager diff --exit-code
+ - name: Login to Docker hub
+ if: matrix.store == 'mysql'
+ uses: docker/login-action@v1
+ with:
+ username: ${{ secrets.DOCKER_USER }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: download mysql image
+ if: matrix.store == 'mysql'
+ run: docker pull mlsmaycon/warmed-mysql:8
+
- name: Test
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags=benchmark -run=^$ -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 10m $(go list ./... | grep /management)
diff --git a/.github/workflows/test-infrastructure-files.yml b/.github/workflows/test-infrastructure-files.yml
index da3ec746a..5a3c6c22e 100644
--- a/.github/workflows/test-infrastructure-files.yml
+++ b/.github/workflows/test-infrastructure-files.yml
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- store: [ 'sqlite', 'postgres' ]
+ store: [ 'sqlite', 'postgres', 'mysql' ]
services:
postgres:
image: ${{ (matrix.store == 'postgres') && 'postgres' || '' }}
@@ -34,6 +34,19 @@ jobs:
--health-timeout 5s
ports:
- 5432:5432
+ mysql:
+ image: ${{ (matrix.store == 'mysql') && 'mysql' || '' }}
+ env:
+ MYSQL_USER: netbird
+ MYSQL_PASSWORD: mysql
+ MYSQL_ROOT_PASSWORD: mysqlroot
+ MYSQL_DATABASE: netbird
+ options: >-
+ --health-cmd "mysqladmin ping --silent"
+ --health-interval 10s
+ --health-timeout 5s
+ ports:
+ - 3306:3306
steps:
- name: Set Database Connection String
run: |
@@ -42,6 +55,11 @@ jobs:
else
echo "NETBIRD_STORE_ENGINE_POSTGRES_DSN==" >> $GITHUB_ENV
fi
+ if [ "${{ matrix.store }}" == "mysql" ]; then
+ echo "NETBIRD_STORE_ENGINE_MYSQL_DSN=netbird:mysql@tcp($(hostname -I | awk '{print $1}'):3306)/netbird" >> $GITHUB_ENV
+ else
+ echo "NETBIRD_STORE_ENGINE_MYSQL_DSN==" >> $GITHUB_ENV
+ fi
- name: Install jq
run: sudo apt-get install -y jq
@@ -84,6 +102,7 @@ jobs:
CI_NETBIRD_AUTH_SUPPORTED_SCOPES: "openid profile email offline_access api email_verified"
CI_NETBIRD_STORE_CONFIG_ENGINE: ${{ matrix.store }}
NETBIRD_STORE_ENGINE_POSTGRES_DSN: ${{ env.NETBIRD_STORE_ENGINE_POSTGRES_DSN }}
+ NETBIRD_STORE_ENGINE_MYSQL_DSN: ${{ env.NETBIRD_STORE_ENGINE_MYSQL_DSN }}
CI_NETBIRD_MGMT_IDP_SIGNKEY_REFRESH: false
- name: check values
@@ -112,6 +131,7 @@ jobs:
CI_NETBIRD_SIGNAL_PORT: 12345
CI_NETBIRD_STORE_CONFIG_ENGINE: ${{ matrix.store }}
NETBIRD_STORE_ENGINE_POSTGRES_DSN: '${{ env.NETBIRD_STORE_ENGINE_POSTGRES_DSN }}$'
+ NETBIRD_STORE_ENGINE_MYSQL_DSN: '${{ env.NETBIRD_STORE_ENGINE_MYSQL_DSN }}$'
CI_NETBIRD_MGMT_IDP_SIGNKEY_REFRESH: false
CI_NETBIRD_TURN_EXTERNAL_IP: "1.2.3.4"
@@ -149,6 +169,7 @@ jobs:
grep -A 10 PKCEAuthorizationFlow management.json | grep -A 10 ProviderConfig | grep Scope | grep "$CI_NETBIRD_AUTH_SUPPORTED_SCOPES"
grep -A 10 PKCEAuthorizationFlow management.json | grep -A 10 ProviderConfig | grep -A 3 RedirectURLs | grep "http://localhost:53000"
grep "external-ip" turnserver.conf | grep $CI_NETBIRD_TURN_EXTERNAL_IP
+ grep "NETBIRD_STORE_ENGINE_MYSQL_DSN=$NETBIRD_STORE_ENGINE_MYSQL_DSN" docker-compose.yml
grep NETBIRD_STORE_ENGINE_POSTGRES_DSN docker-compose.yml | egrep "$NETBIRD_STORE_ENGINE_POSTGRES_DSN"
# check relay values
grep "NB_EXPOSED_ADDRESS=$CI_NETBIRD_DOMAIN:33445" docker-compose.yml
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index e718b3fcd..d6479763e 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -179,6 +179,51 @@ dockers:
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
+
+ - image_templates:
+ - netbirdio/netbird:{{ .Version }}-rootless-amd64
+ ids:
+ - netbird
+ goarch: amd64
+ use: buildx
+ dockerfile: client/Dockerfile-rootless
+ build_flag_templates:
+ - "--platform=linux/amd64"
+ - "--label=org.opencontainers.image.created={{.Date}}"
+ - "--label=org.opencontainers.image.title={{.ProjectName}}"
+ - "--label=org.opencontainers.image.version={{.Version}}"
+ - "--label=org.opencontainers.image.revision={{.FullCommit}}"
+ - "--label=maintainer=dev@netbird.io"
+ - image_templates:
+ - netbirdio/netbird:{{ .Version }}-rootless-arm64v8
+ ids:
+ - netbird
+ goarch: arm64
+ use: buildx
+ dockerfile: client/Dockerfile-rootless
+ build_flag_templates:
+ - "--platform=linux/arm64"
+ - "--label=org.opencontainers.image.created={{.Date}}"
+ - "--label=org.opencontainers.image.title={{.ProjectName}}"
+ - "--label=org.opencontainers.image.version={{.Version}}"
+ - "--label=org.opencontainers.image.revision={{.FullCommit}}"
+ - "--label=maintainer=dev@netbird.io"
+ - image_templates:
+ - netbirdio/netbird:{{ .Version }}-rootless-arm
+ ids:
+ - netbird
+ goarch: arm
+ goarm: 6
+ use: buildx
+ dockerfile: client/Dockerfile-rootless
+ build_flag_templates:
+ - "--platform=linux/arm"
+ - "--label=org.opencontainers.image.created={{.Date}}"
+ - "--label=org.opencontainers.image.title={{.ProjectName}}"
+ - "--label=org.opencontainers.image.version={{.Version}}"
+ - "--label=org.opencontainers.image.revision={{.FullCommit}}"
+ - "--label=maintainer=dev@netbird.io"
+
- image_templates:
- netbirdio/relay:{{ .Version }}-amd64
ids:
@@ -377,6 +422,18 @@ docker_manifests:
- netbirdio/netbird:{{ .Version }}-arm
- netbirdio/netbird:{{ .Version }}-amd64
+ - name_template: netbirdio/netbird:{{ .Version }}-rootless
+ image_templates:
+ - netbirdio/netbird:{{ .Version }}-rootless-arm64v8
+ - netbirdio/netbird:{{ .Version }}-rootless-arm
+ - netbirdio/netbird:{{ .Version }}-rootless-amd64
+
+ - name_template: netbirdio/netbird:rootless-latest
+ image_templates:
+ - netbirdio/netbird:{{ .Version }}-rootless-arm64v8
+ - netbirdio/netbird:{{ .Version }}-rootless-arm
+ - netbirdio/netbird:{{ .Version }}-rootless-amd64
+
- name_template: netbirdio/relay:{{ .Version }}
image_templates:
- netbirdio/relay:{{ .Version }}-arm64v8
diff --git a/README.md b/README.md
index e7925ae09..0537710e9 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,3 @@
-
diff --git a/client/Dockerfile-rootless b/client/Dockerfile-rootless
new file mode 100644
index 000000000..f206debb5
--- /dev/null
+++ b/client/Dockerfile-rootless
@@ -0,0 +1,15 @@
+FROM alpine:3.21.0
+
+COPY netbird /usr/local/bin/netbird
+
+RUN apk add --no-cache ca-certificates \
+ && adduser -D -h /var/lib/netbird netbird
+WORKDIR /var/lib/netbird
+USER netbird:netbird
+
+ENV NB_FOREGROUND_MODE=true
+ENV NB_USE_NETSTACK_MODE=true
+ENV NB_CONFIG=config.json
+ENV NB_DAEMON_ADDR=unix://netbird.sock
+
+ENTRYPOINT [ "/usr/local/bin/netbird", "up" ]
diff --git a/client/iface/netstack/env.go b/client/iface/netstack/env.go
index c77e39fe0..09889a57e 100644
--- a/client/iface/netstack/env.go
+++ b/client/iface/netstack/env.go
@@ -15,6 +15,10 @@ func IsEnabled() bool {
func ListenAddr() string {
sPort := os.Getenv("NB_SOCKS5_LISTENER_PORT")
+ if sPort == "" {
+ return listenAddr(DefaultSocks5Port)
+ }
+
port, err := strconv.Atoi(sPort)
if err != nil {
log.Warnf("invalid socks5 listener port, unable to convert it to int, falling back to default: %d", DefaultSocks5Port)
diff --git a/client/internal/connect.go b/client/internal/connect.go
index 782984e27..5cbf54f75 100644
--- a/client/internal/connect.go
+++ b/client/internal/connect.go
@@ -382,8 +382,7 @@ func (c *ConnectClient) isContextCancelled() bool {
// SetNetworkMapPersistence enables or disables network map persistence.
// When enabled, the last received network map will be stored and can be retrieved
// through the Engine's getLatestNetworkMap method. When disabled, any stored
-// network map will be cleared. This functionality is primarily used for debugging
-// and should not be enabled during normal operation.
+// network map will be cleared.
func (c *ConnectClient) SetNetworkMapPersistence(enabled bool) {
c.engineMutex.Lock()
c.persistNetworkMap = enabled
diff --git a/client/internal/engine.go b/client/internal/engine.go
index 09d35bc8b..cbc4bfaf7 100644
--- a/client/internal/engine.go
+++ b/client/internal/engine.go
@@ -1614,7 +1614,7 @@ func (e *Engine) GetLatestNetworkMap() (*mgmProto.NetworkMap, error) {
return nil, nil
}
- // Create a deep copy to avoid external modifications
+ log.Debugf("Retrieving latest network map with size %d bytes", proto.Size(e.latestNetworkMap))
nm, ok := proto.Clone(e.latestNetworkMap).(*mgmProto.NetworkMap)
if !ok {
diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go
index c3179fede..f8669fcec 100644
--- a/client/internal/routemanager/manager.go
+++ b/client/internal/routemanager/manager.go
@@ -17,6 +17,7 @@ import (
firewall "github.com/netbirdio/netbird/client/firewall/manager"
"github.com/netbirdio/netbird/client/iface"
"github.com/netbirdio/netbird/client/iface/configurer"
+ "github.com/netbirdio/netbird/client/iface/netstack"
"github.com/netbirdio/netbird/client/internal/dns"
"github.com/netbirdio/netbird/client/internal/listener"
"github.com/netbirdio/netbird/client/internal/peer"
@@ -104,22 +105,43 @@ func NewManager(
peerStore: peerStore,
}
- dm.routeRefCounter = refcounter.New(
+ dm.setupRefCounters()
+
+ if runtime.GOOS == "android" {
+ cr := dm.initialClientRoutes(initialRoutes)
+ dm.notifier.SetInitialClientRoutes(cr)
+ }
+ return dm
+}
+
+func (m *DefaultManager) setupRefCounters() {
+ m.routeRefCounter = refcounter.New(
func(prefix netip.Prefix, _ struct{}) (struct{}, error) {
- return struct{}{}, sysOps.AddVPNRoute(prefix, wgInterface.ToInterface())
+ return struct{}{}, m.sysOps.AddVPNRoute(prefix, m.wgInterface.ToInterface())
},
func(prefix netip.Prefix, _ struct{}) error {
- return sysOps.RemoveVPNRoute(prefix, wgInterface.ToInterface())
+ return m.sysOps.RemoveVPNRoute(prefix, m.wgInterface.ToInterface())
},
)
- dm.allowedIPsRefCounter = refcounter.New(
+ if netstack.IsEnabled() {
+ m.routeRefCounter = refcounter.New(
+ func(netip.Prefix, struct{}) (struct{}, error) {
+ return struct{}{}, refcounter.ErrIgnore
+ },
+ func(netip.Prefix, struct{}) error {
+ return nil
+ },
+ )
+ }
+
+ m.allowedIPsRefCounter = refcounter.New(
func(prefix netip.Prefix, peerKey string) (string, error) {
// save peerKey to use it in the remove function
- return peerKey, wgInterface.AddAllowedIP(peerKey, prefix.String())
+ return peerKey, m.wgInterface.AddAllowedIP(peerKey, prefix.String())
},
func(prefix netip.Prefix, peerKey string) error {
- if err := wgInterface.RemoveAllowedIP(peerKey, prefix.String()); err != nil {
+ if err := m.wgInterface.RemoveAllowedIP(peerKey, prefix.String()); err != nil {
if !errors.Is(err, configurer.ErrPeerNotFound) && !errors.Is(err, configurer.ErrAllowedIPNotFound) {
return err
}
@@ -128,12 +150,6 @@ func NewManager(
return nil
},
)
-
- if runtime.GOOS == "android" {
- cr := dm.initialClientRoutes(initialRoutes)
- dm.notifier.SetInitialClientRoutes(cr)
- }
- return dm
}
// Init sets up the routing
diff --git a/client/internal/routemanager/systemops/systemops_generic.go b/client/internal/routemanager/systemops/systemops_generic.go
index 3038c3ec5..31b7f3ac2 100644
--- a/client/internal/routemanager/systemops/systemops_generic.go
+++ b/client/internal/routemanager/systemops/systemops_generic.go
@@ -17,6 +17,7 @@ import (
nberrors "github.com/netbirdio/netbird/client/errors"
"github.com/netbirdio/netbird/client/iface"
+ "github.com/netbirdio/netbird/client/iface/netstack"
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
"github.com/netbirdio/netbird/client/internal/routemanager/util"
"github.com/netbirdio/netbird/client/internal/routemanager/vars"
@@ -62,6 +63,17 @@ func (r *SysOps) setupRefCounter(initAddresses []net.IP, stateManager *statemana
r.removeFromRouteTable,
)
+ if netstack.IsEnabled() {
+ refCounter = refcounter.New(
+ func(netip.Prefix, struct{}) (Nexthop, error) {
+ return Nexthop{}, refcounter.ErrIgnore
+ },
+ func(netip.Prefix, Nexthop) error {
+ return nil
+ },
+ )
+ }
+
r.refCounter = refCounter
return r.setupHooks(initAddresses, stateManager)
diff --git a/client/server/server.go b/client/server/server.go
index 5640ffa39..cba5a1148 100644
--- a/client/server/server.go
+++ b/client/server/server.go
@@ -91,6 +91,8 @@ func New(ctx context.Context, configPath, logFile string) *Server {
signalProbe: internal.NewProbe(),
relayProbe: internal.NewProbe(),
wgProbe: internal.NewProbe(),
+
+ persistNetworkMap: true,
}
}
diff --git a/go.mod b/go.mod
index 8b1410283..ee286df1c 100644
--- a/go.mod
+++ b/go.mod
@@ -77,6 +77,7 @@ require (
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
github.com/stretchr/testify v1.9.0
github.com/testcontainers/testcontainers-go v0.31.0
+ github.com/testcontainers/testcontainers-go/modules/mysql v0.31.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0
github.com/things-go/go-socks5 v0.0.4
github.com/yusufpapurcu/wmi v1.2.4
@@ -96,9 +97,10 @@ require (
golang.org/x/term v0.27.0
google.golang.org/api v0.177.0
gopkg.in/yaml.v3 v3.0.1
+ gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.7
gorm.io/driver/sqlite v1.5.3
- gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde
+ gorm.io/gorm v1.25.7
gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1
nhooyr.io/websocket v1.8.11
)
@@ -108,6 +110,7 @@ require (
cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
cloud.google.com/go/compute/metadata v0.3.0 // indirect
dario.cat/mergo v1.0.0 // indirect
+ filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
@@ -152,6 +155,7 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
+ github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-text/render v0.1.0 // indirect
github.com/go-text/typesetting v0.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
diff --git a/go.sum b/go.sum
index ffa17aed4..a571417df 100644
--- a/go.sum
+++ b/go.sum
@@ -48,6 +48,8 @@ cunicu.li/go-rosenpass v0.4.0/go.mod h1:MPbjH9nxV4l3vEagKVdFNwHOketqgS5/To1VYJpl
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
fyne.io/fyne/v2 v2.5.0 h1:lEjEIso0Vi4sJXYngIMoXOM6aUjqnPjK7pBpxRxG9aI=
fyne.io/fyne/v2 v2.5.0/go.mod h1:9D4oT3NWeG+MLi/lP7ItZZyujHC/qqMJpoGTAYX5Uqc=
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
@@ -238,6 +240,9 @@ github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
+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/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
@@ -681,6 +686,8 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/testcontainers/testcontainers-go v0.31.0 h1:W0VwIhcEVhRflwL9as3dhY6jXjVCA27AkmbnZ+UTh3U=
github.com/testcontainers/testcontainers-go v0.31.0/go.mod h1:D2lAoA0zUFiSY+eAflqK5mcUx/A5hrrORaEQrd0SefI=
+github.com/testcontainers/testcontainers-go/modules/mysql v0.31.0 h1:790+S8ewZYCbG+o8IiFlZ8ZZ33XbNO6zV9qhU6xhlRk=
+github.com/testcontainers/testcontainers-go/modules/mysql v0.31.0/go.mod h1:REFmO+lSG9S6uSBEwIMZCxeI36uhScjTwChYADeO3JA=
github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0 h1:isAwFS3KNKRbJMbWv+wolWqOFUECmjYZ+sIRZCIBc/E=
github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0/go.mod h1:ZNYY8vumNCEG9YI59A9d6/YaMY49uwRhmeU563EzFGw=
github.com/things-go/go-socks5 v0.0.4 h1:jMQjIc+qhD4z9cITOMnBiwo9dDmpGuXmBlkRFrl/qD0=
@@ -1225,12 +1232,14 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
+gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM=
gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
gorm.io/driver/sqlite v1.5.3 h1:7/0dUgX28KAcopdfbRWWl68Rflh6osa4rDh+m51KL2g=
gorm.io/driver/sqlite v1.5.3/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
-gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde h1:9DShaph9qhkIYw7QF91I/ynrr4cOO2PZra2PFD7Mfeg=
-gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A=
+gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY=
gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1 h1:qDCwdCWECGnwQSQC01Dpnp09fRHxJs9PbktotUqG+hs=
diff --git a/infrastructure_files/configure.sh b/infrastructure_files/configure.sh
index ff33004b2..d02e4f40c 100755
--- a/infrastructure_files/configure.sh
+++ b/infrastructure_files/configure.sh
@@ -53,6 +53,18 @@ if [[ "$NETBIRD_STORE_CONFIG_ENGINE" == "postgres" ]]; then
export NETBIRD_STORE_ENGINE_POSTGRES_DSN
fi
+# Check if MySQL is set as the store engine
+if [[ "$NETBIRD_STORE_CONFIG_ENGINE" == "mysql" ]]; then
+ # Exit if 'NETBIRD_STORE_ENGINE_MYSQL_DSN' is not set
+ if [[ -z "$NETBIRD_STORE_ENGINE_MYSQL_DSN" ]]; then
+ echo "Warning: NETBIRD_STORE_CONFIG_ENGINE=mysql but NETBIRD_STORE_ENGINE_MYSQL_DSN is not set."
+ echo "Please add the following line to your setup.env file:"
+ echo 'NETBIRD_STORE_ENGINE_MYSQL_DSN=":@tcp(127.0.0.1:3306)/"'
+ exit 1
+ fi
+ export NETBIRD_STORE_ENGINE_MYSQL_DSN
+fi
+
# local development or tests
if [[ $NETBIRD_DOMAIN == "localhost" || $NETBIRD_DOMAIN == "127.0.0.1" ]]; then
export NETBIRD_MGMT_SINGLE_ACCOUNT_MODE_DOMAIN="netbird.selfhosted"
diff --git a/infrastructure_files/docker-compose.yml.tmpl b/infrastructure_files/docker-compose.yml.tmpl
index ba68b3f8d..b7904fb5b 100644
--- a/infrastructure_files/docker-compose.yml.tmpl
+++ b/infrastructure_files/docker-compose.yml.tmpl
@@ -96,6 +96,7 @@ services:
max-file: "2"
environment:
- NETBIRD_STORE_ENGINE_POSTGRES_DSN=$NETBIRD_STORE_ENGINE_POSTGRES_DSN
+ - NETBIRD_STORE_ENGINE_MYSQL_DSN=$NETBIRD_STORE_ENGINE_MYSQL_DSN
# Coturn
coturn:
diff --git a/infrastructure_files/docker-compose.yml.tmpl.traefik b/infrastructure_files/docker-compose.yml.tmpl.traefik
index c4415d848..7d51c4ffb 100644
--- a/infrastructure_files/docker-compose.yml.tmpl.traefik
+++ b/infrastructure_files/docker-compose.yml.tmpl.traefik
@@ -83,6 +83,7 @@ services:
- traefik.http.services.netbird-management.loadbalancer.server.scheme=h2c
environment:
- NETBIRD_STORE_ENGINE_POSTGRES_DSN=$NETBIRD_STORE_ENGINE_POSTGRES_DSN
+ - NETBIRD_STORE_ENGINE_MYSQL_DSN=$NETBIRD_STORE_ENGINE_MYSQL_DSN
# Coturn
coturn:
diff --git a/management/server/account.go b/management/server/account.go
index 6c8205f26..41da7f079 100644
--- a/management/server/account.go
+++ b/management/server/account.go
@@ -438,7 +438,6 @@ func (am *DefaultAccountManager) handleGroupsPropagationSettings(ctx context.Con
}
func (am *DefaultAccountManager) handleInactivityExpirationSettings(ctx context.Context, account *types.Account, oldSettings, newSettings *types.Settings, userID, accountID string) error {
-
if newSettings.PeerInactivityExpirationEnabled {
if oldSettings.PeerInactivityExpiration != newSettings.PeerInactivityExpiration {
oldSettings.PeerInactivityExpiration = newSettings.PeerInactivityExpiration
@@ -790,7 +789,7 @@ func (am *DefaultAccountManager) lookupUserInCache(ctx context.Context, userID s
if user.Issued == types.UserIssuedIntegration {
continue
}
- users[user.Id] = userLoggedInOnce(!user.LastLogin.IsZero())
+ users[user.Id] = userLoggedInOnce(!user.GetLastLogin().IsZero())
}
log.WithContext(ctx).Debugf("looking up user %s of account %s in cache", userID, account.Id)
userData, err := am.lookupCache(ctx, users, account.Id)
@@ -1135,7 +1134,7 @@ func (am *DefaultAccountManager) MarkPATUsed(ctx context.Context, tokenID string
return fmt.Errorf("token not found")
}
- pat.LastUsed = time.Now().UTC()
+ pat.LastUsed = util.ToPtr(time.Now().UTC())
return am.Store.SaveAccount(ctx, account)
}
diff --git a/management/server/account_test.go b/management/server/account_test.go
index 39a748192..d8ceef0e7 100644
--- a/management/server/account_test.go
+++ b/management/server/account_test.go
@@ -16,6 +16,7 @@ import (
"time"
"github.com/golang-jwt/jwt"
+ "github.com/netbirdio/netbird/management/server/util"
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
@@ -145,7 +146,7 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) {
LoginExpired: true,
},
UserID: userID,
- LastLogin: time.Now().UTC().Add(-time.Hour * 24 * 30 * 30),
+ LastLogin: util.ToPtr(time.Now().UTC().Add(-time.Hour * 24 * 30 * 30)),
},
"peer-2": {
ID: peerID2,
@@ -159,7 +160,7 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) {
LoginExpired: false,
},
UserID: userID,
- LastLogin: time.Now().UTC(),
+ LastLogin: util.ToPtr(time.Now().UTC()),
LoginExpirationEnabled: true,
},
},
@@ -183,7 +184,7 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) {
LoginExpired: true,
},
UserID: userID,
- LastLogin: time.Now().UTC().Add(-time.Hour * 24 * 30 * 30),
+ LastLogin: util.ToPtr(time.Now().UTC().Add(-time.Hour * 24 * 30 * 30)),
LoginExpirationEnabled: true,
},
"peer-2": {
@@ -198,7 +199,7 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) {
LoginExpired: true,
},
UserID: userID,
- LastLogin: time.Now().UTC().Add(-time.Hour * 24 * 30 * 30),
+ LastLogin: util.ToPtr(time.Now().UTC().Add(-time.Hour * 24 * 30 * 30)),
LoginExpirationEnabled: true,
},
},
@@ -771,7 +772,6 @@ func TestDefaultAccountManager_MarkPATUsed(t *testing.T) {
"tokenId": {
ID: "tokenId",
HashedToken: encodedHashedToken,
- LastUsed: time.Time{},
},
},
}
@@ -793,7 +793,7 @@ func TestDefaultAccountManager_MarkPATUsed(t *testing.T) {
if err != nil {
t.Fatalf("Error when getting account: %s", err)
}
- assert.True(t, !account.Users["someUser"].PATs["tokenId"].LastUsed.IsZero())
+ assert.True(t, !account.Users["someUser"].PATs["tokenId"].GetLastUsed().IsZero())
}
func TestAccountManager_PrivateAccount(t *testing.T) {
@@ -1054,7 +1054,7 @@ func genUsers(p string, n int) map[string]*types.User {
users[fmt.Sprintf("%s-%d", p, i)] = &types.User{
Id: fmt.Sprintf("%s-%d", p, i),
Role: types.UserRoleAdmin,
- LastLogin: now,
+ LastLogin: util.ToPtr(now),
CreatedAt: now,
Issued: "api",
AutoGroups: []string{"one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"},
@@ -1706,10 +1706,10 @@ func TestAccount_Copy(t *testing.T) {
ID: "pat1",
Name: "First PAT",
HashedToken: "SoMeHaShEdToKeN",
- ExpirationDate: time.Now().UTC().AddDate(0, 0, 7),
+ ExpirationDate: util.ToPtr(time.Now().UTC().AddDate(0, 0, 7)),
CreatedBy: "user1",
CreatedAt: time.Now().UTC(),
- LastUsed: time.Now().UTC(),
+ LastUsed: util.ToPtr(time.Now().UTC()),
},
},
},
@@ -2065,7 +2065,7 @@ func TestAccount_GetExpiredPeers(t *testing.T) {
Connected: true,
LoginExpired: false,
},
- LastLogin: time.Now().UTC().Add(-30 * time.Minute),
+ LastLogin: util.ToPtr(time.Now().UTC().Add(-30 * time.Minute)),
UserID: userID,
},
"peer-2": {
@@ -2076,7 +2076,7 @@ func TestAccount_GetExpiredPeers(t *testing.T) {
Connected: true,
LoginExpired: false,
},
- LastLogin: time.Now().UTC().Add(-2 * time.Hour),
+ LastLogin: util.ToPtr(time.Now().UTC().Add(-2 * time.Hour)),
UserID: userID,
},
@@ -2088,7 +2088,7 @@ func TestAccount_GetExpiredPeers(t *testing.T) {
Connected: true,
LoginExpired: false,
},
- LastLogin: time.Now().UTC().Add(-1 * time.Hour),
+ LastLogin: util.ToPtr(time.Now().UTC().Add(-1 * time.Hour)),
UserID: userID,
},
},
@@ -2150,7 +2150,7 @@ func TestAccount_GetInactivePeers(t *testing.T) {
Connected: false,
LoginExpired: false,
},
- LastLogin: time.Now().UTC().Add(-30 * time.Minute),
+ LastLogin: util.ToPtr(time.Now().UTC().Add(-30 * time.Minute)),
UserID: userID,
},
"peer-2": {
@@ -2161,7 +2161,7 @@ func TestAccount_GetInactivePeers(t *testing.T) {
Connected: false,
LoginExpired: false,
},
- LastLogin: time.Now().UTC().Add(-2 * time.Hour),
+ LastLogin: util.ToPtr(time.Now().UTC().Add(-2 * time.Hour)),
UserID: userID,
},
"peer-3": {
@@ -2172,7 +2172,7 @@ func TestAccount_GetInactivePeers(t *testing.T) {
Connected: true,
LoginExpired: false,
},
- LastLogin: time.Now().UTC().Add(-1 * time.Hour),
+ LastLogin: util.ToPtr(time.Now().UTC().Add(-1 * time.Hour)),
UserID: userID,
},
},
@@ -2442,7 +2442,7 @@ func TestAccount_GetNextPeerExpiration(t *testing.T) {
LoginExpired: false,
},
LoginExpirationEnabled: true,
- LastLogin: time.Now().UTC(),
+ LastLogin: util.ToPtr(time.Now().UTC()),
UserID: userID,
},
"peer-2": {
@@ -2602,7 +2602,7 @@ func TestAccount_GetNextInactivePeerExpiration(t *testing.T) {
LastSeen: time.Now().Add(-1 * time.Second),
},
InactivityExpirationEnabled: true,
- LastLogin: time.Now().UTC(),
+ LastLogin: util.ToPtr(time.Now().UTC()),
UserID: userID,
},
"peer-2": {
@@ -2680,8 +2680,8 @@ func TestAccount_SetJWTGroups(t *testing.T) {
},
Settings: &types.Settings{GroupsPropagationEnabled: true, JWTGroupsEnabled: true, JWTGroupsClaimName: "groups"},
Users: map[string]*types.User{
- "user1": {Id: "user1", AccountID: "accountID"},
- "user2": {Id: "user2", AccountID: "accountID"},
+ "user1": {Id: "user1", AccountID: "accountID", CreatedAt: time.Now()},
+ "user2": {Id: "user2", AccountID: "accountID", CreatedAt: time.Now()},
},
}
@@ -3021,8 +3021,8 @@ func BenchmarkSyncAndMarkPeer(b *testing.B) {
minMsPerOpCICD float64
maxMsPerOpCICD float64
}{
- {"Small", 50, 5, 1, 3, 3, 11},
- {"Medium", 500, 100, 7, 13, 10, 70},
+ {"Small", 50, 5, 1, 3, 3, 14},
+ {"Medium", 500, 100, 7, 13, 10, 80},
{"Large", 5000, 200, 65, 80, 60, 220},
{"Small single", 50, 10, 1, 3, 3, 70},
{"Medium single", 500, 10, 7, 13, 10, 32},
@@ -3164,7 +3164,7 @@ func BenchmarkLoginPeer_NewPeer(b *testing.B) {
}{
{"Small", 50, 5, 107, 120, 107, 160},
{"Medium", 500, 100, 105, 140, 105, 220},
- {"Large", 5000, 200, 180, 220, 180, 350},
+ {"Large", 5000, 200, 180, 220, 180, 395},
{"Small single", 50, 10, 107, 120, 105, 160},
{"Medium single", 500, 10, 105, 140, 105, 170},
{"Large 5", 5000, 15, 180, 220, 180, 340},
diff --git a/management/server/event.go b/management/server/event.go
index 93b809226..788d1b51c 100644
--- a/management/server/event.go
+++ b/management/server/event.go
@@ -3,6 +3,7 @@ package server
import (
"context"
"fmt"
+ "os"
"time"
log "github.com/sirupsen/logrus"
@@ -11,6 +12,11 @@ import (
"github.com/netbirdio/netbird/management/server/status"
)
+func isEnabled() bool {
+ response := os.Getenv("NB_EVENT_ACTIVITY_LOG_ENABLED")
+ return response == "" || response == "true"
+}
+
// GetEvents returns a list of activity events of an account
func (am *DefaultAccountManager) GetEvents(ctx context.Context, accountID, userID string) ([]*activity.Event, error) {
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
@@ -56,20 +62,20 @@ func (am *DefaultAccountManager) GetEvents(ctx context.Context, accountID, userI
}
func (am *DefaultAccountManager) StoreEvent(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
-
- go func() {
- _, err := am.eventStore.Save(ctx, &activity.Event{
- Timestamp: time.Now().UTC(),
- Activity: activityID,
- InitiatorID: initiatorID,
- TargetID: targetID,
- AccountID: accountID,
- Meta: meta,
- })
- if err != nil {
- // todo add metric
- log.WithContext(ctx).Errorf("received an error while storing an activity event, error: %s", err)
- }
- }()
-
+ if isEnabled() {
+ go func() {
+ _, err := am.eventStore.Save(ctx, &activity.Event{
+ Timestamp: time.Now().UTC(),
+ Activity: activityID,
+ InitiatorID: initiatorID,
+ TargetID: targetID,
+ AccountID: accountID,
+ Meta: meta,
+ })
+ if err != nil {
+ // todo add metric
+ log.WithContext(ctx).Errorf("received an error while storing an activity event, error: %s", err)
+ }
+ }()
+ }
}
diff --git a/management/server/group_test.go b/management/server/group_test.go
index 834388d1e..cc90f187b 100644
--- a/management/server/group_test.go
+++ b/management/server/group_test.go
@@ -355,6 +355,7 @@ func initTestGroupAccount(am *DefaultAccountManager) (*DefaultAccountManager, *t
setupKey := &types.SetupKey{
Id: "example setup key",
AutoGroups: []string{groupForSetupKeys.ID},
+ UpdatedAt: time.Now(),
}
user := &types.User{
diff --git a/management/server/http/handlers/peers/peers_handler.go b/management/server/http/handlers/peers/peers_handler.go
index 5bc616e47..7eb8e2153 100644
--- a/management/server/http/handlers/peers/peers_handler.go
+++ b/management/server/http/handlers/peers/peers_handler.go
@@ -349,7 +349,7 @@ func toSinglePeerResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dnsD
UiVersion: peer.Meta.UIVersion,
DnsLabel: fqdn(peer, dnsDomain),
LoginExpirationEnabled: peer.LoginExpirationEnabled,
- LastLogin: peer.LastLogin,
+ LastLogin: peer.GetLastLogin(),
LoginExpired: peer.Status.LoginExpired,
ApprovalRequired: !approved,
CountryCode: peer.Location.CountryCode,
@@ -383,7 +383,7 @@ func toPeerListItemResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dn
UiVersion: peer.Meta.UIVersion,
DnsLabel: fqdn(peer, dnsDomain),
LoginExpirationEnabled: peer.LoginExpirationEnabled,
- LastLogin: peer.LastLogin,
+ LastLogin: peer.GetLastLogin(),
LoginExpired: peer.Status.LoginExpired,
AccessiblePeersCount: accessiblePeersCount,
CountryCode: peer.Location.CountryCode,
diff --git a/management/server/http/handlers/routes/routes_handler_test.go b/management/server/http/handlers/routes/routes_handler_test.go
index 4cee3ee30..45c465587 100644
--- a/management/server/http/handlers/routes/routes_handler_test.go
+++ b/management/server/http/handlers/routes/routes_handler_test.go
@@ -11,6 +11,7 @@ import (
"net/netip"
"testing"
+ "github.com/netbirdio/netbird/management/server/util"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/management/server/http/api"
@@ -239,7 +240,7 @@ func TestRoutesHandlers(t *testing.T) {
Id: existingRouteID,
Description: "Post",
NetworkId: "awesomeNet",
- Network: toPtr("192.168.0.0/16"),
+ Network: util.ToPtr("192.168.0.0/16"),
Peer: &existingPeerID,
NetworkType: route.IPv4NetworkString,
Masquerade: false,
@@ -259,7 +260,7 @@ func TestRoutesHandlers(t *testing.T) {
Id: existingRouteID,
Description: "Post",
NetworkId: "domainNet",
- Network: toPtr("invalid Prefix"),
+ Network: util.ToPtr("invalid Prefix"),
KeepRoute: true,
Domains: &[]string{existingDomain},
Peer: &existingPeerID,
@@ -281,7 +282,7 @@ func TestRoutesHandlers(t *testing.T) {
Id: existingRouteID,
Description: "Post",
NetworkId: "awesomeNet",
- Network: toPtr("192.168.0.0/16"),
+ Network: util.ToPtr("192.168.0.0/16"),
Peer: &existingPeerID,
NetworkType: route.IPv4NetworkString,
Masquerade: false,
@@ -385,7 +386,7 @@ func TestRoutesHandlers(t *testing.T) {
Id: existingRouteID,
Description: "Post",
NetworkId: "awesomeNet",
- Network: toPtr("192.168.0.0/16"),
+ Network: util.ToPtr("192.168.0.0/16"),
Peer: &existingPeerID,
NetworkType: route.IPv4NetworkString,
Masquerade: false,
@@ -404,7 +405,7 @@ func TestRoutesHandlers(t *testing.T) {
Id: existingRouteID,
Description: "Post",
NetworkId: "awesomeNet",
- Network: toPtr("invalid Prefix"),
+ Network: util.ToPtr("invalid Prefix"),
Domains: &[]string{existingDomain},
Peer: &existingPeerID,
NetworkType: route.DomainNetworkString,
@@ -425,7 +426,7 @@ func TestRoutesHandlers(t *testing.T) {
Id: existingRouteID,
Description: "Post",
NetworkId: "awesomeNet",
- Network: toPtr("192.168.0.0/16"),
+ Network: util.ToPtr("192.168.0.0/16"),
Peer: &emptyString,
PeerGroups: &[]string{existingGroupID},
NetworkType: route.IPv4NetworkString,
@@ -663,7 +664,3 @@ func toApiRoute(t *testing.T, r *route.Route) *api.Route {
require.NoError(t, err, "Failed to convert route")
return apiRoute
}
-
-func toPtr[T any](v T) *T {
- return &v
-}
diff --git a/management/server/http/handlers/setup_keys/setupkeys_handler.go b/management/server/http/handlers/setup_keys/setupkeys_handler.go
index a627d7203..67e296901 100644
--- a/management/server/http/handlers/setup_keys/setupkeys_handler.go
+++ b/management/server/http/handlers/setup_keys/setupkeys_handler.go
@@ -240,12 +240,12 @@ func ToResponseBody(key *types.SetupKey) *api.SetupKey {
Id: key.Id,
Key: key.KeySecret,
Name: key.Name,
- Expires: key.ExpiresAt,
+ Expires: key.GetExpiresAt(),
Type: string(key.Type),
Valid: key.IsValid(),
Revoked: key.Revoked,
UsedTimes: key.UsedTimes,
- LastUsed: key.LastUsed,
+ LastUsed: key.GetLastUsed(),
State: state,
AutoGroups: key.AutoGroups,
UpdatedAt: key.UpdatedAt,
diff --git a/management/server/http/handlers/users/pat_handler.go b/management/server/http/handlers/users/pat_handler.go
index 197785b34..7b93d2ae1 100644
--- a/management/server/http/handlers/users/pat_handler.go
+++ b/management/server/http/handlers/users/pat_handler.go
@@ -3,7 +3,6 @@ package users
import (
"encoding/json"
"net/http"
- "time"
"github.com/gorilla/mux"
@@ -166,17 +165,13 @@ func (h *patHandler) deleteToken(w http.ResponseWriter, r *http.Request) {
}
func toPATResponse(pat *types.PersonalAccessToken) *api.PersonalAccessToken {
- var lastUsed *time.Time
- if !pat.LastUsed.IsZero() {
- lastUsed = &pat.LastUsed
- }
return &api.PersonalAccessToken{
CreatedAt: pat.CreatedAt,
CreatedBy: pat.CreatedBy,
Name: pat.Name,
- ExpirationDate: pat.ExpirationDate,
+ ExpirationDate: pat.GetExpirationDate(),
Id: pat.ID,
- LastUsed: lastUsed,
+ LastUsed: pat.LastUsed,
}
}
diff --git a/management/server/http/handlers/users/pat_handler_test.go b/management/server/http/handlers/users/pat_handler_test.go
index 21bdc461e..9388067a4 100644
--- a/management/server/http/handlers/users/pat_handler_test.go
+++ b/management/server/http/handlers/users/pat_handler_test.go
@@ -12,6 +12,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/gorilla/mux"
+ "github.com/netbirdio/netbird/management/server/util"
"github.com/stretchr/testify/assert"
"github.com/netbirdio/netbird/management/server/http/api"
@@ -42,19 +43,19 @@ var testAccount = &types.Account{
ID: existingTokenID,
Name: "My first token",
HashedToken: "someHash",
- ExpirationDate: time.Now().UTC().AddDate(0, 0, 7),
+ ExpirationDate: util.ToPtr(time.Now().UTC().AddDate(0, 0, 7)),
CreatedBy: existingUserID,
CreatedAt: time.Now().UTC(),
- LastUsed: time.Now().UTC(),
+ LastUsed: util.ToPtr(time.Now().UTC()),
},
"token2": {
ID: "token2",
Name: "My second token",
HashedToken: "someOtherHash",
- ExpirationDate: time.Now().UTC().AddDate(0, 0, 7),
+ ExpirationDate: util.ToPtr(time.Now().UTC().AddDate(0, 0, 7)),
CreatedBy: existingUserID,
CreatedAt: time.Now().UTC(),
- LastUsed: time.Now().UTC(),
+ LastUsed: util.ToPtr(time.Now().UTC()),
},
},
},
@@ -248,8 +249,8 @@ func toTokenResponse(serverToken types.PersonalAccessToken) api.PersonalAccessTo
Id: serverToken.ID,
Name: serverToken.Name,
CreatedAt: serverToken.CreatedAt,
- LastUsed: &serverToken.LastUsed,
+ LastUsed: serverToken.LastUsed,
CreatedBy: serverToken.CreatedBy,
- ExpirationDate: serverToken.ExpirationDate,
+ ExpirationDate: serverToken.GetExpirationDate(),
}
}
diff --git a/management/server/http/middleware/auth_middleware.go b/management/server/http/middleware/auth_middleware.go
index 0a54cbaed..182c30cf6 100644
--- a/management/server/http/middleware/auth_middleware.go
+++ b/management/server/http/middleware/auth_middleware.go
@@ -161,7 +161,7 @@ func (m *AuthMiddleware) checkPATFromRequest(w http.ResponseWriter, r *http.Requ
if err != nil {
return fmt.Errorf("invalid Token: %w", err)
}
- if time.Now().After(pat.ExpirationDate) {
+ if time.Now().After(pat.GetExpirationDate()) {
return fmt.Errorf("token expired")
}
diff --git a/management/server/http/middleware/auth_middleware_test.go b/management/server/http/middleware/auth_middleware_test.go
index b0d970c5d..41bdb7fc5 100644
--- a/management/server/http/middleware/auth_middleware_test.go
+++ b/management/server/http/middleware/auth_middleware_test.go
@@ -9,6 +9,7 @@ import (
"time"
"github.com/golang-jwt/jwt"
+ "github.com/netbirdio/netbird/management/server/util"
"github.com/netbirdio/netbird/management/server/http/middleware/bypass"
"github.com/netbirdio/netbird/management/server/jwtclaims"
@@ -39,10 +40,10 @@ var testAccount = &types.Account{
ID: tokenID,
Name: "My first token",
HashedToken: "someHash",
- ExpirationDate: time.Now().UTC().AddDate(0, 0, 7),
+ ExpirationDate: util.ToPtr(time.Now().UTC().AddDate(0, 0, 7)),
CreatedBy: userID,
CreatedAt: time.Now().UTC(),
- LastUsed: time.Now().UTC(),
+ LastUsed: util.ToPtr(time.Now().UTC()),
},
},
},
diff --git a/management/server/http/testing/testdata/peers.sql b/management/server/http/testing/testdata/peers.sql
index 03ff2d3d3..863eda520 100644
--- a/management/server/http/testing/testdata/peers.sql
+++ b/management/server/http/testing/testdata/peers.sql
@@ -1,21 +1,21 @@
CREATE TABLE `accounts` (`id` text,`created_by` text,`created_at` datetime,`domain` text,`domain_category` text,`is_domain_primary_account` numeric,`network_identifier` text,`network_net` text,`network_dns` text,`network_serial` integer,`dns_settings_disabled_management_groups` text,`settings_peer_login_expiration_enabled` numeric,`settings_peer_login_expiration` integer,`settings_regular_users_view_blocked` numeric,`settings_groups_propagation_enabled` numeric,`settings_jwt_groups_enabled` numeric,`settings_jwt_groups_claim_name` text,`settings_jwt_allow_groups` text,`settings_extra_peer_approval_enabled` numeric,`settings_extra_integrated_validator_groups` text,PRIMARY KEY (`id`));
-CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
+CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime DEFAULT NULL,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
CREATE TABLE `groups` (`id` text,`account_id` text,`name` text,`issued` text,`peers` text,`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_groups_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
-CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`key_secret` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
+CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`key_secret` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
INSERT INTO accounts VALUES('testAccountId','','2024-10-02 16:01:38.000000000+00:00','test.com','private',1,'testNetworkIdentifier','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL);
-INSERT INTO users VALUES('testUserId','testAccountId','user',0,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.000000000+00:00','api',0,'');
-INSERT INTO users VALUES('testAdminId','testAccountId','admin',0,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.000000000+00:00','api',0,'');
-INSERT INTO users VALUES('testOwnerId','testAccountId','owner',0,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.000000000+00:00','api',0,'');
-INSERT INTO users VALUES('testServiceUserId','testAccountId','user',1,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.000000000+00:00','api',0,'');
-INSERT INTO users VALUES('testServiceAdminId','testAccountId','admin',1,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.000000000+00:00','api',0,'');
-INSERT INTO users VALUES('blockedUserId','testAccountId','admin',0,0,'','[]',1,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.000000000+00:00','api',0,'');
-INSERT INTO users VALUES('otherUserId','otherAccountId','admin',0,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.000000000+00:00','api',0,'');
+INSERT INTO users VALUES('testUserId','testAccountId','user',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
+INSERT INTO users VALUES('testAdminId','testAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
+INSERT INTO users VALUES('testOwnerId','testAccountId','owner',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
+INSERT INTO users VALUES('testServiceUserId','testAccountId','user',1,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
+INSERT INTO users VALUES('testServiceAdminId','testAccountId','admin',1,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
+INSERT INTO users VALUES('blockedUserId','testAccountId','admin',0,0,'','[]',1,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
+INSERT INTO users VALUES('otherUserId','otherAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
INSERT INTO "groups" VALUES('testGroupId','testAccountId','testGroupName','api','[]',0,'');
INSERT INTO "groups" VALUES('newGroupId','testAccountId','newGroupName','api','[]',0,'');
-INSERT INTO setup_keys VALUES('testKeyId','testAccountId','testKey','testK****','existingKey','one-off','2021-08-19 20:46:20.000000000+00:00','2321-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:000',0,0,'0001-01-01 00:00:00+00:00','["testGroupId"]',1,0);
-INSERT INTO setup_keys VALUES('revokedKeyId','testAccountId','revokedKey','testK****','existingKey','reusable','2021-08-19 20:46:20.000000000+00:00','2321-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:00',1,0,'0001-01-01 00:00:00+00:00','["testGroupId"]',3,0);
-INSERT INTO setup_keys VALUES('expiredKeyId','testAccountId','expiredKey','testK****','existingKey','reusable','2021-08-19 20:46:20.000000000+00:00','1921-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:00',0,1,'0001-01-01 00:00:00+00:00','["testGroupId"]',5,1);
+INSERT INTO setup_keys VALUES('testKeyId','testAccountId','testKey','testK****','existingKey','one-off','2021-08-19 20:46:20.000000000+00:00','2321-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:000',0,0,NULL,'["testGroupId"]',1,0);
+INSERT INTO setup_keys VALUES('revokedKeyId','testAccountId','revokedKey','testK****','existingKey','reusable','2021-08-19 20:46:20.000000000+00:00','2321-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:00',1,0,NULL,'["testGroupId"]',3,0);
+INSERT INTO setup_keys VALUES('expiredKeyId','testAccountId','expiredKey','testK****','existingKey','reusable','2021-08-19 20:46:20.000000000+00:00','1921-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:00',0,1,NULL,'["testGroupId"]',5,1);
CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
diff --git a/management/server/http/testing/testdata/setup_keys.sql b/management/server/http/testing/testdata/setup_keys.sql
index a315ea0f7..6d30fb5fe 100644
--- a/management/server/http/testing/testdata/setup_keys.sql
+++ b/management/server/http/testing/testdata/setup_keys.sql
@@ -1,24 +1,24 @@
CREATE TABLE `accounts` (`id` text,`created_by` text,`created_at` datetime,`domain` text,`domain_category` text,`is_domain_primary_account` numeric,`network_identifier` text,`network_net` text,`network_dns` text,`network_serial` integer,`dns_settings_disabled_management_groups` text,`settings_peer_login_expiration_enabled` numeric,`settings_peer_login_expiration` integer,`settings_regular_users_view_blocked` numeric,`settings_groups_propagation_enabled` numeric,`settings_jwt_groups_enabled` numeric,`settings_jwt_groups_claim_name` text,`settings_jwt_allow_groups` text,`settings_extra_peer_approval_enabled` numeric,`settings_extra_integrated_validator_groups` text,PRIMARY KEY (`id`));
-CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
-CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
+CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime DEFAULT NULL,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
+CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime DEFAULT NULL,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime DEFAULT NULL,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
CREATE TABLE `groups` (`id` text,`account_id` text,`name` text,`issued` text,`peers` text,`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_groups_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
INSERT INTO accounts VALUES('testAccountId','','2024-10-02 16:01:38.000000000+00:00','test.com','private',1,'testNetworkIdentifier','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL);
-INSERT INTO users VALUES('testUserId','testAccountId','user',0,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.000000000+00:00','api',0,'');
-INSERT INTO users VALUES('testAdminId','testAccountId','admin',0,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.000000000+00:00','api',0,'');
-INSERT INTO users VALUES('testOwnerId','testAccountId','owner',0,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.000000000+00:00','api',0,'');
-INSERT INTO users VALUES('testServiceUserId','testAccountId','user',1,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.000000000+00:00','api',0,'');
-INSERT INTO users VALUES('testServiceAdminId','testAccountId','admin',1,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.000000000+00:00','api',0,'');
-INSERT INTO users VALUES('blockedUserId','testAccountId','admin',0,0,'','[]',1,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.000000000+00:00','api',0,'');
-INSERT INTO users VALUES('otherUserId','otherAccountId','admin',0,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.000000000+00:00','api',0,'');
+INSERT INTO users VALUES('testUserId','testAccountId','user',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
+INSERT INTO users VALUES('testAdminId','testAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
+INSERT INTO users VALUES('testOwnerId','testAccountId','owner',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
+INSERT INTO users VALUES('testServiceUserId','testAccountId','user',1,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
+INSERT INTO users VALUES('testServiceAdminId','testAccountId','admin',1,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
+INSERT INTO users VALUES('blockedUserId','testAccountId','admin',0,0,'','[]',1,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
+INSERT INTO users VALUES('otherUserId','otherAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
INSERT INTO peers VALUES('testPeerId','testAccountId','5rvhvriKJZ3S9oxYToVj5TzDM9u9y8cxg7htIMWlYAg=','72546A29-6BC8-4311-BCFC-9CDBF33F1A48','"100.64.114.31"','f2a34f6a4731','linux','Linux','11','unknown','Debian GNU/Linux','','0.12.0','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'f2a34f6a4731','f2a34f6a4731','2023-03-02 09:21:02.189035775+01:00',0,0,0,'','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILzUUSYG/LGnV8zarb2SGN+tib/PZ+M7cL4WtTzUrTpk',0,1,'2023-03-01 19:48:19.817799698+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0);
INSERT INTO "groups" VALUES('testGroupId','testAccountId','testGroupName','api','[]',0,'');
INSERT INTO "groups" VALUES('newGroupId','testAccountId','newGroupName','api','[]',0,'');
-CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`key_secret` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
+CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`key_secret` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime DEFAULT NULL,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
-INSERT INTO setup_keys VALUES('testKeyId','testAccountId','testKey','testK****','existingKey','one-off','2021-08-19 20:46:20.000000000+00:00','2321-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:000',0,0,'0001-01-01 00:00:00+00:00','["testGroupId"]',1,0);
-INSERT INTO setup_keys VALUES('revokedKeyId','testAccountId','revokedKey','testK****','existingKey','reusable','2021-08-19 20:46:20.000000000+00:00','2321-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:00',1,0,'0001-01-01 00:00:00+00:00','["testGroupId"]',3,0);
-INSERT INTO setup_keys VALUES('expiredKeyId','testAccountId','expiredKey','testK****','existingKey','reusable','2021-08-19 20:46:20.000000000+00:00','1921-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:00',0,1,'0001-01-01 00:00:00+00:00','["testGroupId"]',5,1);
+INSERT INTO setup_keys VALUES('testKeyId','testAccountId','testKey','testK****','existingKey','one-off','2021-08-19 20:46:20.000000000+00:00','2321-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:000',0,0,NULL,'["testGroupId"]',1,0);
+INSERT INTO setup_keys VALUES('revokedKeyId','testAccountId','revokedKey','testK****','existingKey','reusable','2021-08-19 20:46:20.000000000+00:00','2321-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:00',1,0,NULL,'["testGroupId"]',3,0);
+INSERT INTO setup_keys VALUES('expiredKeyId','testAccountId','expiredKey','testK****','existingKey','reusable','2021-08-19 20:46:20.000000000+00:00','1921-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:00',0,1,NULL,'["testGroupId"]',5,1);
diff --git a/management/server/http/testing/testdata/users.sql b/management/server/http/testing/testdata/users.sql
index da7cae565..346f7b7ac 100644
--- a/management/server/http/testing/testdata/users.sql
+++ b/management/server/http/testing/testdata/users.sql
@@ -1,23 +1,23 @@
CREATE TABLE `accounts` (`id` text,`created_by` text,`created_at` datetime,`domain` text,`domain_category` text,`is_domain_primary_account` numeric,`network_identifier` text,`network_net` text,`network_dns` text,`network_serial` integer,`dns_settings_disabled_management_groups` text,`settings_peer_login_expiration_enabled` numeric,`settings_peer_login_expiration` integer,`settings_regular_users_view_blocked` numeric,`settings_groups_propagation_enabled` numeric,`settings_jwt_groups_enabled` numeric,`settings_jwt_groups_claim_name` text,`settings_jwt_allow_groups` text,`settings_extra_peer_approval_enabled` numeric,`settings_extra_integrated_validator_groups` text,PRIMARY KEY (`id`));
CREATE TABLE `groups` (`id` text,`account_id` text,`name` text,`issued` text,`peers` text,`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_groups_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
-CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`key_secret` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
+CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`key_secret` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
INSERT INTO accounts VALUES('testAccountId','','2024-10-02 16:01:38.000000000+00:00','test.com','private',1,'testNetworkIdentifier','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL);
INSERT INTO "groups" VALUES('testGroupId','testAccountId','testGroupName','api','[]',0,'');
INSERT INTO "groups" VALUES('newGroupId','testAccountId','newGroupName','api','[]',0,'');
-INSERT INTO setup_keys VALUES('testKeyId','testAccountId','testKey','testK****','existingKey','one-off','2021-08-19 20:46:20.000000000+00:00','2321-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:000',0,0,'0001-01-01 00:00:00+00:00','["testGroupId"]',1,0);
-INSERT INTO setup_keys VALUES('revokedKeyId','testAccountId','revokedKey','testK****','existingKey','reusable','2021-08-19 20:46:20.000000000+00:00','2321-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:00',1,0,'0001-01-01 00:00:00+00:00','["testGroupId"]',3,0);
-INSERT INTO setup_keys VALUES('expiredKeyId','testAccountId','expiredKey','testK****','existingKey','reusable','2021-08-19 20:46:20.000000000+00:00','1921-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:00',0,1,'0001-01-01 00:00:00+00:00','["testGroupId"]',5,1);
+INSERT INTO setup_keys VALUES('testKeyId','testAccountId','testKey','testK****','existingKey','one-off','2021-08-19 20:46:20.000000000+00:00','2321-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:000',0,0,NULL,'["testGroupId"]',1,0);
+INSERT INTO setup_keys VALUES('revokedKeyId','testAccountId','revokedKey','testK****','existingKey','reusable','2021-08-19 20:46:20.000000000+00:00','2321-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:00',1,0,NULL,'["testGroupId"]',3,0);
+INSERT INTO setup_keys VALUES('expiredKeyId','testAccountId','expiredKey','testK****','existingKey','reusable','2021-08-19 20:46:20.000000000+00:00','1921-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:00',0,1,NULL,'["testGroupId"]',5,1);
INSERT INTO peers VALUES('testPeerId','testAccountId','5rvhvriKJZ3S9oxYToVj5TzDM9u9y8cxg7htIMWlYAg=','72546A29-6BC8-4311-BCFC-9CDBF33F1A48','"100.64.114.31"','f2a34f6a4731','linux','Linux','11','unknown','Debian GNU/Linux','','0.12.0','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'f2a34f6a4731','f2a34f6a4731','2023-03-02 09:21:02.189035775+01:00',0,0,0,'','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILzUUSYG/LGnV8zarb2SGN+tib/PZ+M7cL4WtTzUrTpk',0,1,'2023-03-01 19:48:19.817799698+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0);
-CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
+CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime DEFAULT NULL,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
-INSERT INTO users VALUES('testUserId','testAccountId','user',0,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.000000000+00:00','api',0,'');
-INSERT INTO users VALUES('testAdminId','testAccountId','admin',0,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.000000000+00:00','api',0,'');
-INSERT INTO users VALUES('testOwnerId','testAccountId','owner',0,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.000000000+00:00','api',0,'');
-INSERT INTO users VALUES('testServiceUserId','testAccountId','user',1,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.000000000+00:00','api',0,'');
-INSERT INTO users VALUES('testServiceAdminId','testAccountId','admin',1,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.000000000+00:00','api',0,'');
-INSERT INTO users VALUES('blockedUserId','testAccountId','admin',0,0,'','[]',1,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.000000000+00:00','api',0,'');
-INSERT INTO users VALUES('otherUserId','otherAccountId','admin',0,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.000000000+00:00','api',0,'');
+INSERT INTO users VALUES('testUserId','testAccountId','user',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
+INSERT INTO users VALUES('testAdminId','testAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
+INSERT INTO users VALUES('testOwnerId','testAccountId','owner',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
+INSERT INTO users VALUES('testServiceUserId','testAccountId','user',1,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
+INSERT INTO users VALUES('testServiceAdminId','testAccountId','admin',1,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
+INSERT INTO users VALUES('blockedUserId','testAccountId','admin',0,0,'','[]',1,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
+INSERT INTO users VALUES('otherUserId','otherAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
diff --git a/management/server/http/testing/testing_tools/tools.go b/management/server/http/testing/testing_tools/tools.go
index 534836250..006d5679c 100644
--- a/management/server/http/testing/testing_tools/tools.go
+++ b/management/server/http/testing/testing_tools/tools.go
@@ -13,6 +13,7 @@ import (
"testing"
"time"
+ "github.com/netbirdio/netbird/management/server/util"
"github.com/stretchr/testify/assert"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
@@ -199,7 +200,7 @@ func PopulateTestData(b *testing.B, am *server.DefaultAccountManager, peers, gro
DNSLabel: fmt.Sprintf("oldpeer-%d", i),
Key: peerKey.PublicKey().String(),
IP: net.ParseIP(fmt.Sprintf("100.64.%d.%d", i/256, i%256)),
- Status: &nbpeer.PeerStatus{},
+ Status: &nbpeer.PeerStatus{LastSeen: time.Now().UTC(), Connected: true},
UserID: TestUserId,
}
account.Peers[peer.ID] = peer
@@ -220,7 +221,8 @@ func PopulateTestData(b *testing.B, am *server.DefaultAccountManager, peers, gro
Id: fmt.Sprintf("oldkey-%d", i),
AccountID: account.Id,
AutoGroups: []string{"someGroupID"},
- ExpiresAt: time.Now().Add(ExpiresIn * time.Second),
+ UpdatedAt: time.Now().UTC(),
+ ExpiresAt: util.ToPtr(time.Now().Add(ExpiresIn * time.Second)),
Name: NewKeyName + strconv.Itoa(i),
Type: "reusable",
UsageLimit: 0,
diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go
index c66423736..8147afa44 100644
--- a/management/server/management_proto_test.go
+++ b/management/server/management_proto_test.go
@@ -475,8 +475,14 @@ func createRawClient(addr string) (mgmtProto.ManagementServiceClient, *grpc.Clie
func Test_SyncStatusRace(t *testing.T) {
t.Skip()
- if os.Getenv("CI") == "true" && os.Getenv("NETBIRD_STORE_ENGINE") == "postgres" {
- t.Skip("Skipping on CI and Postgres store")
+ if os.Getenv("CI") == "true" {
+ if os.Getenv("NETBIRD_STORE_ENGINE") == "postgres" {
+ t.Skip("Skipping on CI and Postgres store")
+ }
+
+ if os.Getenv("NETBIRD_STORE_ENGINE") == "mysql" {
+ t.Skip("Skipping on CI and MySQL store")
+ }
}
for i := 0; i < 500; i++ {
t.Run(fmt.Sprintf("TestRun-%d", i), func(t *testing.T) {
diff --git a/management/server/migration/migration.go b/management/server/migration/migration.go
index 6f12d94b4..0a6951736 100644
--- a/management/server/migration/migration.go
+++ b/management/server/migration/migration.go
@@ -17,12 +17,19 @@ import (
"gorm.io/gorm"
)
+func GetColumnName(db *gorm.DB, column string) string {
+ if db.Name() == "mysql" {
+ return fmt.Sprintf("`%s`", column)
+ }
+ return column
+}
+
// MigrateFieldFromGobToJSON migrates a column from Gob encoding to JSON encoding.
// T is the type of the model that contains the field to be migrated.
// S is the type of the field to be migrated.
func MigrateFieldFromGobToJSON[T any, S any](ctx context.Context, db *gorm.DB, fieldName string) error {
-
- oldColumnName := fieldName
+ orgColumnName := fieldName
+ oldColumnName := GetColumnName(db, orgColumnName)
newColumnName := fieldName + "_tmp"
var model T
@@ -72,7 +79,7 @@ func MigrateFieldFromGobToJSON[T any, S any](ctx context.Context, db *gorm.DB, f
for _, row := range rows {
var field S
- str, ok := row[oldColumnName].(string)
+ str, ok := row[orgColumnName].(string)
if !ok {
return fmt.Errorf("type assertion failed")
}
@@ -111,7 +118,8 @@ func MigrateFieldFromGobToJSON[T any, S any](ctx context.Context, db *gorm.DB, f
// MigrateNetIPFieldFromBlobToJSON migrates a Net IP column from Blob encoding to JSON encoding.
// T is the type of the model that contains the field to be migrated.
func MigrateNetIPFieldFromBlobToJSON[T any](ctx context.Context, db *gorm.DB, fieldName string, indexName string) error {
- oldColumnName := fieldName
+ orgColumnName := fieldName
+ oldColumnName := GetColumnName(db, orgColumnName)
newColumnName := fieldName + "_tmp"
var model T
@@ -163,7 +171,7 @@ func MigrateNetIPFieldFromBlobToJSON[T any](ctx context.Context, db *gorm.DB, fi
for _, row := range rows {
var blobValue string
- if columnValue := row[oldColumnName]; columnValue != nil {
+ if columnValue := row[orgColumnName]; columnValue != nil {
value, ok := columnValue.(string)
if !ok {
return fmt.Errorf("type assertion failed")
@@ -210,7 +218,8 @@ func MigrateNetIPFieldFromBlobToJSON[T any](ctx context.Context, db *gorm.DB, fi
}
func MigrateSetupKeyToHashedSetupKey[T any](ctx context.Context, db *gorm.DB) error {
- oldColumnName := "key"
+ orgColumnName := "key"
+ oldColumnName := GetColumnName(db, orgColumnName)
newColumnName := "key_secret"
var model T
@@ -250,8 +259,9 @@ func MigrateSetupKeyToHashedSetupKey[T any](ctx context.Context, db *gorm.DB) er
}
for _, row := range rows {
+
var plainKey string
- if columnValue := row[oldColumnName]; columnValue != nil {
+ if columnValue := row[orgColumnName]; columnValue != nil {
value, ok := columnValue.(string)
if !ok {
return fmt.Errorf("type assertion failed")
diff --git a/management/server/peer.go b/management/server/peer.go
index ad20d279a..6c4c2d100 100644
--- a/management/server/peer.go
+++ b/management/server/peer.go
@@ -11,6 +11,7 @@ import (
"sync"
"time"
+ "github.com/netbirdio/netbird/management/server/util"
"github.com/rs/xid"
log "github.com/sirupsen/logrus"
@@ -511,7 +512,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s
Status: &nbpeer.PeerStatus{Connected: false, LastSeen: registrationTime},
SSHEnabled: false,
SSHKey: peer.SSHKey,
- LastLogin: registrationTime,
+ LastLogin: util.ToPtr(registrationTime),
CreatedAt: registrationTime,
LoginExpirationEnabled: addedByUser,
Ephemeral: ephemeral,
@@ -566,7 +567,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s
}
if addedByUser {
- err := transaction.SaveUserLastLogin(ctx, accountID, userID, newPeer.LastLogin)
+ err := transaction.SaveUserLastLogin(ctx, accountID, userID, newPeer.GetLastLogin())
if err != nil {
return fmt.Errorf("failed to update user last login: %w", err)
}
@@ -911,7 +912,7 @@ func (am *DefaultAccountManager) handleExpiredPeer(ctx context.Context, user *ty
return err
}
- err = am.Store.SaveUserLastLogin(ctx, user.AccountID, user.Id, peer.LastLogin)
+ err = am.Store.SaveUserLastLogin(ctx, user.AccountID, user.Id, peer.GetLastLogin())
if err != nil {
return err
}
diff --git a/management/server/peer/peer.go b/management/server/peer/peer.go
index 34d791844..355d78ce0 100644
--- a/management/server/peer/peer.go
+++ b/management/server/peer/peer.go
@@ -6,6 +6,8 @@ import (
"slices"
"sort"
"time"
+
+ "github.com/netbirdio/netbird/management/server/util"
)
// Peer represents a machine connected to the network.
@@ -40,7 +42,7 @@ type Peer struct {
InactivityExpirationEnabled bool
// LastLogin the time when peer performed last login operation
- LastLogin time.Time
+ LastLogin *time.Time
// CreatedAt records the time the peer was created
CreatedAt time.Time
// Indicate ephemeral peer attribute
@@ -222,6 +224,15 @@ func (p *Peer) UpdateMetaIfNew(meta PeerSystemMeta) bool {
return true
}
+// GetLastLogin returns the last login time of the peer.
+func (p *Peer) GetLastLogin() time.Time {
+ if p.LastLogin != nil {
+ return *p.LastLogin
+ }
+ return time.Time{}
+
+}
+
// MarkLoginExpired marks peer's status expired or not
func (p *Peer) MarkLoginExpired(expired bool) {
newStatus := p.Status.Copy()
@@ -258,7 +269,7 @@ func (p *Peer) LoginExpired(expiresIn time.Duration) (bool, time.Duration) {
if !p.AddedWithSSOLogin() || !p.LoginExpirationEnabled {
return false, 0
}
- expiresAt := p.LastLogin.Add(expiresIn)
+ expiresAt := p.GetLastLogin().Add(expiresIn)
now := time.Now()
timeLeft := expiresAt.Sub(now)
return timeLeft <= 0, timeLeft
@@ -291,7 +302,7 @@ func (p *PeerStatus) Copy() *PeerStatus {
// UpdateLastLogin and set login expired false
func (p *Peer) UpdateLastLogin() *Peer {
- p.LastLogin = time.Now().UTC()
+ p.LastLogin = util.ToPtr(time.Now().UTC())
newStatus := p.Status.Copy()
newStatus.LoginExpired = false
p.Status = newStatus
diff --git a/management/server/peer_test.go b/management/server/peer_test.go
index 9ad67d2bf..5f500c226 100644
--- a/management/server/peer_test.go
+++ b/management/server/peer_test.go
@@ -13,6 +13,7 @@ import (
"testing"
"time"
+ "github.com/netbirdio/netbird/management/server/util"
"github.com/rs/xid"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
@@ -80,7 +81,7 @@ func TestPeer_LoginExpired(t *testing.T) {
t.Run(c.name, func(t *testing.T) {
peer := &nbpeer.Peer{
LoginExpirationEnabled: c.expirationEnabled,
- LastLogin: c.lastLogin,
+ LastLogin: util.ToPtr(c.lastLogin),
UserID: userID,
}
@@ -141,7 +142,7 @@ func TestPeer_SessionExpired(t *testing.T) {
}
peer := &nbpeer.Peer{
InactivityExpirationEnabled: c.expirationEnabled,
- LastLogin: c.lastLogin,
+ LastLogin: util.ToPtr(c.lastLogin),
Status: peerStatus,
UserID: userID,
}
@@ -744,7 +745,7 @@ func setupTestAccountManager(b *testing.B, peers int, groups int) (*DefaultAccou
DNSLabel: fmt.Sprintf("peer-%d", i),
Key: peerKey.PublicKey().String(),
IP: net.ParseIP(fmt.Sprintf("100.64.%d.%d", i/256, i%256)),
- Status: &nbpeer.PeerStatus{},
+ Status: &nbpeer.PeerStatus{LastSeen: time.Now().UTC(), Connected: true},
UserID: regularUser,
}
account.Peers[peer.ID] = peer
@@ -783,7 +784,7 @@ func setupTestAccountManager(b *testing.B, peers int, groups int) (*DefaultAccou
DNSLabel: fmt.Sprintf("peer-nr-%d", len(account.Peers)+1),
Key: peerKey.PublicKey().String(),
IP: peerIP,
- Status: &nbpeer.PeerStatus{},
+ Status: &nbpeer.PeerStatus{LastSeen: time.Now().UTC(), Connected: true},
UserID: regularUser,
Meta: nbpeer.PeerSystemMeta{
Hostname: fmt.Sprintf("peer-nr-%d", len(account.Peers)+1),
@@ -1209,7 +1210,7 @@ func Test_RegisterPeerByUser(t *testing.T) {
UserID: existingUserID,
Status: &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now()},
SSHEnabled: false,
- LastLogin: time.Now(),
+ LastLogin: util.ToPtr(time.Now()),
}
addedPeer, _, _, err := am.AddPeer(context.Background(), "", existingUserID, newPeer)
@@ -1231,7 +1232,7 @@ func Test_RegisterPeerByUser(t *testing.T) {
lastLogin, err := time.Parse("2006-01-02T15:04:05Z", "0001-01-01T00:00:00Z")
assert.NoError(t, err)
- assert.NotEqual(t, lastLogin, account.Users[existingUserID].LastLogin)
+ assert.NotEqual(t, lastLogin, account.Users[existingUserID].GetLastLogin())
}
func Test_RegisterPeerBySetupKey(t *testing.T) {
@@ -1361,7 +1362,7 @@ func Test_RegisterPeerRollbackOnFailure(t *testing.T) {
hashedKey := sha256.Sum256([]byte(faultyKey))
encodedHashedKey := b64.StdEncoding.EncodeToString(hashedKey[:])
- assert.Equal(t, lastUsed, account.SetupKeys[encodedHashedKey].LastUsed.UTC())
+ assert.Equal(t, lastUsed, account.SetupKeys[encodedHashedKey].GetLastUsed().UTC())
assert.Equal(t, 0, account.SetupKeys[encodedHashedKey].UsedTimes)
}
diff --git a/management/server/route_test.go b/management/server/route_test.go
index 2ef2b0173..48dbd11ed 100644
--- a/management/server/route_test.go
+++ b/management/server/route_test.go
@@ -1307,7 +1307,7 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*types.Accou
WtVersion: "development",
UIVersion: "development",
},
- Status: &nbpeer.PeerStatus{},
+ Status: &nbpeer.PeerStatus{LastSeen: time.Now().UTC(), Connected: true},
}
account.Peers[peer1.ID] = peer1
@@ -1334,7 +1334,7 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*types.Accou
WtVersion: "development",
UIVersion: "development",
},
- Status: &nbpeer.PeerStatus{},
+ Status: &nbpeer.PeerStatus{LastSeen: time.Now().UTC(), Connected: true},
}
account.Peers[peer2.ID] = peer2
@@ -1361,7 +1361,7 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*types.Accou
WtVersion: "development",
UIVersion: "development",
},
- Status: &nbpeer.PeerStatus{},
+ Status: &nbpeer.PeerStatus{LastSeen: time.Now().UTC(), Connected: true},
}
account.Peers[peer3.ID] = peer3
@@ -1388,7 +1388,7 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*types.Accou
WtVersion: "development",
UIVersion: "development",
},
- Status: &nbpeer.PeerStatus{},
+ Status: &nbpeer.PeerStatus{LastSeen: time.Now().UTC(), Connected: true},
}
account.Peers[peer4.ID] = peer4
@@ -1415,7 +1415,7 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*types.Accou
WtVersion: "development",
UIVersion: "development",
},
- Status: &nbpeer.PeerStatus{},
+ Status: &nbpeer.PeerStatus{LastSeen: time.Now().UTC(), Connected: true},
}
account.Peers[peer5.ID] = peer5
diff --git a/management/server/setupkey_test.go b/management/server/setupkey_test.go
index f728db5d4..e225ec54b 100644
--- a/management/server/setupkey_test.go
+++ b/management/server/setupkey_test.go
@@ -66,7 +66,7 @@ func TestDefaultAccountManager_SaveSetupKey(t *testing.T) {
t.Fatal(err)
}
- assertKey(t, newKey, keyName, revoked, "reusable", 0, key.CreatedAt, key.ExpiresAt,
+ assertKey(t, newKey, keyName, revoked, "reusable", 0, key.CreatedAt, key.GetExpiresAt(),
key.Id, time.Now().UTC(), autoGroups, true)
// check the corresponding events that should have been generated
@@ -336,8 +336,8 @@ func assertKey(t *testing.T, key *types.SetupKey, expectedName string, expectedR
t.Errorf("expected setup key to have UsedTimes = %v, got %v", expectedUsedTimes, key.UsedTimes)
}
- if key.ExpiresAt.Sub(expectedExpiresAt).Round(time.Hour) != 0 {
- t.Errorf("expected setup key to have ExpiresAt ~ %v, got %v", expectedExpiresAt, key.ExpiresAt)
+ if key.GetExpiresAt().Sub(expectedExpiresAt).Round(time.Hour) != 0 {
+ t.Errorf("expected setup key to have ExpiresAt ~ %v, got %v", expectedExpiresAt, key.GetExpiresAt())
}
if key.UpdatedAt.Sub(expectedUpdatedAt).Round(time.Hour) != 0 {
@@ -391,7 +391,7 @@ func TestSetupKey_Copy(t *testing.T) {
key, _ := types.GenerateSetupKey("key name", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false)
keyCopy := key.Copy()
- assertKey(t, keyCopy, key.Name, key.Revoked, string(key.Type), key.UsedTimes, key.CreatedAt, key.ExpiresAt, key.Id,
+ assertKey(t, keyCopy, key.Name, key.Revoked, string(key.Type), key.UsedTimes, key.CreatedAt, key.GetExpiresAt(), key.Id,
key.UpdatedAt, key.AutoGroups, true)
}
diff --git a/management/server/store/file_store.go b/management/server/store/file_store.go
index f40a0392b..4c9134e41 100644
--- a/management/server/store/file_store.go
+++ b/management/server/store/file_store.go
@@ -14,6 +14,7 @@ import (
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/telemetry"
"github.com/netbirdio/netbird/management/server/types"
+ nbutil "github.com/netbirdio/netbird/management/server/util"
"github.com/netbirdio/netbird/util"
)
@@ -175,8 +176,8 @@ func restore(ctx context.Context, file string) (*FileStore, error) {
migrationPeers := make(map[string]*nbpeer.Peer) // key to Peer
for key, peer := range account.Peers {
// set LastLogin for the peers that were onboarded before the peer login expiration feature
- if peer.LastLogin.IsZero() {
- peer.LastLogin = time.Now().UTC()
+ if peer.GetLastLogin().IsZero() {
+ peer.LastLogin = nbutil.ToPtr(time.Now().UTC())
}
if peer.ID != "" {
continue
diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go
index 62b004f9c..7b1a63411 100644
--- a/management/server/store/sql_store.go
+++ b/management/server/store/sql_store.go
@@ -16,6 +16,7 @@ import (
"time"
log "github.com/sirupsen/logrus"
+ "gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
@@ -39,6 +40,7 @@ const (
storeSqliteFileName = "store.db"
idQueryCondition = "id = ?"
keyQueryCondition = "key = ?"
+ mysqlKeyQueryCondition = "`key` = ?"
accountAndIDQueryCondition = "account_id = ? and id = ?"
accountAndIDsQueryCondition = "account_id = ? AND id IN ?"
accountIDCondition = "account_id = ?"
@@ -101,6 +103,13 @@ func NewSqlStore(ctx context.Context, db *gorm.DB, storeEngine Engine, metrics t
return &SqlStore{db: db, storeEngine: storeEngine, metrics: metrics, installationPK: 1}, nil
}
+func GetKeyQueryCondition(s *SqlStore) string {
+ if s.storeEngine == MysqlStoreEngine {
+ return mysqlKeyQueryCondition
+ }
+ return keyQueryCondition
+}
+
// AcquireGlobalLock acquires global lock across all the accounts and returns a function that releases the lock
func (s *SqlStore) AcquireGlobalLock(ctx context.Context) (unlock func()) {
log.WithContext(ctx).Tracef("acquiring global lock")
@@ -397,7 +406,7 @@ func (s *SqlStore) SavePeerLocation(accountID string, peerWithLocation *nbpeer.P
return result.Error
}
- if result.RowsAffected == 0 {
+ if result.RowsAffected == 0 && s.storeEngine != MysqlStoreEngine {
return status.Errorf(status.NotFound, peerNotFoundFMT, peerWithLocation.ID)
}
@@ -487,7 +496,7 @@ func (s *SqlStore) GetAccountIDByPrivateDomain(ctx context.Context, lockStrength
func (s *SqlStore) GetAccountBySetupKey(ctx context.Context, setupKey string) (*types.Account, error) {
var key types.SetupKey
- result := s.db.Select("account_id").First(&key, keyQueryCondition, setupKey)
+ result := s.db.Select("account_id").First(&key, GetKeyQueryCondition(s), setupKey)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, status.NewSetupKeyNotFoundError(setupKey)
@@ -734,7 +743,8 @@ func (s *SqlStore) GetAccountByPeerID(ctx context.Context, peerID string) (*type
func (s *SqlStore) GetAccountByPeerPubKey(ctx context.Context, peerKey string) (*types.Account, error) {
var peer nbpeer.Peer
- result := s.db.Select("account_id").First(&peer, keyQueryCondition, peerKey)
+ result := s.db.Select("account_id").First(&peer, GetKeyQueryCondition(s), peerKey)
+
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, status.Errorf(status.NotFound, "account not found: index lookup failed")
@@ -752,7 +762,7 @@ func (s *SqlStore) GetAccountByPeerPubKey(ctx context.Context, peerKey string) (
func (s *SqlStore) GetAccountIDByPeerPubKey(ctx context.Context, peerKey string) (string, error) {
var peer nbpeer.Peer
var accountID string
- result := s.db.Model(&peer).Select("account_id").Where(keyQueryCondition, peerKey).First(&accountID)
+ result := s.db.Model(&peer).Select("account_id").Where(GetKeyQueryCondition(s), peerKey).First(&accountID)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return "", status.Errorf(status.NotFound, "account not found: index lookup failed")
@@ -778,7 +788,7 @@ func (s *SqlStore) GetAccountIDByUserID(userID string) (string, error) {
func (s *SqlStore) GetAccountIDBySetupKey(ctx context.Context, setupKey string) (string, error) {
var accountID string
- result := s.db.Model(&types.SetupKey{}).Select("account_id").Where(keyQueryCondition, setupKey).First(&accountID)
+ result := s.db.Model(&types.SetupKey{}).Select("account_id").Where(GetKeyQueryCondition(s), setupKey).First(&accountID)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return "", status.NewSetupKeyNotFoundError(setupKey)
@@ -851,7 +861,8 @@ func (s *SqlStore) GetAccountNetwork(ctx context.Context, lockStrength LockingSt
func (s *SqlStore) GetPeerByPeerPubKey(ctx context.Context, lockStrength LockingStrength, peerKey string) (*nbpeer.Peer, error) {
var peer nbpeer.Peer
- result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).First(&peer, keyQueryCondition, peerKey)
+ result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).First(&peer, GetKeyQueryCondition(s), peerKey)
+
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, status.Errorf(status.NotFound, "peer not found")
@@ -883,9 +894,13 @@ func (s *SqlStore) SaveUserLastLogin(ctx context.Context, accountID, userID stri
}
return status.NewGetUserFromStoreError()
}
- user.LastLogin = lastLogin
- return s.db.Save(&user).Error
+ if !lastLogin.IsZero() {
+ user.LastLogin = &lastLogin
+ return s.db.Save(&user).Error
+ }
+
+ return nil
}
func (s *SqlStore) GetPostureCheckByChecksDefinition(accountID string, checks *posture.ChecksDefinition) (*posture.Checks, error) {
@@ -944,6 +959,16 @@ func NewPostgresqlStore(ctx context.Context, dsn string, metrics telemetry.AppMe
return NewSqlStore(ctx, db, PostgresStoreEngine, metrics)
}
+// NewMysqlStore creates a new MySQL store.
+func NewMysqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) {
+ db, err := gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), getGormConfig())
+ if err != nil {
+ return nil, err
+ }
+
+ return NewSqlStore(ctx, db, MysqlStoreEngine, metrics)
+}
+
func getGormConfig() *gorm.Config {
return &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
@@ -961,6 +986,15 @@ func newPostgresStore(ctx context.Context, metrics telemetry.AppMetrics) (Store,
return NewPostgresqlStore(ctx, dsn, metrics)
}
+// newMysqlStore initializes a new MySQL store.
+func newMysqlStore(ctx context.Context, metrics telemetry.AppMetrics) (Store, error) {
+ dsn, ok := os.LookupEnv(mysqlDsnEnv)
+ if !ok {
+ return nil, fmt.Errorf("%s is not set", mysqlDsnEnv)
+ }
+ return NewMysqlStore(ctx, dsn, metrics)
+}
+
// NewSqliteStoreFromFileStore restores a store from FileStore and stores SQLite DB in the file located in datadir.
func NewSqliteStoreFromFileStore(ctx context.Context, fileStore *FileStore, dataDir string, metrics telemetry.AppMetrics) (*SqlStore, error) {
store, err := NewSqliteStore(ctx, dataDir, metrics)
@@ -1005,10 +1039,33 @@ func NewPostgresqlStoreFromSqlStore(ctx context.Context, sqliteStore *SqlStore,
return store, nil
}
+// NewMysqlStoreFromSqlStore restores a store from SqlStore and stores MySQL DB.
+func NewMysqlStoreFromSqlStore(ctx context.Context, sqliteStore *SqlStore, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) {
+ store, err := NewMysqlStore(ctx, dsn, metrics)
+ if err != nil {
+ return nil, err
+ }
+
+ err = store.SaveInstallationID(ctx, sqliteStore.GetInstallationID())
+ if err != nil {
+ return nil, err
+ }
+
+ for _, account := range sqliteStore.GetAllAccounts(ctx) {
+ err := store.SaveAccount(ctx, account)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return store, nil
+}
+
func (s *SqlStore) GetSetupKeyBySecret(ctx context.Context, lockStrength LockingStrength, key string) (*types.SetupKey, error) {
var setupKey types.SetupKey
result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
- First(&setupKey, keyQueryCondition, key)
+ First(&setupKey, GetKeyQueryCondition(s), key)
+
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, status.NewSetupKeyNotFoundError(key)
@@ -1300,9 +1357,13 @@ func (s *SqlStore) GetGroupByName(ctx context.Context, lockStrength LockingStren
// TODO: This fix is accepted for now, but if we need to handle this more frequently
// we may need to reconsider changing the types.
query := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Preload(clause.Associations)
- if s.storeEngine == PostgresStoreEngine {
+
+ switch s.storeEngine {
+ case PostgresStoreEngine:
query = query.Order("json_array_length(peers::json) DESC")
- } else {
+ case MysqlStoreEngine:
+ query = query.Order("JSON_LENGTH(JSON_EXTRACT(peers, \"$\")) DESC")
+ default:
query = query.Order("json_array_length(peers) DESC")
}
diff --git a/management/server/store/store.go b/management/server/store/store.go
index d9dc6b8f7..e1a6937e7 100644
--- a/management/server/store/store.go
+++ b/management/server/store/store.go
@@ -18,6 +18,7 @@ import (
"gorm.io/gorm"
"github.com/netbirdio/netbird/dns"
+ "github.com/netbirdio/netbird/management/server/testutil"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/management/server/telemetry"
@@ -29,7 +30,6 @@ import (
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/posture"
- "github.com/netbirdio/netbird/management/server/testutil"
"github.com/netbirdio/netbird/route"
)
@@ -171,8 +171,10 @@ const (
FileStoreEngine Engine = "jsonfile"
SqliteStoreEngine Engine = "sqlite"
PostgresStoreEngine Engine = "postgres"
+ MysqlStoreEngine Engine = "mysql"
postgresDsnEnv = "NETBIRD_STORE_ENGINE_POSTGRES_DSN"
+ mysqlDsnEnv = "NETBIRD_STORE_ENGINE_MYSQL_DSN"
)
func getStoreEngineFromEnv() Engine {
@@ -183,7 +185,7 @@ func getStoreEngineFromEnv() Engine {
}
value := Engine(strings.ToLower(kind))
- if value == SqliteStoreEngine || value == PostgresStoreEngine {
+ if value == SqliteStoreEngine || value == PostgresStoreEngine || value == MysqlStoreEngine {
return value
}
@@ -234,6 +236,9 @@ func NewStore(ctx context.Context, kind Engine, dataDir string, metrics telemetr
case PostgresStoreEngine:
log.WithContext(ctx).Info("using Postgres store engine")
return newPostgresStore(ctx, metrics)
+ case MysqlStoreEngine:
+ log.WithContext(ctx).Info("using MySQL store engine")
+ return newMysqlStore(ctx, metrics)
default:
return nil, fmt.Errorf("unsupported kind of store: %s", kind)
}
@@ -317,12 +322,13 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) (
if err != nil {
return nil, nil, fmt.Errorf("failed to create test store: %v", err)
}
- cleanUp := func() {
- store.Close(ctx)
- }
+ return getSqlStoreEngine(ctx, store, kind)
+}
+
+func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store, func(), error) {
if kind == PostgresStoreEngine {
- cleanUp, err = testutil.CreatePGDB()
+ cleanUp, err := testutil.CreatePostgresTestContainer()
if err != nil {
return nil, nil, err
}
@@ -336,9 +342,34 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) (
if err != nil {
return nil, nil, err
}
+
+ return store, cleanUp, nil
}
- return store, cleanUp, nil
+ if kind == MysqlStoreEngine {
+ cleanUp, err := testutil.CreateMysqlTestContainer()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ dsn, ok := os.LookupEnv(mysqlDsnEnv)
+ if !ok {
+ return nil, nil, fmt.Errorf("%s is not set", mysqlDsnEnv)
+ }
+
+ store, err = NewMysqlStoreFromSqlStore(ctx, store, dsn, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return store, cleanUp, nil
+ }
+
+ closeConnection := func() {
+ store.Close(ctx)
+ }
+
+ return store, closeConnection, nil
}
func loadSQL(db *gorm.DB, filepath string) error {
diff --git a/management/server/testdata/extended-store.sql b/management/server/testdata/extended-store.sql
index 455111439..2859e82c8 100644
--- a/management/server/testdata/extended-store.sql
+++ b/management/server/testdata/extended-store.sql
@@ -1,7 +1,7 @@
CREATE TABLE `accounts` (`id` text,`created_by` text,`created_at` datetime,`domain` text,`domain_category` text,`is_domain_primary_account` numeric,`network_identifier` text,`network_net` text,`network_dns` text,`network_serial` integer,`dns_settings_disabled_management_groups` text,`settings_peer_login_expiration_enabled` numeric,`settings_peer_login_expiration` integer,`settings_regular_users_view_blocked` numeric,`settings_groups_propagation_enabled` numeric,`settings_jwt_groups_enabled` numeric,`settings_jwt_groups_claim_name` text,`settings_jwt_allow_groups` text,`settings_extra_peer_approval_enabled` numeric,`settings_extra_integrated_validator_groups` text,PRIMARY KEY (`id`));
-CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
+CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
-CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
+CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime DEFAULT NULL,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
CREATE TABLE `personal_access_tokens` (`id` text,`user_id` text,`name` text,`hashed_token` text,`expiration_date` datetime,`created_by` text,`created_at` datetime,`last_used` datetime,PRIMARY KEY (`id`),CONSTRAINT `fk_users_pa_ts_g` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`));
CREATE TABLE `groups` (`id` text,`account_id` text,`name` text,`issued` text,`peers` text,`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_groups_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
CREATE TABLE `policies` (`id` text,`account_id` text,`name` text,`description` text,`enabled` numeric,`source_posture_checks` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_policies` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
@@ -26,10 +26,10 @@ CREATE INDEX `idx_name_server_groups_account_id` ON `name_server_groups`(`accoun
CREATE INDEX `idx_posture_checks_account_id` ON `posture_checks`(`account_id`);
INSERT INTO accounts VALUES('bf1c8084-ba50-4ce7-9439-34653001fc3b','','2024-10-02 16:01:38.210014+02:00','test.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL);
-INSERT INTO setup_keys VALUES('A2C8E62B-38F5-4553-B31E-DD66C696CEBB','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,'0001-01-01 00:00:00+00:00','["cfefqs706sqkneg59g2g"]',0,0);
-INSERT INTO setup_keys VALUES('A2C8E62B-38F5-4553-B31E-DD66C696CEBC','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBC','Faulty key with non existing group','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,'0001-01-01 00:00:00+00:00','["abcd"]',0,0);
-INSERT INTO users VALUES('edafee4e-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','admin',0,0,'','["cfefqs706sqkneg59g3g"]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.210678+02:00','api',0,'');
-INSERT INTO users VALUES('f4f6d672-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','user',0,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:01:38.210678+02:00','api',0,'');
+INSERT INTO setup_keys VALUES('A2C8E62B-38F5-4553-B31E-DD66C696CEBB','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["cfefqs706sqkneg59g2g"]',0,0);
+INSERT INTO setup_keys VALUES('A2C8E62B-38F5-4553-B31E-DD66C696CEBC','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBC','Faulty key with non existing group','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["abcd"]',0,0);
+INSERT INTO users VALUES('edafee4e-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','admin',0,0,'','["cfefqs706sqkneg59g3g"]',0,NULL,'2024-10-02 16:01:38.210678+02:00','api',0,'');
+INSERT INTO users VALUES('f4f6d672-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','user',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.210678+02:00','api',0,'');
INSERT INTO personal_access_tokens VALUES('9dj38s35-63fb-11ec-90d6-0242ac120003','f4f6d672-63fb-11ec-90d6-0242ac120003','','SoMeHaShEdToKeN','2023-02-27 00:00:00+00:00','user','2023-01-01 00:00:00+00:00','2023-02-01 00:00:00+00:00');
INSERT INTO "groups" VALUES('cfefqs706sqkneg59g4g','bf1c8084-ba50-4ce7-9439-34653001fc3b','All','api','[]',0,'');
INSERT INTO "groups" VALUES('cfefqs706sqkneg59g3g','bf1c8084-ba50-4ce7-9439-34653001fc3b','AwesomeGroup1','api','[]',0,'');
diff --git a/management/server/testdata/store.sql b/management/server/testdata/store.sql
index 7f0c7b5a4..17f029713 100644
--- a/management/server/testdata/store.sql
+++ b/management/server/testdata/store.sql
@@ -1,8 +1,8 @@
CREATE TABLE `accounts` (`id` text,`created_by` text,`created_at` datetime,`domain` text,`domain_category` text,`is_domain_primary_account` numeric,`network_identifier` text,`network_net` text,`network_dns` text,`network_serial` integer,`dns_settings_disabled_management_groups` text,`settings_peer_login_expiration_enabled` numeric,`settings_peer_login_expiration` integer,`settings_regular_users_view_blocked` numeric,`settings_groups_propagation_enabled` numeric,`settings_jwt_groups_enabled` numeric,`settings_jwt_groups_claim_name` text,`settings_jwt_allow_groups` text,`settings_extra_peer_approval_enabled` numeric,`settings_extra_integrated_validator_groups` text,PRIMARY KEY (`id`));
-CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
+CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
-CREATE TABLE `personal_access_tokens` (`id` text,`user_id` text,`name` text,`hashed_token` text,`expiration_date` datetime,`created_by` text,`created_at` datetime,`last_used` datetime,PRIMARY KEY (`id`),CONSTRAINT `fk_users_pa_ts_g` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`));
+CREATE TABLE `personal_access_tokens` (`id` text,`user_id` text,`name` text,`hashed_token` text,`expiration_date` datetime,`created_by` text,`created_at` datetime,`last_used` datetime DEFAULT NULL,PRIMARY KEY (`id`),CONSTRAINT `fk_users_pa_ts_g` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`));
CREATE TABLE `groups` (`id` text,`account_id` text,`name` text,`issued` text,`peers` text,`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_groups_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
CREATE TABLE `policies` (`id` text,`account_id` text,`name` text,`description` text,`enabled` numeric,`source_posture_checks` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_policies` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
CREATE TABLE `policy_rules` (`id` text,`policy_id` text,`name` text,`description` text,`enabled` numeric,`action` text,`destinations` text,`sources` text,`bidirectional` numeric,`protocol` text,`ports` text,`port_ranges` text,PRIMARY KEY (`id`),CONSTRAINT `fk_policies_rules` FOREIGN KEY (`policy_id`) REFERENCES `policies`(`id`) ON DELETE CASCADE);
@@ -38,9 +38,9 @@ CREATE INDEX `idx_networks_account_id` ON `networks`(`account_id`);
INSERT INTO accounts VALUES('bf1c8084-ba50-4ce7-9439-34653001fc3b','','2024-10-02 16:03:06.778746+02:00','test.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL);
INSERT INTO "groups" VALUES('cs1tnh0hhcjnqoiuebeg','bf1c8084-ba50-4ce7-9439-34653001fc3b','All','api','[]',0,'');
-INSERT INTO setup_keys VALUES('','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,'0001-01-01 00:00:00+00:00','["cs1tnh0hhcjnqoiuebeg"]',0,0);
-INSERT INTO users VALUES('edafee4e-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','admin',0,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:03:06.779156+02:00','api',0,'');
-INSERT INTO users VALUES('f4f6d672-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','user',0,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:03:06.779156+02:00','api',0,'');
+INSERT INTO setup_keys VALUES('','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["cs1tnh0hhcjnqoiuebeg"]',0,0);
+INSERT INTO users VALUES('edafee4e-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','admin',0,0,'','[]',0,NULL,'2024-10-02 16:03:06.779156+02:00','api',0,'');
+INSERT INTO users VALUES('f4f6d672-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','user',0,0,'','[]',0,NULL,'2024-10-02 16:03:06.779156+02:00','api',0,'');
INSERT INTO personal_access_tokens VALUES('9dj38s35-63fb-11ec-90d6-0242ac120003','f4f6d672-63fb-11ec-90d6-0242ac120003','','SoMeHaShEdToKeN','2023-02-27 00:00:00+00:00','user','2023-01-01 00:00:00+00:00','2023-02-01 00:00:00+00:00');
INSERT INTO installations VALUES(1,'');
INSERT INTO policies VALUES('cs1tnh0hhcjnqoiuebf0','bf1c8084-ba50-4ce7-9439-34653001fc3b','Default','This is a default rule that allows connections between all the resources',1,'[]');
diff --git a/management/server/testdata/store_policy_migrate.sql b/management/server/testdata/store_policy_migrate.sql
index a9360e9d6..9c961e389 100644
--- a/management/server/testdata/store_policy_migrate.sql
+++ b/management/server/testdata/store_policy_migrate.sql
@@ -1,7 +1,7 @@
CREATE TABLE `accounts` (`id` text,`created_by` text,`created_at` datetime,`domain` text,`domain_category` text,`is_domain_primary_account` numeric,`network_identifier` text,`network_net` text,`network_dns` text,`network_serial` integer,`dns_settings_disabled_management_groups` text,`settings_peer_login_expiration_enabled` numeric,`settings_peer_login_expiration` integer,`settings_regular_users_view_blocked` numeric,`settings_groups_propagation_enabled` numeric,`settings_jwt_groups_enabled` numeric,`settings_jwt_groups_claim_name` text,`settings_jwt_allow_groups` text,`settings_extra_peer_approval_enabled` numeric,`settings_extra_integrated_validator_groups` text,PRIMARY KEY (`id`));
-CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
+CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
-CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
+CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime DEFAULT NULL,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
CREATE TABLE `personal_access_tokens` (`id` text,`user_id` text,`name` text,`hashed_token` text,`expiration_date` datetime,`created_by` text,`created_at` datetime,`last_used` datetime,PRIMARY KEY (`id`),CONSTRAINT `fk_users_pa_ts_g` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`));
CREATE TABLE `groups` (`id` text,`account_id` text,`name` text,`issued` text,`peers` text,`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_groups_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
CREATE TABLE `policies` (`id` text,`account_id` text,`name` text,`description` text,`enabled` numeric,`source_posture_checks` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_policies` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
@@ -26,10 +26,10 @@ CREATE INDEX `idx_name_server_groups_account_id` ON `name_server_groups`(`accoun
CREATE INDEX `idx_posture_checks_account_id` ON `posture_checks`(`account_id`);
INSERT INTO accounts VALUES('bf1c8084-ba50-4ce7-9439-34653001fc3b','','2024-10-02 16:04:23.538411+02:00','test.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL);
-INSERT INTO setup_keys VALUES('','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,'0001-01-01 00:00:00+00:00','[]',0,0);
+INSERT INTO setup_keys VALUES('','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'[]',0,0);
INSERT INTO peers VALUES('cfefqs706sqkneg59g4g','bf1c8084-ba50-4ce7-9439-34653001fc3b','MI5mHfJhbggPfD3FqEIsXm8X5bSWeUI2LhO9MpEEtWA=','','"100.103.179.238"','Ubuntu-2204-jammy-amd64-base','linux','Linux','22.04','x86_64','Ubuntu','','development','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'crocodile','crocodile','2023-02-13 12:37:12.635454796+00:00',1,0,0,'edafee4e-63fb-11ec-90d6-0242ac120003','AAAAC3NzaC1lZDI1NTE5AAAAIJN1NM4bpB9K',0,0,'2024-10-02 14:04:23.523293+00:00','2024-10-02 16:04:23.538926+02:00',0,'""','','',0);
INSERT INTO peers VALUES('cfeg6sf06sqkneg59g50','bf1c8084-ba50-4ce7-9439-34653001fc3b','zMAOKUeIYIuun4n0xPR1b3IdYZPmsyjYmB2jWCuloC4=','','"100.103.26.180"','borg','linux','Linux','22.04','x86_64','Ubuntu','','development','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'dingo','dingo','2023-02-21 09:37:42.565899199+00:00',0,0,0,'f4f6d672-63fb-11ec-90d6-0242ac120003','AAAAC3NzaC1lZDI1NTE5AAAAILHW',1,0,'2024-10-02 14:04:23.523293+00:00','2024-10-02 16:04:23.538926+02:00',0,'""','','',0);
-INSERT INTO users VALUES('edafee4e-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','admin',0,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:04:23.539152+02:00','api',0,'');
-INSERT INTO users VALUES('f4f6d672-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','user',0,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 16:04:23.539152+02:00','api',0,'');
+INSERT INTO users VALUES('edafee4e-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','admin',0,0,'','[]',0,NULL,'2024-10-02 16:04:23.539152+02:00','api',0,'');
+INSERT INTO users VALUES('f4f6d672-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','user',0,0,'','[]',0,NULL,'2024-10-02 16:04:23.539152+02:00','api',0,'');
INSERT INTO "groups" VALUES('cfefqs706sqkneg59g3g','bf1c8084-ba50-4ce7-9439-34653001fc3b','All','api','["cfefqs706sqkneg59g4g","cfeg6sf06sqkneg59g50"]',0,'');
INSERT INTO installations VALUES(1,'');
diff --git a/management/server/testdata/store_with_expired_peers.sql b/management/server/testdata/store_with_expired_peers.sql
index 100a6470f..518c484d7 100644
--- a/management/server/testdata/store_with_expired_peers.sql
+++ b/management/server/testdata/store_with_expired_peers.sql
@@ -1,7 +1,7 @@
CREATE TABLE `accounts` (`id` text,`created_by` text,`created_at` datetime,`domain` text,`domain_category` text,`is_domain_primary_account` numeric,`network_identifier` text,`network_net` text,`network_dns` text,`network_serial` integer,`dns_settings_disabled_management_groups` text,`settings_peer_login_expiration_enabled` numeric,`settings_peer_login_expiration` integer,`settings_regular_users_view_blocked` numeric,`settings_groups_propagation_enabled` numeric,`settings_jwt_groups_enabled` numeric,`settings_jwt_groups_claim_name` text,`settings_jwt_allow_groups` text,`settings_extra_peer_approval_enabled` numeric,`settings_extra_integrated_validator_groups` text,PRIMARY KEY (`id`));
-CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
+CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
-CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
+CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime DEFAULT NULL,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
CREATE TABLE `personal_access_tokens` (`id` text,`user_id` text,`name` text,`hashed_token` text,`expiration_date` datetime,`created_by` text,`created_at` datetime,`last_used` datetime,PRIMARY KEY (`id`),CONSTRAINT `fk_users_pa_ts_g` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`));
CREATE TABLE `groups` (`id` text,`account_id` text,`name` text,`issued` text,`peers` text,`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_groups_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
CREATE TABLE `policies` (`id` text,`account_id` text,`name` text,`description` text,`enabled` numeric,`source_posture_checks` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_policies` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
@@ -26,10 +26,10 @@ CREATE INDEX `idx_name_server_groups_account_id` ON `name_server_groups`(`accoun
CREATE INDEX `idx_posture_checks_account_id` ON `posture_checks`(`account_id`);
INSERT INTO accounts VALUES('bf1c8084-ba50-4ce7-9439-34653001fc3b','','2024-10-02 17:00:32.527528+02:00','test.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',1,3600000000000,0,0,0,'',NULL,NULL,NULL);
-INSERT INTO setup_keys VALUES('','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,'0001-01-01 00:00:00+00:00','[]',0,0);
+INSERT INTO setup_keys VALUES('','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'[]',0,0);
INSERT INTO peers VALUES('cfvprsrlo1hqoo49ohog','bf1c8084-ba50-4ce7-9439-34653001fc3b','5rvhvriKJZ3S9oxYToVj5TzDM9u9y8cxg7htIMWlYAg=','72546A29-6BC8-4311-BCFC-9CDBF33F1A48','"100.64.114.31"','f2a34f6a4731','linux','Linux','11','unknown','Debian GNU/Linux','','0.12.0','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'f2a34f6a4731','f2a34f6a4731','2023-03-02 09:21:02.189035775+01:00',0,0,0,'','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILzUUSYG/LGnV8zarb2SGN+tib/PZ+M7cL4WtTzUrTpk',0,1,'2023-03-01 19:48:19.817799698+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0);
INSERT INTO peers VALUES('cg05lnblo1hkg2j514p0','bf1c8084-ba50-4ce7-9439-34653001fc3b','RlSy2vzoG2HyMBTUImXOiVhCBiiBa5qD5xzMxkiFDW4=','','"100.64.39.54"','expiredhost','linux','Linux','22.04','x86_64','Ubuntu','','development','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'expiredhost','expiredhost','2023-03-02 09:19:57.276717255+01:00',0,1,0,'edafee4e-63fb-11ec-90d6-0242ac120003','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMbK5ZXJsGOOWoBT4OmkPtgdPZe2Q7bDuS/zjn2CZxhK',0,1,'2023-03-02 09:14:21.791679181+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0);
INSERT INTO peers VALUES('cg3161rlo1hs9cq94gdg','bf1c8084-ba50-4ce7-9439-34653001fc3b','mVABSKj28gv+JRsf7e0NEGKgSOGTfU/nPB2cpuG56HU=','','"100.64.117.96"','testhost','linux','Linux','22.04','x86_64','Ubuntu','','development','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'testhost','testhost','2023-03-06 18:21:27.252010027+01:00',0,0,0,'edafee4e-63fb-11ec-90d6-0242ac120003','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINWvvUkFFcrj48CWTkNUb/do/n52i1L5dH4DhGu+4ZuM',0,0,'2023-03-07 09:02:47.442857106+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0);
-INSERT INTO users VALUES('f4f6d672-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','user',0,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 17:00:32.528196+02:00','api',0,'');
-INSERT INTO users VALUES('edafee4e-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','admin',0,0,'','[]',0,'0001-01-01 00:00:00+00:00','2024-10-02 17:00:32.528196+02:00','api',0,'');
+INSERT INTO users VALUES('f4f6d672-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','user',0,0,'','[]',0,NULL,'2024-10-02 17:00:32.528196+02:00','api',0,'');
+INSERT INTO users VALUES('edafee4e-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','admin',0,0,'','[]',0,NULL,'2024-10-02 17:00:32.528196+02:00','api',0,'');
INSERT INTO installations VALUES(1,'');
diff --git a/management/server/testutil/store.go b/management/server/testutil/store.go
index 156a762fb..16438cab8 100644
--- a/management/server/testutil/store.go
+++ b/management/server/testutil/store.go
@@ -10,36 +10,75 @@ import (
log "github.com/sirupsen/logrus"
"github.com/testcontainers/testcontainers-go"
+ "github.com/testcontainers/testcontainers-go/modules/mysql"
"github.com/testcontainers/testcontainers-go/modules/postgres"
"github.com/testcontainers/testcontainers-go/wait"
)
-func CreatePGDB() (func(), error) {
+// CreateMysqlTestContainer creates a new MySQL container for testing.
+func CreateMysqlTestContainer() (func(), error) {
ctx := context.Background()
- c, err := postgres.RunContainer(ctx,
- testcontainers.WithImage("postgres:alpine"),
- postgres.WithDatabase("test"),
- postgres.WithUsername("postgres"),
- postgres.WithPassword("postgres"),
+
+ myContainer, err := mysql.RunContainer(ctx,
+ testcontainers.WithImage("mlsmaycon/warmed-mysql:8"),
+ mysql.WithDatabase("testing"),
+ mysql.WithUsername("testing"),
+ mysql.WithPassword("testing"),
testcontainers.WithWaitStrategy(
- wait.ForLog("database system is ready to accept connections").
- WithOccurrence(2).WithStartupTimeout(15*time.Second)),
+ wait.ForLog("/usr/sbin/mysqld: ready for connections").
+ WithOccurrence(1).WithStartupTimeout(15*time.Second).WithPollInterval(100*time.Millisecond),
+ ),
)
if err != nil {
return nil, err
}
cleanup := func() {
- timeout := 10 * time.Second
- err = c.Stop(ctx, &timeout)
- if err != nil {
- log.WithContext(ctx).Warnf("failed to stop container: %s", err)
+ timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second)
+ defer cancelFunc()
+ if err = myContainer.Terminate(timeoutCtx); err != nil {
+ log.WithContext(ctx).Warnf("failed to stop mysql container %s: %s", myContainer.GetContainerID(), err)
}
}
- talksConn, err := c.ConnectionString(ctx)
+ talksConn, err := myContainer.ConnectionString(ctx)
if err != nil {
- return cleanup, err
+ return nil, err
}
+
+ return cleanup, os.Setenv("NETBIRD_STORE_ENGINE_MYSQL_DSN", talksConn)
+}
+
+// CreatePostgresTestContainer creates a new PostgreSQL container for testing.
+func CreatePostgresTestContainer() (func(), error) {
+ ctx := context.Background()
+
+ pgContainer, err := postgres.RunContainer(ctx,
+ testcontainers.WithImage("postgres:16-alpine"),
+ postgres.WithDatabase("netbird"),
+ postgres.WithUsername("root"),
+ postgres.WithPassword("netbird"),
+ testcontainers.WithWaitStrategy(
+ wait.ForLog("database system is ready to accept connections").
+ WithOccurrence(2).WithStartupTimeout(15*time.Second),
+ ),
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ cleanup := func() {
+ timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second)
+ defer cancelFunc()
+ if err = pgContainer.Terminate(timeoutCtx); err != nil {
+ log.WithContext(ctx).Warnf("failed to stop postgres container %s: %s", pgContainer.GetContainerID(), err)
+ }
+ }
+
+ talksConn, err := pgContainer.ConnectionString(ctx)
+ if err != nil {
+ return nil, err
+ }
+
return cleanup, os.Setenv("NETBIRD_STORE_ENGINE_POSTGRES_DSN", talksConn)
}
diff --git a/management/server/testutil/store_ios.go b/management/server/testutil/store_ios.go
index af2cf7a3f..edde62f1e 100644
--- a/management/server/testutil/store_ios.go
+++ b/management/server/testutil/store_ios.go
@@ -3,4 +3,14 @@
package testutil
-func CreatePGDB() (func(), error) { return func() {}, nil }
+func CreatePostgresTestContainer() (func(), error) {
+ return func() {
+ // Empty function for Postgres
+ }, nil
+}
+
+func CreateMysqlTestContainer() (func(), error) {
+ return func() {
+ // Empty function for MySQL
+ }, nil
+}
diff --git a/management/server/types/personal_access_token.go b/management/server/types/personal_access_token.go
index 1bf225856..ff157fcc6 100644
--- a/management/server/types/personal_access_token.go
+++ b/management/server/types/personal_access_token.go
@@ -8,6 +8,7 @@ import (
"time"
b "github.com/hashicorp/go-secure-stdlib/base62"
+ "github.com/netbirdio/netbird/management/server/util"
"github.com/rs/xid"
"github.com/netbirdio/netbird/base62"
@@ -31,11 +32,11 @@ type PersonalAccessToken struct {
UserID string `gorm:"index"`
Name string
HashedToken string
- ExpirationDate time.Time
+ ExpirationDate *time.Time
// scope could be added in future
CreatedBy string
CreatedAt time.Time
- LastUsed time.Time
+ LastUsed *time.Time
}
func (t *PersonalAccessToken) Copy() *PersonalAccessToken {
@@ -50,6 +51,22 @@ func (t *PersonalAccessToken) Copy() *PersonalAccessToken {
}
}
+// GetExpirationDate returns the expiration time of the token.
+func (t *PersonalAccessToken) GetExpirationDate() time.Time {
+ if t.ExpirationDate != nil {
+ return *t.ExpirationDate
+ }
+ return time.Time{}
+}
+
+// GetLastUsed returns the last time the token was used.
+func (t *PersonalAccessToken) GetLastUsed() time.Time {
+ if t.LastUsed != nil {
+ return *t.LastUsed
+ }
+ return time.Time{}
+}
+
// PersonalAccessTokenGenerated holds the new PersonalAccessToken and the plain text version of it
type PersonalAccessTokenGenerated struct {
PlainToken string
@@ -69,10 +86,9 @@ func CreateNewPAT(name string, expirationInDays int, createdBy string) (*Persona
ID: xid.New().String(),
Name: name,
HashedToken: hashedToken,
- ExpirationDate: currentTime.AddDate(0, 0, expirationInDays),
+ ExpirationDate: util.ToPtr(currentTime.AddDate(0, 0, expirationInDays)),
CreatedBy: createdBy,
CreatedAt: currentTime,
- LastUsed: time.Time{},
},
PlainToken: plainToken,
}, nil
diff --git a/management/server/types/setupkey.go b/management/server/types/setupkey.go
index a5cf346a0..2cd835289 100644
--- a/management/server/types/setupkey.go
+++ b/management/server/types/setupkey.go
@@ -10,6 +10,7 @@ import (
"unicode/utf8"
"github.com/google/uuid"
+ "github.com/netbirdio/netbird/management/server/util"
)
const (
@@ -38,14 +39,14 @@ type SetupKey struct {
Name string
Type SetupKeyType
CreatedAt time.Time
- ExpiresAt time.Time
+ ExpiresAt *time.Time
UpdatedAt time.Time `gorm:"autoUpdateTime:false"`
// Revoked indicates whether the key was revoked or not (we don't remove them for tracking purposes)
Revoked bool
// UsedTimes indicates how many times the key was used
UsedTimes int
// LastUsed last time the key was used for peer registration
- LastUsed time.Time
+ LastUsed *time.Time
// AutoGroups is a list of Group IDs that are auto assigned to a Peer when it uses this key to register
AutoGroups []string `gorm:"serializer:json"`
// UsageLimit indicates the number of times this key can be used to enroll a machine.
@@ -86,6 +87,22 @@ func (key *SetupKey) EventMeta() map[string]any {
return map[string]any{"name": key.Name, "type": key.Type, "key": key.KeySecret}
}
+// GetLastUsed returns the last used time of the setup key.
+func (key *SetupKey) GetLastUsed() time.Time {
+ if key.LastUsed != nil {
+ return *key.LastUsed
+ }
+ return time.Time{}
+}
+
+// GetExpiresAt returns the expiration time of the setup key.
+func (key *SetupKey) GetExpiresAt() time.Time {
+ if key.ExpiresAt != nil {
+ return *key.ExpiresAt
+ }
+ return time.Time{}
+}
+
// HiddenKey returns the Key value hidden with "*" and a 5 character prefix.
// E.g., "831F6*******************************"
func HiddenKey(key string, length int) string {
@@ -100,7 +117,7 @@ func HiddenKey(key string, length int) string {
func (key *SetupKey) IncrementUsage() *SetupKey {
c := key.Copy()
c.UsedTimes++
- c.LastUsed = time.Now().UTC()
+ c.LastUsed = util.ToPtr(time.Now().UTC())
return c
}
@@ -116,10 +133,10 @@ func (key *SetupKey) IsRevoked() bool {
// IsExpired if key was expired
func (key *SetupKey) IsExpired() bool {
- if key.ExpiresAt.IsZero() {
+ if key.GetExpiresAt().IsZero() {
return false
}
- return time.Now().After(key.ExpiresAt)
+ return time.Now().After(key.GetExpiresAt())
}
// IsOverUsed if the key was used too many times. SetupKey.UsageLimit == 0 indicates the unlimited usage.
@@ -140,9 +157,9 @@ func GenerateSetupKey(name string, t SetupKeyType, validFor time.Duration, autoG
limit = 1
}
- expiresAt := time.Time{}
+ var expiresAt *time.Time
if validFor != 0 {
- expiresAt = time.Now().UTC().Add(validFor)
+ expiresAt = util.ToPtr(time.Now().UTC().Add(validFor))
}
hashedKey := sha256.Sum256([]byte(key))
diff --git a/management/server/types/user.go b/management/server/types/user.go
index 5f1b71792..348fbfb22 100644
--- a/management/server/types/user.go
+++ b/management/server/types/user.go
@@ -84,7 +84,7 @@ type User struct {
// Blocked indicates whether the user is blocked. Blocked users can't use the system.
Blocked bool
// LastLogin is the last time the user logged in to IdP
- LastLogin time.Time
+ LastLogin *time.Time
// CreatedAt records the time the user was created
CreatedAt time.Time
@@ -99,8 +99,16 @@ func (u *User) IsBlocked() bool {
return u.Blocked
}
-func (u *User) LastDashboardLoginChanged(LastLogin time.Time) bool {
- return LastLogin.After(u.LastLogin) && !u.LastLogin.IsZero()
+func (u *User) LastDashboardLoginChanged(lastLogin time.Time) bool {
+ return lastLogin.After(u.GetLastLogin()) && !u.GetLastLogin().IsZero()
+}
+
+// GetLastLogin returns the last login time of the user.
+func (u *User) GetLastLogin() time.Time {
+ if u.LastLogin != nil {
+ return *u.LastLogin
+ }
+ return time.Time{}
}
// HasAdminPower returns true if the user has admin or owner roles, false otherwise
@@ -143,7 +151,7 @@ func (u *User) ToUserInfo(userData *idp.UserData, settings *Settings) (*UserInfo
Status: string(UserStatusActive),
IsServiceUser: u.IsServiceUser,
IsBlocked: u.Blocked,
- LastLogin: u.LastLogin,
+ LastLogin: u.GetLastLogin(),
Issued: u.Issued,
Permissions: UserPermissions{
DashboardView: dashboardViewPermissions,
@@ -168,7 +176,7 @@ func (u *User) ToUserInfo(userData *idp.UserData, settings *Settings) (*UserInfo
Status: string(userStatus),
IsServiceUser: u.IsServiceUser,
IsBlocked: u.Blocked,
- LastLogin: u.LastLogin,
+ LastLogin: u.GetLastLogin(),
Issued: u.Issued,
Permissions: UserPermissions{
DashboardView: dashboardViewPermissions,
diff --git a/management/server/user.go b/management/server/user.go
index 457721917..fcf3d34ff 100644
--- a/management/server/user.go
+++ b/management/server/user.go
@@ -880,7 +880,7 @@ func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accoun
continue
}
if !user.IsServiceUser {
- users[user.Id] = userLoggedInOnce(!user.LastLogin.IsZero())
+ users[user.Id] = userLoggedInOnce(!user.GetLastLogin().IsZero())
}
}
queriedUsers, err = am.lookupCache(ctx, users, accountID)
diff --git a/management/server/user_test.go b/management/server/user_test.go
index 75d88f9c8..a028d164b 100644
--- a/management/server/user_test.go
+++ b/management/server/user_test.go
@@ -10,6 +10,7 @@ import (
"github.com/eko/gocache/v3/cache"
cacheStore "github.com/eko/gocache/v3/store"
"github.com/google/go-cmp/cmp"
+ "github.com/netbirdio/netbird/management/server/util"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/store"
@@ -321,14 +322,14 @@ func TestUser_Copy(t *testing.T) {
ID: "pat1",
Name: "First PAT",
HashedToken: "SoMeHaShEdToKeN",
- ExpirationDate: time.Now().AddDate(0, 0, 7),
+ ExpirationDate: util.ToPtr(time.Now().AddDate(0, 0, 7)),
CreatedBy: "userId",
CreatedAt: time.Now(),
- LastUsed: time.Now(),
+ LastUsed: util.ToPtr(time.Now()),
},
},
Blocked: false,
- LastLogin: time.Now().UTC(),
+ LastLogin: util.ToPtr(time.Now().UTC()),
CreatedAt: time.Now().UTC(),
Issued: "test",
IntegrationReference: integration_reference.IntegrationReference{
diff --git a/management/server/util/util.go b/management/server/util/util.go
index ff738781f..d85b55f02 100644
--- a/management/server/util/util.go
+++ b/management/server/util/util.go
@@ -14,3 +14,8 @@ func Difference(a, b []string) []string {
}
return diff
}
+
+// ToPtr returns a pointer to the given value.
+func ToPtr[T any](value T) *T {
+ return &value
+}
diff --git a/relay/server/listener/ws/listener.go b/relay/server/listener/ws/listener.go
index 1ad57d27a..5c62c0826 100644
--- a/relay/server/listener/ws/listener.go
+++ b/relay/server/listener/ws/listener.go
@@ -96,5 +96,5 @@ func remoteAddr(r *http.Request) string {
if r.Header.Get("X-Real-Ip") == "" || r.Header.Get("X-Real-Port") == "" {
return r.RemoteAddr
}
- return fmt.Sprintf("%s:%s", r.Header.Get("X-Real-Ip"), r.Header.Get("X-Real-Port"))
+ return net.JoinHostPort(r.Header.Get("X-Real-Ip"), r.Header.Get("X-Real-Port"))
}