Merge remote-tracking branch 'origin/main' into proto-ipv6-overlay

# Conflicts:
#	client/android/client.go
#	client/ssh/server/server.go
#	shared/management/proto/management.pb.go
This commit is contained in:
Viktor Liu
2026-04-07 18:35:13 +02:00
137 changed files with 16950 additions and 1827 deletions

View File

@@ -75,7 +75,7 @@ type Manager interface {
GetUsersFromAccount(ctx context.Context, accountID, userID string) (map[string]*types.UserInfo, error)
GetGroup(ctx context.Context, accountId, groupID, userID string) (*types.Group, error)
GetAllGroups(ctx context.Context, accountID, userID string) ([]*types.Group, error)
GetGroupByName(ctx context.Context, groupName, accountID string) (*types.Group, error)
GetGroupByName(ctx context.Context, groupName, accountID, userID string) (*types.Group, error)
CreateGroup(ctx context.Context, accountID, userID string, group *types.Group) error
UpdateGroup(ctx context.Context, accountID, userID string, group *types.Group) error
CreateGroups(ctx context.Context, accountID, userID string, newGroups []*types.Group) error

View File

@@ -736,18 +736,18 @@ func (mr *MockManagerMockRecorder) GetGroup(ctx, accountId, groupID, userID inte
}
// GetGroupByName mocks base method.
func (m *MockManager) GetGroupByName(ctx context.Context, groupName, accountID string) (*types.Group, error) {
func (m *MockManager) GetGroupByName(ctx context.Context, groupName, accountID, userID string) (*types.Group, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetGroupByName", ctx, groupName, accountID)
ret := m.ctrl.Call(m, "GetGroupByName", ctx, groupName, accountID, userID)
ret0, _ := ret[0].(*types.Group)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetGroupByName indicates an expected call of GetGroupByName.
func (mr *MockManagerMockRecorder) GetGroupByName(ctx, groupName, accountID interface{}) *gomock.Call {
func (mr *MockManagerMockRecorder) GetGroupByName(ctx, groupName, accountID, userID interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupByName", reflect.TypeOf((*MockManager)(nil).GetGroupByName), ctx, groupName, accountID)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupByName", reflect.TypeOf((*MockManager)(nil).GetGroupByName), ctx, groupName, accountID, userID)
}
// GetIdentityProvider mocks base method.

View File

@@ -3138,7 +3138,7 @@ func createManager(t testing.TB) (*DefaultAccountManager, *update_channel.PeersU
if err != nil {
return nil, nil, err
}
manager.SetServiceManager(reverseproxymanager.NewManager(store, manager, permissionsManager, proxyController, nil))
manager.SetServiceManager(reverseproxymanager.NewManager(store, manager, permissionsManager, proxyController, proxyManager, nil))
return manager, updateManager, nil
}

View File

@@ -0,0 +1,61 @@
package store
// This file contains migration-only methods on Store.
// They satisfy the migration.MigrationEventStore interface via duck typing.
// Delete this file when migration tooling is no longer needed.
import (
"context"
"fmt"
"gorm.io/gorm"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/management/server/idp/migration"
)
// CheckSchema verifies that all tables and columns required by the migration exist in the event database.
func (store *Store) CheckSchema(checks []migration.SchemaCheck) []migration.SchemaError {
migrator := store.db.Migrator()
var errs []migration.SchemaError
for _, check := range checks {
if !migrator.HasTable(check.Table) {
errs = append(errs, migration.SchemaError{Table: check.Table})
continue
}
for _, col := range check.Columns {
if !migrator.HasColumn(check.Table, col) {
errs = append(errs, migration.SchemaError{Table: check.Table, Column: col})
}
}
}
return errs
}
// UpdateUserID updates all references to oldUserID in events and deleted_users tables.
func (store *Store) UpdateUserID(ctx context.Context, oldUserID, newUserID string) error {
return store.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&activity.Event{}).
Where("initiator_id = ?", oldUserID).
Update("initiator_id", newUserID).Error; err != nil {
return fmt.Errorf("update events.initiator_id: %w", err)
}
if err := tx.Model(&activity.Event{}).
Where("target_id = ?", oldUserID).
Update("target_id", newUserID).Error; err != nil {
return fmt.Errorf("update events.target_id: %w", err)
}
// Raw exec: GORM can't update a PK via Model().Update()
if err := tx.Exec(
"UPDATE deleted_users SET id = ? WHERE id = ?", newUserID, oldUserID,
).Error; err != nil {
return fmt.Errorf("update deleted_users.id: %w", err)
}
return nil
})
}

View File

@@ -0,0 +1,161 @@
package store
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/util/crypt"
)
func TestUpdateUserID(t *testing.T) {
ctx := context.Background()
newStore := func(t *testing.T) *Store {
t.Helper()
key, _ := crypt.GenerateKey()
s, err := NewSqlStore(ctx, t.TempDir(), key)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { s.Close(ctx) }) //nolint
return s
}
t.Run("updates initiator_id in events", func(t *testing.T) {
store := newStore(t)
accountID := "account_1"
_, err := store.Save(ctx, &activity.Event{
Timestamp: time.Now().UTC(),
Activity: activity.PeerAddedByUser,
InitiatorID: "old-user",
TargetID: "some-peer",
AccountID: accountID,
})
assert.NoError(t, err)
err = store.UpdateUserID(ctx, "old-user", "new-user")
assert.NoError(t, err)
result, err := store.Get(ctx, accountID, 0, 10, false)
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, "new-user", result[0].InitiatorID)
})
t.Run("updates target_id in events", func(t *testing.T) {
store := newStore(t)
accountID := "account_1"
_, err := store.Save(ctx, &activity.Event{
Timestamp: time.Now().UTC(),
Activity: activity.PeerAddedByUser,
InitiatorID: "some-admin",
TargetID: "old-user",
AccountID: accountID,
})
assert.NoError(t, err)
err = store.UpdateUserID(ctx, "old-user", "new-user")
assert.NoError(t, err)
result, err := store.Get(ctx, accountID, 0, 10, false)
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, "new-user", result[0].TargetID)
})
t.Run("updates deleted_users id", func(t *testing.T) {
store := newStore(t)
accountID := "account_1"
// Save an event with email/name meta to create a deleted_users row for "old-user"
_, err := store.Save(ctx, &activity.Event{
Timestamp: time.Now().UTC(),
Activity: activity.PeerAddedByUser,
InitiatorID: "admin",
TargetID: "old-user",
AccountID: accountID,
Meta: map[string]any{
"email": "user@example.com",
"name": "Test User",
},
})
assert.NoError(t, err)
err = store.UpdateUserID(ctx, "old-user", "new-user")
assert.NoError(t, err)
// Save another event referencing new-user with email/name meta.
// This should upsert (not conflict) because the PK was already migrated.
_, err = store.Save(ctx, &activity.Event{
Timestamp: time.Now().UTC(),
Activity: activity.PeerAddedByUser,
InitiatorID: "admin",
TargetID: "new-user",
AccountID: accountID,
Meta: map[string]any{
"email": "user@example.com",
"name": "Test User",
},
})
assert.NoError(t, err)
// The deleted user info should be retrievable via Get (joined on target_id)
result, err := store.Get(ctx, accountID, 0, 10, false)
assert.NoError(t, err)
assert.Len(t, result, 2)
for _, ev := range result {
assert.Equal(t, "new-user", ev.TargetID)
}
})
t.Run("no-op when old user ID does not exist", func(t *testing.T) {
store := newStore(t)
err := store.UpdateUserID(ctx, "nonexistent-user", "new-user")
assert.NoError(t, err)
})
t.Run("only updates matching user leaves others unchanged", func(t *testing.T) {
store := newStore(t)
accountID := "account_1"
_, err := store.Save(ctx, &activity.Event{
Timestamp: time.Now().UTC(),
Activity: activity.PeerAddedByUser,
InitiatorID: "user-a",
TargetID: "peer-1",
AccountID: accountID,
})
assert.NoError(t, err)
_, err = store.Save(ctx, &activity.Event{
Timestamp: time.Now().UTC(),
Activity: activity.PeerAddedByUser,
InitiatorID: "user-b",
TargetID: "peer-2",
AccountID: accountID,
})
assert.NoError(t, err)
err = store.UpdateUserID(ctx, "user-a", "user-a-new")
assert.NoError(t, err)
result, err := store.Get(ctx, accountID, 0, 10, false)
assert.NoError(t, err)
assert.Len(t, result, 2)
for _, ev := range result {
if ev.TargetID == "peer-1" {
assert.Equal(t, "user-a-new", ev.InitiatorID)
} else {
assert.Equal(t, "user-b", ev.InitiatorID)
}
}
})
}

View File

@@ -33,15 +33,20 @@ type manager struct {
extractor *nbjwt.ClaimsExtractor
}
func NewManager(store store.Store, issuer, audience, keysLocation, userIdClaim string, allAudiences []string, idpRefreshKeys bool) Manager {
// @note if invalid/missing parameters are sent the validator will instantiate
// but it will fail when validating and parsing the token
jwtValidator := nbjwt.NewValidator(
issuer,
allAudiences,
keysLocation,
idpRefreshKeys,
)
func NewManager(store store.Store, issuer, audience, keysLocation, userIdClaim string, allAudiences []string, idpRefreshKeys bool, keyFetcher nbjwt.KeyFetcher) Manager {
var jwtValidator *nbjwt.Validator
if keyFetcher != nil {
jwtValidator = nbjwt.NewValidatorWithKeyFetcher(issuer, allAudiences, keyFetcher)
} else {
// @note if invalid/missing parameters are sent the validator will instantiate
// but it will fail when validating and parsing the token
jwtValidator = nbjwt.NewValidator(
issuer,
allAudiences,
keysLocation,
idpRefreshKeys,
)
}
claimsExtractor := nbjwt.NewClaimsExtractor(
nbjwt.WithAudience(audience),

View File

@@ -52,7 +52,7 @@ func TestAuthManager_GetAccountInfoFromPAT(t *testing.T) {
t.Fatalf("Error when saving account: %s", err)
}
manager := auth.NewManager(store, "", "", "", "", []string{}, false)
manager := auth.NewManager(store, "", "", "", "", []string{}, false, nil)
user, pat, _, _, err := manager.GetPATInfo(context.Background(), token)
if err != nil {
@@ -92,7 +92,7 @@ func TestAuthManager_MarkPATUsed(t *testing.T) {
t.Fatalf("Error when saving account: %s", err)
}
manager := auth.NewManager(store, "", "", "", "", []string{}, false)
manager := auth.NewManager(store, "", "", "", "", []string{}, false, nil)
err = manager.MarkPATUsed(context.Background(), "tokenId")
if err != nil {
@@ -142,7 +142,7 @@ func TestAuthManager_EnsureUserAccessByJWTGroups(t *testing.T) {
// these tests only assert groups are parsed from token as per account settings
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{"idp-groups": []interface{}{"group1", "group2"}})
manager := auth.NewManager(store, "", "", "", "", []string{}, false)
manager := auth.NewManager(store, "", "", "", "", []string{}, false, nil)
t.Run("JWT groups disabled", func(t *testing.T) {
userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token)
@@ -225,7 +225,7 @@ func TestAuthManager_ValidateAndParseToken(t *testing.T) {
keyId := "test-key"
// note, we can use a nil store because ValidateAndParseToken does not use it in it's flow
manager := auth.NewManager(nil, issuer, audience, server.URL, userIdClaim, []string{audience}, false)
manager := auth.NewManager(nil, issuer, audience, server.URL, userIdClaim, []string{audience}, false, nil)
customClaim := func(name string) string {
return fmt.Sprintf("%s/%s", audience, name)

View File

@@ -130,6 +130,10 @@ func (gl *geolocationImpl) Lookup(ip net.IP) (*Record, error) {
gl.mux.RLock()
defer gl.mux.RUnlock()
if gl.db == nil {
return nil, fmt.Errorf("geolocation database is not available")
}
var record Record
err := gl.db.Lookup(ip, &record)
if err != nil {
@@ -173,8 +177,14 @@ func (gl *geolocationImpl) GetCitiesByCountry(countryISOCode string) ([]City, er
func (gl *geolocationImpl) Stop() error {
close(gl.stopCh)
if gl.db != nil {
if err := gl.db.Close(); err != nil {
gl.mux.Lock()
db := gl.db
gl.db = nil
gl.mux.Unlock()
if db != nil {
if err := db.Close(); err != nil {
return err
}
}

View File

@@ -61,7 +61,10 @@ func (am *DefaultAccountManager) GetAllGroups(ctx context.Context, accountID, us
}
// GetGroupByName filters all groups in an account by name and returns the one with the most peers
func (am *DefaultAccountManager) GetGroupByName(ctx context.Context, groupName, accountID string) (*types.Group, error) {
func (am *DefaultAccountManager) GetGroupByName(ctx context.Context, groupName, accountID, userID string) (*types.Group, error) {
if err := am.CheckGroupPermissions(ctx, accountID, userID); err != nil {
return nil, err
}
return am.Store.GetGroupByName(ctx, store.LockingStrengthNone, accountID, groupName)
}

View File

@@ -52,7 +52,7 @@ func (h *handler) getAllGroups(w http.ResponseWriter, r *http.Request) {
groupName := r.URL.Query().Get("name")
if groupName != "" {
// Get single group by name
group, err := h.accountManager.GetGroupByName(r.Context(), groupName, accountID)
group, err := h.accountManager.GetGroupByName(r.Context(), groupName, accountID, userID)
if err != nil {
util.WriteError(r.Context(), err, w)
return
@@ -118,7 +118,7 @@ func (h *handler) updateGroup(w http.ResponseWriter, r *http.Request) {
return
}
allGroup, err := h.accountManager.GetGroupByName(r.Context(), "All", accountID)
allGroup, err := h.accountManager.GetGroupByName(r.Context(), "All", accountID, userID)
if err != nil {
util.WriteError(r.Context(), err, w)
return

View File

@@ -71,7 +71,7 @@ func initGroupTestData(initGroups ...*types.Group) *handler {
return groups, nil
},
GetGroupByNameFunc: func(ctx context.Context, groupName, _ string) (*types.Group, error) {
GetGroupByNameFunc: func(ctx context.Context, groupName, _, _ string) (*types.Group, error) {
if groupName == "All" {
return &types.Group{ID: "id-all", Name: "All", Issued: types.GroupIssuedAPI}, nil
}

View File

@@ -0,0 +1,238 @@
//go:build integration
package integration
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools/channel"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/shared/management/http/api"
)
func Test_Accounts_GetAll(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, true},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
for _, user := range users {
t.Run(user.name+" - Get all accounts", func(t *testing.T) {
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/accounts.sql", nil, true)
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, "/api/accounts", user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, user.expectResponse)
if !expectResponse {
return
}
got := []api.Account{}
if err := json.Unmarshal(content, &got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
assert.Equal(t, 1, len(got))
account := got[0]
assert.Equal(t, "test.com", account.Domain)
assert.Equal(t, "private", account.DomainCategory)
assert.Equal(t, true, account.Settings.PeerLoginExpirationEnabled)
assert.Equal(t, 86400, account.Settings.PeerLoginExpiration)
assert.Equal(t, false, account.Settings.RegularUsersViewBlocked)
select {
case <-done:
case <-time.After(time.Second):
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
}
})
}
}
func Test_Accounts_Update(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
trueVal := true
falseVal := false
tt := []struct {
name string
expectedStatus int
requestBody *api.AccountRequest
verifyResponse func(t *testing.T, account *api.Account)
verifyDB func(t *testing.T, account *types.Account)
}{
{
name: "Disable peer login expiration",
requestBody: &api.AccountRequest{
Settings: api.AccountSettings{
PeerLoginExpirationEnabled: false,
PeerLoginExpiration: 86400,
},
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, account *api.Account) {
t.Helper()
assert.Equal(t, false, account.Settings.PeerLoginExpirationEnabled)
},
verifyDB: func(t *testing.T, dbAccount *types.Account) {
t.Helper()
assert.Equal(t, false, dbAccount.Settings.PeerLoginExpirationEnabled)
},
},
{
name: "Update peer login expiration to 48h",
requestBody: &api.AccountRequest{
Settings: api.AccountSettings{
PeerLoginExpirationEnabled: true,
PeerLoginExpiration: 172800,
},
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, account *api.Account) {
t.Helper()
assert.Equal(t, 172800, account.Settings.PeerLoginExpiration)
},
verifyDB: func(t *testing.T, dbAccount *types.Account) {
t.Helper()
assert.Equal(t, 172800*time.Second, dbAccount.Settings.PeerLoginExpiration)
},
},
{
name: "Enable regular users view blocked",
requestBody: &api.AccountRequest{
Settings: api.AccountSettings{
PeerLoginExpirationEnabled: true,
PeerLoginExpiration: 86400,
RegularUsersViewBlocked: true,
},
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, account *api.Account) {
t.Helper()
assert.Equal(t, true, account.Settings.RegularUsersViewBlocked)
},
verifyDB: func(t *testing.T, dbAccount *types.Account) {
t.Helper()
assert.Equal(t, true, dbAccount.Settings.RegularUsersViewBlocked)
},
},
{
name: "Enable groups propagation",
requestBody: &api.AccountRequest{
Settings: api.AccountSettings{
PeerLoginExpirationEnabled: true,
PeerLoginExpiration: 86400,
GroupsPropagationEnabled: &trueVal,
},
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, account *api.Account) {
t.Helper()
assert.NotNil(t, account.Settings.GroupsPropagationEnabled)
assert.Equal(t, true, *account.Settings.GroupsPropagationEnabled)
},
verifyDB: func(t *testing.T, dbAccount *types.Account) {
t.Helper()
assert.Equal(t, true, dbAccount.Settings.GroupsPropagationEnabled)
},
},
{
name: "Enable JWT groups",
requestBody: &api.AccountRequest{
Settings: api.AccountSettings{
PeerLoginExpirationEnabled: true,
PeerLoginExpiration: 86400,
GroupsPropagationEnabled: &falseVal,
JwtGroupsEnabled: &trueVal,
JwtGroupsClaimName: stringPointer("groups"),
},
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, account *api.Account) {
t.Helper()
assert.NotNil(t, account.Settings.JwtGroupsEnabled)
assert.Equal(t, true, *account.Settings.JwtGroupsEnabled)
assert.NotNil(t, account.Settings.JwtGroupsClaimName)
assert.Equal(t, "groups", *account.Settings.JwtGroupsClaimName)
},
verifyDB: func(t *testing.T, dbAccount *types.Account) {
t.Helper()
assert.Equal(t, true, dbAccount.Settings.JWTGroupsEnabled)
assert.Equal(t, "groups", dbAccount.Settings.JWTGroupsClaimName)
},
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/accounts.sql", nil, false)
body, err := json.Marshal(tc.requestBody)
if err != nil {
t.Fatalf("Failed to marshal request body: %v", err)
}
req := testing_tools.BuildRequest(t, body, http.MethodPut, strings.Replace("/api/accounts/{accountId}", "{accountId}", testing_tools.TestAccountId, 1), user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
got := &api.Account{}
if err := json.Unmarshal(content, got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
assert.Equal(t, testing_tools.TestAccountId, got.Id)
assert.Equal(t, "test.com", got.Domain)
tc.verifyResponse(t, got)
db := testing_tools.GetDB(t, am.GetStore())
dbAccount := testing_tools.VerifyAccountSettings(t, db)
tc.verifyDB(t, dbAccount)
})
}
}
}
func stringPointer(s string) *string {
return &s
}

View File

@@ -0,0 +1,554 @@
//go:build integration
package integration
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools/channel"
"github.com/netbirdio/netbird/shared/management/http/api"
)
func Test_Nameservers_GetAll(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
for _, user := range users {
t.Run(user.name+" - Get all nameservers", func(t *testing.T) {
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/dns.sql", nil, true)
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, "/api/dns/nameservers", user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, user.expectResponse)
if !expectResponse {
return
}
got := []api.NameserverGroup{}
if err := json.Unmarshal(content, &got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
assert.Equal(t, 1, len(got))
assert.Equal(t, "testNSGroup", got[0].Name)
select {
case <-done:
case <-time.After(time.Second):
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
}
})
}
}
func Test_Nameservers_GetById(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
tt := []struct {
name string
nsGroupId string
expectedStatus int
expectGroup bool
}{
{
name: "Get existing nameserver group",
nsGroupId: "testNSGroupId",
expectedStatus: http.StatusOK,
expectGroup: true,
},
{
name: "Get non-existing nameserver group",
nsGroupId: "nonExistingNSGroupId",
expectedStatus: http.StatusNotFound,
expectGroup: false,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/dns.sql", nil, true)
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, strings.Replace("/api/dns/nameservers/{nsgroupId}", "{nsgroupId}", tc.nsGroupId, 1), user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
if tc.expectGroup {
got := &api.NameserverGroup{}
if err := json.Unmarshal(content, got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
assert.Equal(t, "testNSGroupId", got.Id)
assert.Equal(t, "testNSGroup", got.Name)
}
select {
case <-done:
case <-time.After(time.Second):
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
}
})
}
}
}
func Test_Nameservers_Create(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
tt := []struct {
name string
requestBody *api.PostApiDnsNameserversJSONRequestBody
expectedStatus int
verifyResponse func(t *testing.T, nsGroup *api.NameserverGroup)
}{
{
name: "Create nameserver group with single NS",
requestBody: &api.PostApiDnsNameserversJSONRequestBody{
Name: "newNSGroup",
Description: "a new nameserver group",
Nameservers: []api.Nameserver{
{Ip: "8.8.8.8", NsType: "udp", Port: 53},
},
Groups: []string{testing_tools.TestGroupId},
Primary: false,
Domains: []string{"test.com"},
Enabled: true,
SearchDomainsEnabled: false,
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, nsGroup *api.NameserverGroup) {
t.Helper()
assert.NotEmpty(t, nsGroup.Id)
assert.Equal(t, "newNSGroup", nsGroup.Name)
assert.Equal(t, 1, len(nsGroup.Nameservers))
assert.Equal(t, false, nsGroup.Primary)
},
},
{
name: "Create primary nameserver group",
requestBody: &api.PostApiDnsNameserversJSONRequestBody{
Name: "primaryNS",
Description: "primary nameserver",
Nameservers: []api.Nameserver{
{Ip: "1.1.1.1", NsType: "udp", Port: 53},
},
Groups: []string{testing_tools.TestGroupId},
Primary: true,
Domains: []string{},
Enabled: true,
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, nsGroup *api.NameserverGroup) {
t.Helper()
assert.Equal(t, true, nsGroup.Primary)
},
},
{
name: "Create nameserver group with empty groups",
requestBody: &api.PostApiDnsNameserversJSONRequestBody{
Name: "emptyGroupsNS",
Description: "no groups",
Nameservers: []api.Nameserver{
{Ip: "8.8.8.8", NsType: "udp", Port: 53},
},
Groups: []string{},
Primary: true,
Domains: []string{},
Enabled: true,
},
expectedStatus: http.StatusUnprocessableEntity,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/dns.sql", nil, false)
body, err := json.Marshal(tc.requestBody)
if err != nil {
t.Fatalf("Failed to marshal request body: %v", err)
}
req := testing_tools.BuildRequest(t, body, http.MethodPost, "/api/dns/nameservers", user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
if tc.verifyResponse != nil {
got := &api.NameserverGroup{}
if err := json.Unmarshal(content, got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
tc.verifyResponse(t, got)
// Verify the created NS group directly in the DB
db := testing_tools.GetDB(t, am.GetStore())
dbNS := testing_tools.VerifyNSGroupInDB(t, db, got.Id)
assert.Equal(t, got.Name, dbNS.Name)
assert.Equal(t, got.Primary, dbNS.Primary)
assert.Equal(t, len(got.Nameservers), len(dbNS.NameServers))
assert.Equal(t, got.Enabled, dbNS.Enabled)
assert.Equal(t, got.SearchDomainsEnabled, dbNS.SearchDomainsEnabled)
}
})
}
}
}
func Test_Nameservers_Update(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
tt := []struct {
name string
nsGroupId string
requestBody *api.PutApiDnsNameserversNsgroupIdJSONRequestBody
expectedStatus int
verifyResponse func(t *testing.T, nsGroup *api.NameserverGroup)
}{
{
name: "Update nameserver group name",
nsGroupId: "testNSGroupId",
requestBody: &api.PutApiDnsNameserversNsgroupIdJSONRequestBody{
Name: "updatedNSGroup",
Description: "updated description",
Nameservers: []api.Nameserver{
{Ip: "1.1.1.1", NsType: "udp", Port: 53},
},
Groups: []string{testing_tools.TestGroupId},
Primary: false,
Domains: []string{"example.com"},
Enabled: true,
SearchDomainsEnabled: false,
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, nsGroup *api.NameserverGroup) {
t.Helper()
assert.Equal(t, "updatedNSGroup", nsGroup.Name)
assert.Equal(t, "updated description", nsGroup.Description)
},
},
{
name: "Update non-existing nameserver group",
nsGroupId: "nonExistingNSGroupId",
requestBody: &api.PutApiDnsNameserversNsgroupIdJSONRequestBody{
Name: "whatever",
Nameservers: []api.Nameserver{
{Ip: "1.1.1.1", NsType: "udp", Port: 53},
},
Groups: []string{testing_tools.TestGroupId},
Primary: true,
Domains: []string{},
Enabled: true,
},
expectedStatus: http.StatusNotFound,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/dns.sql", nil, false)
body, err := json.Marshal(tc.requestBody)
if err != nil {
t.Fatalf("Failed to marshal request body: %v", err)
}
req := testing_tools.BuildRequest(t, body, http.MethodPut, strings.Replace("/api/dns/nameservers/{nsgroupId}", "{nsgroupId}", tc.nsGroupId, 1), user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
if tc.verifyResponse != nil {
got := &api.NameserverGroup{}
if err := json.Unmarshal(content, got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
tc.verifyResponse(t, got)
// Verify the updated NS group directly in the DB
db := testing_tools.GetDB(t, am.GetStore())
dbNS := testing_tools.VerifyNSGroupInDB(t, db, tc.nsGroupId)
assert.Equal(t, "updatedNSGroup", dbNS.Name)
assert.Equal(t, "updated description", dbNS.Description)
assert.Equal(t, false, dbNS.Primary)
assert.Equal(t, true, dbNS.Enabled)
assert.Equal(t, 1, len(dbNS.NameServers))
assert.Equal(t, false, dbNS.SearchDomainsEnabled)
}
})
}
}
}
func Test_Nameservers_Delete(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
tt := []struct {
name string
nsGroupId string
expectedStatus int
}{
{
name: "Delete existing nameserver group",
nsGroupId: "testNSGroupId",
expectedStatus: http.StatusOK,
},
{
name: "Delete non-existing nameserver group",
nsGroupId: "nonExistingNSGroupId",
expectedStatus: http.StatusNotFound,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/dns.sql", nil, false)
req := testing_tools.BuildRequest(t, []byte{}, http.MethodDelete, strings.Replace("/api/dns/nameservers/{nsgroupId}", "{nsgroupId}", tc.nsGroupId, 1), user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
// Verify deletion in DB for successful deletes by privileged users
if tc.expectedStatus == http.StatusOK && user.expectResponse {
db := testing_tools.GetDB(t, am.GetStore())
testing_tools.VerifyNSGroupNotInDB(t, db, tc.nsGroupId)
}
})
}
}
}
func Test_DnsSettings_Get(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
for _, user := range users {
t.Run(user.name+" - Get DNS settings", func(t *testing.T) {
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/dns.sql", nil, true)
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, "/api/dns/settings", user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, user.expectResponse)
if !expectResponse {
return
}
got := &api.DNSSettings{}
if err := json.Unmarshal(content, got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
assert.NotNil(t, got.DisabledManagementGroups)
select {
case <-done:
case <-time.After(time.Second):
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
}
})
}
}
func Test_DnsSettings_Update(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
tt := []struct {
name string
requestBody *api.PutApiDnsSettingsJSONRequestBody
expectedStatus int
verifyResponse func(t *testing.T, settings *api.DNSSettings)
expectedDBDisabledMgmtLen int
expectedDBDisabledMgmtItem string
}{
{
name: "Update disabled management groups",
requestBody: &api.PutApiDnsSettingsJSONRequestBody{
DisabledManagementGroups: []string{testing_tools.TestGroupId},
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, settings *api.DNSSettings) {
t.Helper()
assert.Equal(t, 1, len(settings.DisabledManagementGroups))
assert.Equal(t, testing_tools.TestGroupId, settings.DisabledManagementGroups[0])
},
expectedDBDisabledMgmtLen: 1,
expectedDBDisabledMgmtItem: testing_tools.TestGroupId,
},
{
name: "Update with empty disabled management groups",
requestBody: &api.PutApiDnsSettingsJSONRequestBody{
DisabledManagementGroups: []string{},
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, settings *api.DNSSettings) {
t.Helper()
assert.Equal(t, 0, len(settings.DisabledManagementGroups))
},
expectedDBDisabledMgmtLen: 0,
},
{
name: "Update with non-existing group",
requestBody: &api.PutApiDnsSettingsJSONRequestBody{
DisabledManagementGroups: []string{"nonExistingGroupId"},
},
expectedStatus: http.StatusUnprocessableEntity,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/dns.sql", nil, false)
body, err := json.Marshal(tc.requestBody)
if err != nil {
t.Fatalf("Failed to marshal request body: %v", err)
}
req := testing_tools.BuildRequest(t, body, http.MethodPut, "/api/dns/settings", user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
if tc.verifyResponse != nil {
got := &api.DNSSettings{}
if err := json.Unmarshal(content, got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
tc.verifyResponse(t, got)
// Verify DNS settings directly in the DB
db := testing_tools.GetDB(t, am.GetStore())
dbAccount := testing_tools.VerifyAccountSettings(t, db)
assert.Equal(t, tc.expectedDBDisabledMgmtLen, len(dbAccount.DNSSettings.DisabledManagementGroups))
if tc.expectedDBDisabledMgmtItem != "" {
assert.Contains(t, dbAccount.DNSSettings.DisabledManagementGroups, tc.expectedDBDisabledMgmtItem)
}
}
})
}
}
}

View File

@@ -0,0 +1,105 @@
//go:build integration
package integration
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools/channel"
"github.com/netbirdio/netbird/shared/management/http/api"
)
func Test_Events_GetAll(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
for _, user := range users {
t.Run(user.name+" - Get all events", func(t *testing.T) {
apiHandler, _, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/events.sql", nil, false)
// First, perform a mutation to generate an event (create a group as admin)
groupBody, err := json.Marshal(&api.GroupRequest{Name: "eventTestGroup"})
if err != nil {
t.Fatalf("Failed to marshal group request: %v", err)
}
createReq := testing_tools.BuildRequest(t, groupBody, http.MethodPost, "/api/groups", testing_tools.TestAdminId)
createRecorder := httptest.NewRecorder()
apiHandler.ServeHTTP(createRecorder, createReq)
assert.Equal(t, http.StatusOK, createRecorder.Code, "Failed to create group to generate event")
// Now query events
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, "/api/events", user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, user.expectResponse)
if !expectResponse {
return
}
got := []api.Event{}
if err := json.Unmarshal(content, &got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
assert.GreaterOrEqual(t, len(got), 1, "Expected at least one event after creating a group")
// Verify the group creation event exists
found := false
for _, event := range got {
if event.ActivityCode == "group.add" {
found = true
assert.Equal(t, testing_tools.TestAdminId, event.InitiatorId)
assert.Equal(t, "Group created", event.Activity)
break
}
}
assert.True(t, found, "Expected to find a group.add event")
})
}
}
func Test_Events_GetAll_Empty(t *testing.T) {
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/events.sql", nil, true)
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, "/api/events", testing_tools.TestAdminId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, true)
if !expectResponse {
return
}
got := []api.Event{}
if err := json.Unmarshal(content, &got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
assert.Equal(t, 0, len(got), "Expected empty events list when no mutations have been performed")
select {
case <-done:
case <-time.After(time.Second):
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
}
}

View File

@@ -0,0 +1,382 @@
//go:build integration
package integration
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools/channel"
"github.com/netbirdio/netbird/shared/management/http/api"
)
func Test_Groups_GetAll(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, true},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
for _, user := range users {
t.Run(user.name+" - Get all groups", func(t *testing.T) {
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/groups.sql", nil, true)
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, "/api/groups", user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, user.expectResponse)
if !expectResponse {
return
}
got := []api.Group{}
if err := json.Unmarshal(content, &got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
assert.GreaterOrEqual(t, len(got), 2)
select {
case <-done:
case <-time.After(time.Second):
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
}
})
}
}
func Test_Groups_GetById(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, true},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
tt := []struct {
name string
groupId string
expectedStatus int
expectGroup bool
}{
{
name: "Get existing group",
groupId: testing_tools.TestGroupId,
expectedStatus: http.StatusOK,
expectGroup: true,
},
{
name: "Get non-existing group",
groupId: "nonExistingGroupId",
expectedStatus: http.StatusNotFound,
expectGroup: false,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/groups.sql", nil, true)
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, strings.Replace("/api/groups/{groupId}", "{groupId}", tc.groupId, 1), user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
if tc.expectGroup {
got := &api.Group{}
if err := json.Unmarshal(content, got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
assert.Equal(t, tc.groupId, got.Id)
assert.Equal(t, "testGroupName", got.Name)
assert.Equal(t, 1, got.PeersCount)
}
select {
case <-done:
case <-time.After(time.Second):
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
}
})
}
}
}
func Test_Groups_Create(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
tt := []struct {
name string
requestBody *api.GroupRequest
expectedStatus int
verifyResponse func(t *testing.T, group *api.Group)
}{
{
name: "Create group with valid name",
requestBody: &api.GroupRequest{
Name: "brandNewGroup",
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, group *api.Group) {
t.Helper()
assert.NotEmpty(t, group.Id)
assert.Equal(t, "brandNewGroup", group.Name)
assert.Equal(t, 0, group.PeersCount)
},
},
{
name: "Create group with peers",
requestBody: &api.GroupRequest{
Name: "groupWithPeers",
Peers: &[]string{testing_tools.TestPeerId},
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, group *api.Group) {
t.Helper()
assert.NotEmpty(t, group.Id)
assert.Equal(t, "groupWithPeers", group.Name)
assert.Equal(t, 1, group.PeersCount)
},
},
{
name: "Create group with empty name",
requestBody: &api.GroupRequest{
Name: "",
},
expectedStatus: http.StatusUnprocessableEntity,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/groups.sql", nil, false)
body, err := json.Marshal(tc.requestBody)
if err != nil {
t.Fatalf("Failed to marshal request body: %v", err)
}
req := testing_tools.BuildRequest(t, body, http.MethodPost, "/api/groups", user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
if tc.verifyResponse != nil {
got := &api.Group{}
if err := json.Unmarshal(content, got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
tc.verifyResponse(t, got)
// Verify group exists in DB
db := testing_tools.GetDB(t, am.GetStore())
dbGroup := testing_tools.VerifyGroupInDB(t, db, got.Id)
assert.Equal(t, tc.requestBody.Name, dbGroup.Name)
}
})
}
}
}
func Test_Groups_Update(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
tt := []struct {
name string
groupId string
requestBody *api.GroupRequest
expectedStatus int
verifyResponse func(t *testing.T, group *api.Group)
}{
{
name: "Update group name",
groupId: testing_tools.TestGroupId,
requestBody: &api.GroupRequest{
Name: "updatedGroupName",
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, group *api.Group) {
t.Helper()
assert.Equal(t, testing_tools.TestGroupId, group.Id)
assert.Equal(t, "updatedGroupName", group.Name)
},
},
{
name: "Update group peers",
groupId: testing_tools.TestGroupId,
requestBody: &api.GroupRequest{
Name: "testGroupName",
Peers: &[]string{},
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, group *api.Group) {
t.Helper()
assert.Equal(t, 0, group.PeersCount)
},
},
{
name: "Update with empty name",
groupId: testing_tools.TestGroupId,
requestBody: &api.GroupRequest{
Name: "",
},
expectedStatus: http.StatusUnprocessableEntity,
},
{
name: "Update non-existing group",
groupId: "nonExistingGroupId",
requestBody: &api.GroupRequest{
Name: "someName",
},
expectedStatus: http.StatusNotFound,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/groups.sql", nil, false)
body, err := json.Marshal(tc.requestBody)
if err != nil {
t.Fatalf("Failed to marshal request body: %v", err)
}
req := testing_tools.BuildRequest(t, body, http.MethodPut, strings.Replace("/api/groups/{groupId}", "{groupId}", tc.groupId, 1), user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
if tc.verifyResponse != nil {
got := &api.Group{}
if err := json.Unmarshal(content, got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
tc.verifyResponse(t, got)
// Verify updated group in DB
db := testing_tools.GetDB(t, am.GetStore())
dbGroup := testing_tools.VerifyGroupInDB(t, db, tc.groupId)
assert.Equal(t, tc.requestBody.Name, dbGroup.Name)
}
})
}
}
}
func Test_Groups_Delete(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
tt := []struct {
name string
groupId string
expectedStatus int
}{
{
name: "Delete existing group not in use",
groupId: testing_tools.NewGroupId,
expectedStatus: http.StatusOK,
},
{
name: "Delete non-existing group",
groupId: "nonExistingGroupId",
expectedStatus: http.StatusBadRequest,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/groups.sql", nil, false)
req := testing_tools.BuildRequest(t, []byte{}, http.MethodDelete, strings.Replace("/api/groups/{groupId}", "{groupId}", tc.groupId, 1), user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
_, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if expectResponse && tc.expectedStatus == http.StatusOK {
db := testing_tools.GetDB(t, am.GetStore())
testing_tools.VerifyGroupNotInDB(t, db, tc.groupId)
}
})
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,605 @@
//go:build integration
package integration
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools/channel"
"github.com/netbirdio/netbird/shared/management/http/api"
)
const (
testPeerId2 = "testPeerId2"
)
func Test_Peers_GetAll(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{
name: "Regular user",
userId: testing_tools.TestUserId,
expectResponse: false,
},
{
name: "Admin user",
userId: testing_tools.TestAdminId,
expectResponse: true,
},
{
name: "Owner user",
userId: testing_tools.TestOwnerId,
expectResponse: true,
},
{
name: "Regular service user",
userId: testing_tools.TestServiceUserId,
expectResponse: true,
},
{
name: "Admin service user",
userId: testing_tools.TestServiceAdminId,
expectResponse: true,
},
{
name: "Blocked user",
userId: testing_tools.BlockedUserId,
expectResponse: false,
},
{
name: "Other user",
userId: testing_tools.OtherUserId,
expectResponse: false,
},
{
name: "Invalid token",
userId: testing_tools.InvalidToken,
expectResponse: false,
},
}
for _, user := range users {
t.Run(user.name+" - Get all peers", func(t *testing.T) {
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/peers_integration.sql", nil, true)
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, "/api/peers", user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, user.expectResponse)
if !expectResponse {
return
}
var got []api.PeerBatch
if err := json.Unmarshal(content, &got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
assert.GreaterOrEqual(t, len(got), 2, "Expected at least 2 peers")
select {
case <-done:
case <-time.After(time.Second):
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
}
})
}
}
func Test_Peers_GetById(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{
name: "Regular user",
userId: testing_tools.TestUserId,
expectResponse: false,
},
{
name: "Admin user",
userId: testing_tools.TestAdminId,
expectResponse: true,
},
{
name: "Owner user",
userId: testing_tools.TestOwnerId,
expectResponse: true,
},
{
name: "Regular service user",
userId: testing_tools.TestServiceUserId,
expectResponse: true,
},
{
name: "Admin service user",
userId: testing_tools.TestServiceAdminId,
expectResponse: true,
},
{
name: "Blocked user",
userId: testing_tools.BlockedUserId,
expectResponse: false,
},
{
name: "Other user",
userId: testing_tools.OtherUserId,
expectResponse: false,
},
{
name: "Invalid token",
userId: testing_tools.InvalidToken,
expectResponse: false,
},
}
tt := []struct {
name string
expectedStatus int
requestType string
requestPath string
requestId string
verifyResponse func(t *testing.T, peer *api.Peer)
}{
{
name: "Get existing peer",
requestType: http.MethodGet,
requestPath: "/api/peers/{peerId}",
requestId: testing_tools.TestPeerId,
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, peer *api.Peer) {
t.Helper()
assert.Equal(t, testing_tools.TestPeerId, peer.Id)
assert.Equal(t, "test-peer-1", peer.Name)
assert.Equal(t, "test-host-1", peer.Hostname)
assert.Equal(t, "Debian GNU/Linux ", peer.Os)
assert.Equal(t, "0.12.0", peer.Version)
assert.Equal(t, false, peer.SshEnabled)
assert.Equal(t, true, peer.LoginExpirationEnabled)
},
},
{
name: "Get second existing peer",
requestType: http.MethodGet,
requestPath: "/api/peers/{peerId}",
requestId: testPeerId2,
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, peer *api.Peer) {
t.Helper()
assert.Equal(t, testPeerId2, peer.Id)
assert.Equal(t, "test-peer-2", peer.Name)
assert.Equal(t, "test-host-2", peer.Hostname)
assert.Equal(t, "Ubuntu ", peer.Os)
assert.Equal(t, true, peer.SshEnabled)
assert.Equal(t, false, peer.LoginExpirationEnabled)
assert.Equal(t, true, peer.Connected)
},
},
{
name: "Get non-existing peer",
requestType: http.MethodGet,
requestPath: "/api/peers/{peerId}",
requestId: "nonExistingPeerId",
expectedStatus: http.StatusNotFound,
verifyResponse: nil,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/peers_integration.sql", nil, true)
req := testing_tools.BuildRequest(t, []byte{}, tc.requestType, strings.Replace(tc.requestPath, "{peerId}", tc.requestId, 1), user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
if tc.verifyResponse != nil {
got := &api.Peer{}
if err := json.Unmarshal(content, got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
tc.verifyResponse(t, got)
}
select {
case <-done:
case <-time.After(time.Second):
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
}
})
}
}
}
func Test_Peers_Update(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{
name: "Regular user",
userId: testing_tools.TestUserId,
expectResponse: false,
},
{
name: "Admin user",
userId: testing_tools.TestAdminId,
expectResponse: true,
},
{
name: "Owner user",
userId: testing_tools.TestOwnerId,
expectResponse: true,
},
{
name: "Regular service user",
userId: testing_tools.TestServiceUserId,
expectResponse: false,
},
{
name: "Admin service user",
userId: testing_tools.TestServiceAdminId,
expectResponse: true,
},
{
name: "Blocked user",
userId: testing_tools.BlockedUserId,
expectResponse: false,
},
{
name: "Other user",
userId: testing_tools.OtherUserId,
expectResponse: false,
},
{
name: "Invalid token",
userId: testing_tools.InvalidToken,
expectResponse: false,
},
}
tt := []struct {
name string
expectedStatus int
requestBody *api.PeerRequest
requestType string
requestPath string
requestId string
verifyResponse func(t *testing.T, peer *api.Peer)
}{
{
name: "Update peer name",
requestType: http.MethodPut,
requestPath: "/api/peers/{peerId}",
requestId: testing_tools.TestPeerId,
requestBody: &api.PeerRequest{
Name: "updated-peer-name",
SshEnabled: false,
LoginExpirationEnabled: true,
InactivityExpirationEnabled: false,
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, peer *api.Peer) {
t.Helper()
assert.Equal(t, testing_tools.TestPeerId, peer.Id)
assert.Equal(t, "updated-peer-name", peer.Name)
assert.Equal(t, false, peer.SshEnabled)
assert.Equal(t, true, peer.LoginExpirationEnabled)
},
},
{
name: "Enable SSH on peer",
requestType: http.MethodPut,
requestPath: "/api/peers/{peerId}",
requestId: testing_tools.TestPeerId,
requestBody: &api.PeerRequest{
Name: "test-peer-1",
SshEnabled: true,
LoginExpirationEnabled: true,
InactivityExpirationEnabled: false,
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, peer *api.Peer) {
t.Helper()
assert.Equal(t, testing_tools.TestPeerId, peer.Id)
assert.Equal(t, "test-peer-1", peer.Name)
assert.Equal(t, true, peer.SshEnabled)
assert.Equal(t, true, peer.LoginExpirationEnabled)
},
},
{
name: "Disable login expiration on peer",
requestType: http.MethodPut,
requestPath: "/api/peers/{peerId}",
requestId: testing_tools.TestPeerId,
requestBody: &api.PeerRequest{
Name: "test-peer-1",
SshEnabled: false,
LoginExpirationEnabled: false,
InactivityExpirationEnabled: false,
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, peer *api.Peer) {
t.Helper()
assert.Equal(t, testing_tools.TestPeerId, peer.Id)
assert.Equal(t, false, peer.LoginExpirationEnabled)
},
},
{
name: "Update non-existing peer",
requestType: http.MethodPut,
requestPath: "/api/peers/{peerId}",
requestId: "nonExistingPeerId",
requestBody: &api.PeerRequest{
Name: "updated-name",
SshEnabled: false,
LoginExpirationEnabled: false,
InactivityExpirationEnabled: false,
},
expectedStatus: http.StatusNotFound,
verifyResponse: nil,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/peers_integration.sql", nil, false)
body, err := json.Marshal(tc.requestBody)
if err != nil {
t.Fatalf("Failed to marshal request body: %v", err)
}
req := testing_tools.BuildRequest(t, body, tc.requestType, strings.Replace(tc.requestPath, "{peerId}", tc.requestId, 1), user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
if tc.verifyResponse != nil {
got := &api.Peer{}
if err := json.Unmarshal(content, got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
tc.verifyResponse(t, got)
// Verify updated peer in DB
db := testing_tools.GetDB(t, am.GetStore())
dbPeer := testing_tools.VerifyPeerInDB(t, db, tc.requestId)
assert.Equal(t, tc.requestBody.Name, dbPeer.Name)
assert.Equal(t, tc.requestBody.SshEnabled, dbPeer.SSHEnabled)
assert.Equal(t, tc.requestBody.LoginExpirationEnabled, dbPeer.LoginExpirationEnabled)
assert.Equal(t, tc.requestBody.InactivityExpirationEnabled, dbPeer.InactivityExpirationEnabled)
}
})
}
}
}
func Test_Peers_Delete(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{
name: "Regular user",
userId: testing_tools.TestUserId,
expectResponse: false,
},
{
name: "Admin user",
userId: testing_tools.TestAdminId,
expectResponse: true,
},
{
name: "Owner user",
userId: testing_tools.TestOwnerId,
expectResponse: true,
},
{
name: "Regular service user",
userId: testing_tools.TestServiceUserId,
expectResponse: false,
},
{
name: "Admin service user",
userId: testing_tools.TestServiceAdminId,
expectResponse: true,
},
{
name: "Blocked user",
userId: testing_tools.BlockedUserId,
expectResponse: false,
},
{
name: "Other user",
userId: testing_tools.OtherUserId,
expectResponse: false,
},
{
name: "Invalid token",
userId: testing_tools.InvalidToken,
expectResponse: false,
},
}
tt := []struct {
name string
expectedStatus int
requestType string
requestPath string
requestId string
}{
{
name: "Delete existing peer",
requestType: http.MethodDelete,
requestPath: "/api/peers/{peerId}",
requestId: testPeerId2,
expectedStatus: http.StatusOK,
},
{
name: "Delete non-existing peer",
requestType: http.MethodDelete,
requestPath: "/api/peers/{peerId}",
requestId: "nonExistingPeerId",
expectedStatus: http.StatusNotFound,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/peers_integration.sql", nil, false)
req := testing_tools.BuildRequest(t, []byte{}, tc.requestType, strings.Replace(tc.requestPath, "{peerId}", tc.requestId, 1), user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
_, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
// Verify peer is actually deleted in DB
if tc.expectedStatus == http.StatusOK {
db := testing_tools.GetDB(t, am.GetStore())
testing_tools.VerifyPeerNotInDB(t, db, tc.requestId)
}
})
}
}
}
func Test_Peers_GetAccessiblePeers(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{
name: "Regular user",
userId: testing_tools.TestUserId,
expectResponse: false,
},
{
name: "Admin user",
userId: testing_tools.TestAdminId,
expectResponse: true,
},
{
name: "Owner user",
userId: testing_tools.TestOwnerId,
expectResponse: true,
},
{
name: "Regular service user",
userId: testing_tools.TestServiceUserId,
expectResponse: false,
},
{
name: "Admin service user",
userId: testing_tools.TestServiceAdminId,
expectResponse: true,
},
{
name: "Blocked user",
userId: testing_tools.BlockedUserId,
expectResponse: false,
},
{
name: "Other user",
userId: testing_tools.OtherUserId,
expectResponse: false,
},
{
name: "Invalid token",
userId: testing_tools.InvalidToken,
expectResponse: false,
},
}
tt := []struct {
name string
expectedStatus int
requestType string
requestPath string
requestId string
}{
{
name: "Get accessible peers for existing peer",
requestType: http.MethodGet,
requestPath: "/api/peers/{peerId}/accessible-peers",
requestId: testing_tools.TestPeerId,
expectedStatus: http.StatusOK,
},
{
name: "Get accessible peers for non-existing peer",
requestType: http.MethodGet,
requestPath: "/api/peers/{peerId}/accessible-peers",
requestId: "nonExistingPeerId",
expectedStatus: http.StatusOK,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/peers_integration.sql", nil, true)
req := testing_tools.BuildRequest(t, []byte{}, tc.requestType, strings.Replace(tc.requestPath, "{peerId}", tc.requestId, 1), user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
if tc.expectedStatus == http.StatusOK {
var got []api.AccessiblePeer
if err := json.Unmarshal(content, &got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
// The accessible peers list should be a valid array (may be empty if no policies connect peers)
assert.NotNil(t, got, "Expected accessible peers to be a valid array")
}
select {
case <-done:
case <-time.After(time.Second):
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
}
})
}
}
}

View File

@@ -0,0 +1,488 @@
//go:build integration
package integration
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools/channel"
"github.com/netbirdio/netbird/shared/management/http/api"
)
func Test_Policies_GetAll(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
for _, user := range users {
t.Run(user.name+" - Get all policies", func(t *testing.T) {
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/policies.sql", nil, true)
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, "/api/policies", user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, user.expectResponse)
if !expectResponse {
return
}
got := []api.Policy{}
if err := json.Unmarshal(content, &got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
assert.Equal(t, 1, len(got))
assert.Equal(t, "testPolicy", got[0].Name)
select {
case <-done:
case <-time.After(time.Second):
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
}
})
}
}
func Test_Policies_GetById(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
tt := []struct {
name string
policyId string
expectedStatus int
expectPolicy bool
}{
{
name: "Get existing policy",
policyId: "testPolicyId",
expectedStatus: http.StatusOK,
expectPolicy: true,
},
{
name: "Get non-existing policy",
policyId: "nonExistingPolicyId",
expectedStatus: http.StatusNotFound,
expectPolicy: false,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/policies.sql", nil, true)
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, strings.Replace("/api/policies/{policyId}", "{policyId}", tc.policyId, 1), user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
if tc.expectPolicy {
got := &api.Policy{}
if err := json.Unmarshal(content, got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
assert.NotNil(t, got.Id)
assert.Equal(t, tc.policyId, *got.Id)
assert.Equal(t, "testPolicy", got.Name)
assert.Equal(t, true, got.Enabled)
assert.GreaterOrEqual(t, len(got.Rules), 1)
}
select {
case <-done:
case <-time.After(time.Second):
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
}
})
}
}
}
func Test_Policies_Create(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
srcGroups := []string{testing_tools.TestGroupId}
dstGroups := []string{testing_tools.TestGroupId}
tt := []struct {
name string
requestBody *api.PolicyCreate
expectedStatus int
verifyResponse func(t *testing.T, policy *api.Policy)
}{
{
name: "Create policy with accept rule",
requestBody: &api.PolicyCreate{
Name: "newPolicy",
Enabled: true,
Rules: []api.PolicyRuleUpdate{
{
Name: "allowAll",
Enabled: true,
Action: "accept",
Protocol: "all",
Bidirectional: true,
Sources: &srcGroups,
Destinations: &dstGroups,
},
},
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, policy *api.Policy) {
t.Helper()
assert.NotNil(t, policy.Id)
assert.Equal(t, "newPolicy", policy.Name)
assert.Equal(t, true, policy.Enabled)
assert.Equal(t, 1, len(policy.Rules))
assert.Equal(t, "allowAll", policy.Rules[0].Name)
},
},
{
name: "Create policy with drop rule",
requestBody: &api.PolicyCreate{
Name: "dropPolicy",
Enabled: true,
Rules: []api.PolicyRuleUpdate{
{
Name: "dropAll",
Enabled: true,
Action: "drop",
Protocol: "all",
Bidirectional: true,
Sources: &srcGroups,
Destinations: &dstGroups,
},
},
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, policy *api.Policy) {
t.Helper()
assert.Equal(t, "dropPolicy", policy.Name)
},
},
{
name: "Create policy with TCP rule and ports",
requestBody: &api.PolicyCreate{
Name: "tcpPolicy",
Enabled: true,
Rules: []api.PolicyRuleUpdate{
{
Name: "tcpRule",
Enabled: true,
Action: "accept",
Protocol: "tcp",
Bidirectional: true,
Sources: &srcGroups,
Destinations: &dstGroups,
Ports: &[]string{"80", "443"},
},
},
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, policy *api.Policy) {
t.Helper()
assert.Equal(t, "tcpPolicy", policy.Name)
assert.NotNil(t, policy.Rules[0].Ports)
assert.Equal(t, 2, len(*policy.Rules[0].Ports))
},
},
{
name: "Create policy with empty name",
requestBody: &api.PolicyCreate{
Name: "",
Enabled: true,
Rules: []api.PolicyRuleUpdate{
{
Name: "rule",
Enabled: true,
Action: "accept",
Protocol: "all",
Sources: &srcGroups,
Destinations: &dstGroups,
},
},
},
expectedStatus: http.StatusUnprocessableEntity,
},
{
name: "Create policy with no rules",
requestBody: &api.PolicyCreate{
Name: "noRulesPolicy",
Enabled: true,
Rules: []api.PolicyRuleUpdate{},
},
expectedStatus: http.StatusUnprocessableEntity,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/policies.sql", nil, false)
body, err := json.Marshal(tc.requestBody)
if err != nil {
t.Fatalf("Failed to marshal request body: %v", err)
}
req := testing_tools.BuildRequest(t, body, http.MethodPost, "/api/policies", user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
if tc.verifyResponse != nil {
got := &api.Policy{}
if err := json.Unmarshal(content, got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
tc.verifyResponse(t, got)
// Verify policy exists in DB with correct fields
db := testing_tools.GetDB(t, am.GetStore())
dbPolicy := testing_tools.VerifyPolicyInDB(t, db, *got.Id)
assert.Equal(t, tc.requestBody.Name, dbPolicy.Name)
assert.Equal(t, tc.requestBody.Enabled, dbPolicy.Enabled)
assert.Equal(t, len(tc.requestBody.Rules), len(dbPolicy.Rules))
}
})
}
}
}
func Test_Policies_Update(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
srcGroups := []string{testing_tools.TestGroupId}
dstGroups := []string{testing_tools.TestGroupId}
tt := []struct {
name string
policyId string
requestBody *api.PolicyCreate
expectedStatus int
verifyResponse func(t *testing.T, policy *api.Policy)
}{
{
name: "Update policy name",
policyId: "testPolicyId",
requestBody: &api.PolicyCreate{
Name: "updatedPolicy",
Enabled: true,
Rules: []api.PolicyRuleUpdate{
{
Name: "testRule",
Enabled: true,
Action: "accept",
Protocol: "all",
Bidirectional: true,
Sources: &srcGroups,
Destinations: &dstGroups,
},
},
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, policy *api.Policy) {
t.Helper()
assert.Equal(t, "updatedPolicy", policy.Name)
},
},
{
name: "Update policy enabled state",
policyId: "testPolicyId",
requestBody: &api.PolicyCreate{
Name: "testPolicy",
Enabled: false,
Rules: []api.PolicyRuleUpdate{
{
Name: "testRule",
Enabled: true,
Action: "accept",
Protocol: "all",
Bidirectional: true,
Sources: &srcGroups,
Destinations: &dstGroups,
},
},
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, policy *api.Policy) {
t.Helper()
assert.Equal(t, false, policy.Enabled)
},
},
{
name: "Update non-existing policy",
policyId: "nonExistingPolicyId",
requestBody: &api.PolicyCreate{
Name: "whatever",
Enabled: true,
Rules: []api.PolicyRuleUpdate{
{
Name: "rule",
Enabled: true,
Action: "accept",
Protocol: "all",
Sources: &srcGroups,
Destinations: &dstGroups,
},
},
},
expectedStatus: http.StatusNotFound,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/policies.sql", nil, false)
body, err := json.Marshal(tc.requestBody)
if err != nil {
t.Fatalf("Failed to marshal request body: %v", err)
}
req := testing_tools.BuildRequest(t, body, http.MethodPut, strings.Replace("/api/policies/{policyId}", "{policyId}", tc.policyId, 1), user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
if tc.verifyResponse != nil {
got := &api.Policy{}
if err := json.Unmarshal(content, got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
tc.verifyResponse(t, got)
// Verify updated policy in DB
db := testing_tools.GetDB(t, am.GetStore())
dbPolicy := testing_tools.VerifyPolicyInDB(t, db, tc.policyId)
assert.Equal(t, tc.requestBody.Name, dbPolicy.Name)
assert.Equal(t, tc.requestBody.Enabled, dbPolicy.Enabled)
}
})
}
}
}
func Test_Policies_Delete(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
tt := []struct {
name string
policyId string
expectedStatus int
}{
{
name: "Delete existing policy",
policyId: "testPolicyId",
expectedStatus: http.StatusOK,
},
{
name: "Delete non-existing policy",
policyId: "nonExistingPolicyId",
expectedStatus: http.StatusNotFound,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/policies.sql", nil, false)
req := testing_tools.BuildRequest(t, []byte{}, http.MethodDelete, strings.Replace("/api/policies/{policyId}", "{policyId}", tc.policyId, 1), user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
_, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if expectResponse && tc.expectedStatus == http.StatusOK {
db := testing_tools.GetDB(t, am.GetStore())
testing_tools.VerifyPolicyNotInDB(t, db, tc.policyId)
}
})
}
}
}

View File

@@ -0,0 +1,455 @@
//go:build integration
package integration
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools/channel"
"github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/shared/management/http/api"
)
func Test_Routes_GetAll(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
for _, user := range users {
t.Run(user.name+" - Get all routes", func(t *testing.T) {
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/routes.sql", nil, true)
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, "/api/routes", user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, user.expectResponse)
if !expectResponse {
return
}
got := []api.Route{}
if err := json.Unmarshal(content, &got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
assert.Equal(t, 2, len(got))
select {
case <-done:
case <-time.After(time.Second):
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
}
})
}
}
func Test_Routes_GetById(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
tt := []struct {
name string
routeId string
expectedStatus int
expectRoute bool
}{
{
name: "Get existing route",
routeId: "testRouteId",
expectedStatus: http.StatusOK,
expectRoute: true,
},
{
name: "Get non-existing route",
routeId: "nonExistingRouteId",
expectedStatus: http.StatusNotFound,
expectRoute: false,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/routes.sql", nil, true)
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, strings.Replace("/api/routes/{routeId}", "{routeId}", tc.routeId, 1), user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
if tc.expectRoute {
got := &api.Route{}
if err := json.Unmarshal(content, got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
assert.Equal(t, tc.routeId, got.Id)
assert.Equal(t, "Test Network Route", got.Description)
}
select {
case <-done:
case <-time.After(time.Second):
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
}
})
}
}
}
func Test_Routes_Create(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
networkCIDR := "10.10.0.0/24"
peerID := testing_tools.TestPeerId
peerGroups := []string{"peerGroupId"}
tt := []struct {
name string
requestBody *api.RouteRequest
expectedStatus int
verifyResponse func(t *testing.T, route *api.Route)
}{
{
name: "Create network route with peer",
requestBody: &api.RouteRequest{
Description: "New network route",
Network: &networkCIDR,
Peer: &peerID,
NetworkId: "newNet",
Metric: 100,
Masquerade: true,
Enabled: true,
Groups: []string{testing_tools.TestGroupId},
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, route *api.Route) {
t.Helper()
assert.NotEmpty(t, route.Id)
assert.Equal(t, "New network route", route.Description)
assert.Equal(t, 100, route.Metric)
assert.Equal(t, true, route.Masquerade)
assert.Equal(t, true, route.Enabled)
},
},
{
name: "Create network route with peer groups",
requestBody: &api.RouteRequest{
Description: "Route with peer groups",
Network: &networkCIDR,
PeerGroups: &peerGroups,
NetworkId: "peerGroupNet",
Metric: 150,
Masquerade: false,
Enabled: true,
Groups: []string{testing_tools.TestGroupId},
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, route *api.Route) {
t.Helper()
assert.NotEmpty(t, route.Id)
assert.Equal(t, "Route with peer groups", route.Description)
},
},
{
name: "Create route with empty network_id",
requestBody: &api.RouteRequest{
Description: "Empty net id",
Network: &networkCIDR,
Peer: &peerID,
NetworkId: "",
Metric: 100,
Enabled: true,
Groups: []string{testing_tools.TestGroupId},
},
expectedStatus: http.StatusUnprocessableEntity,
},
{
name: "Create route with metric 0",
requestBody: &api.RouteRequest{
Description: "Zero metric",
Network: &networkCIDR,
Peer: &peerID,
NetworkId: "zeroMetric",
Metric: 0,
Enabled: true,
Groups: []string{testing_tools.TestGroupId},
},
expectedStatus: http.StatusUnprocessableEntity,
},
{
name: "Create route with metric 10000",
requestBody: &api.RouteRequest{
Description: "High metric",
Network: &networkCIDR,
Peer: &peerID,
NetworkId: "highMetric",
Metric: 10000,
Enabled: true,
Groups: []string{testing_tools.TestGroupId},
},
expectedStatus: http.StatusUnprocessableEntity,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/routes.sql", nil, false)
body, err := json.Marshal(tc.requestBody)
if err != nil {
t.Fatalf("Failed to marshal request body: %v", err)
}
req := testing_tools.BuildRequest(t, body, http.MethodPost, "/api/routes", user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
if tc.verifyResponse != nil {
got := &api.Route{}
if err := json.Unmarshal(content, got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
tc.verifyResponse(t, got)
// Verify route exists in DB with correct fields
db := testing_tools.GetDB(t, am.GetStore())
dbRoute := testing_tools.VerifyRouteInDB(t, db, route.ID(got.Id))
assert.Equal(t, tc.requestBody.Description, dbRoute.Description)
assert.Equal(t, tc.requestBody.Metric, dbRoute.Metric)
assert.Equal(t, tc.requestBody.Masquerade, dbRoute.Masquerade)
assert.Equal(t, tc.requestBody.Enabled, dbRoute.Enabled)
assert.Equal(t, route.NetID(tc.requestBody.NetworkId), dbRoute.NetID)
}
})
}
}
}
func Test_Routes_Update(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
networkCIDR := "10.0.0.0/24"
peerID := testing_tools.TestPeerId
tt := []struct {
name string
routeId string
requestBody *api.RouteRequest
expectedStatus int
verifyResponse func(t *testing.T, route *api.Route)
}{
{
name: "Update route description",
routeId: "testRouteId",
requestBody: &api.RouteRequest{
Description: "Updated description",
Network: &networkCIDR,
Peer: &peerID,
NetworkId: "testNet",
Metric: 100,
Masquerade: true,
Enabled: true,
Groups: []string{testing_tools.TestGroupId},
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, route *api.Route) {
t.Helper()
assert.Equal(t, "testRouteId", route.Id)
assert.Equal(t, "Updated description", route.Description)
},
},
{
name: "Update route metric",
routeId: "testRouteId",
requestBody: &api.RouteRequest{
Description: "Test Network Route",
Network: &networkCIDR,
Peer: &peerID,
NetworkId: "testNet",
Metric: 500,
Masquerade: true,
Enabled: true,
Groups: []string{testing_tools.TestGroupId},
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, route *api.Route) {
t.Helper()
assert.Equal(t, 500, route.Metric)
},
},
{
name: "Update non-existing route",
routeId: "nonExistingRouteId",
requestBody: &api.RouteRequest{
Description: "whatever",
Network: &networkCIDR,
Peer: &peerID,
NetworkId: "testNet",
Metric: 100,
Enabled: true,
Groups: []string{testing_tools.TestGroupId},
},
expectedStatus: http.StatusNotFound,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/routes.sql", nil, false)
body, err := json.Marshal(tc.requestBody)
if err != nil {
t.Fatalf("Failed to marshal request body: %v", err)
}
req := testing_tools.BuildRequest(t, body, http.MethodPut, strings.Replace("/api/routes/{routeId}", "{routeId}", tc.routeId, 1), user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
if tc.verifyResponse != nil {
got := &api.Route{}
if err := json.Unmarshal(content, got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
tc.verifyResponse(t, got)
// Verify updated route in DB
db := testing_tools.GetDB(t, am.GetStore())
dbRoute := testing_tools.VerifyRouteInDB(t, db, route.ID(got.Id))
assert.Equal(t, tc.requestBody.Description, dbRoute.Description)
assert.Equal(t, tc.requestBody.Metric, dbRoute.Metric)
assert.Equal(t, tc.requestBody.Masquerade, dbRoute.Masquerade)
assert.Equal(t, tc.requestBody.Enabled, dbRoute.Enabled)
}
})
}
}
}
func Test_Routes_Delete(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
tt := []struct {
name string
routeId string
expectedStatus int
}{
{
name: "Delete existing route",
routeId: "testRouteId",
expectedStatus: http.StatusOK,
},
{
name: "Delete non-existing route",
routeId: "nonExistingRouteId",
expectedStatus: http.StatusNotFound,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/routes.sql", nil, false)
req := testing_tools.BuildRequest(t, []byte{}, http.MethodDelete, strings.Replace("/api/routes/{routeId}", "{routeId}", tc.routeId, 1), user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
// Verify route was deleted from DB for successful deletes
if tc.expectedStatus == http.StatusOK && user.expectResponse {
db := testing_tools.GetDB(t, am.GetStore())
testing_tools.VerifyRouteNotInDB(t, db, route.ID(tc.routeId))
}
})
}
}
}

View File

@@ -3,7 +3,6 @@
package integration
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
@@ -14,7 +13,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/netbirdio/netbird/management/server/http/handlers/setup_keys"
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools/channel"
"github.com/netbirdio/netbird/shared/management/http/api"
@@ -254,7 +252,7 @@ func Test_SetupKeys_Create(t *testing.T) {
expectedResponse: nil,
},
{
name: "Create Setup Key",
name: "Create Setup Key with nil AutoGroups",
requestType: http.MethodPost,
requestPath: "/api/setup-keys",
requestBody: &api.CreateSetupKeyRequest{
@@ -308,14 +306,15 @@ func Test_SetupKeys_Create(t *testing.T) {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
gotID := got.Id
validateCreatedKey(t, tc.expectedResponse, got)
key, err := am.GetSetupKey(context.Background(), testing_tools.TestAccountId, testing_tools.TestUserId, got.Id)
if err != nil {
return
}
validateCreatedKey(t, tc.expectedResponse, setup_keys.ToResponseBody(key))
// Verify setup key exists in DB via gorm
db := testing_tools.GetDB(t, am.GetStore())
dbKey := testing_tools.VerifySetupKeyInDB(t, db, gotID)
assert.Equal(t, tc.expectedResponse.Name, dbKey.Name)
assert.Equal(t, tc.expectedResponse.Revoked, dbKey.Revoked)
assert.Equal(t, tc.expectedResponse.UsageLimit, dbKey.UsageLimit)
select {
case <-done:
@@ -571,7 +570,7 @@ func Test_SetupKeys_Update(t *testing.T) {
for _, tc := range tt {
for _, user := range users {
t.Run(tc.name, func(t *testing.T) {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, am, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/setup_keys.sql", nil, true)
body, err := json.Marshal(tc.requestBody)
@@ -594,14 +593,16 @@ func Test_SetupKeys_Update(t *testing.T) {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
gotID := got.Id
gotRevoked := got.Revoked
gotUsageLimit := got.UsageLimit
validateCreatedKey(t, tc.expectedResponse, got)
key, err := am.GetSetupKey(context.Background(), testing_tools.TestAccountId, testing_tools.TestUserId, got.Id)
if err != nil {
return
}
validateCreatedKey(t, tc.expectedResponse, setup_keys.ToResponseBody(key))
// Verify updated setup key in DB via gorm
db := testing_tools.GetDB(t, am.GetStore())
dbKey := testing_tools.VerifySetupKeyInDB(t, db, gotID)
assert.Equal(t, gotRevoked, dbKey.Revoked)
assert.Equal(t, gotUsageLimit, dbKey.UsageLimit)
select {
case <-done:
@@ -759,8 +760,8 @@ func Test_SetupKeys_Get(t *testing.T) {
apiHandler.ServeHTTP(recorder, req)
content, expectRespnose := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectRespnose {
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
got := &api.SetupKey{}
@@ -768,14 +769,16 @@ func Test_SetupKeys_Get(t *testing.T) {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
gotID := got.Id
gotName := got.Name
gotRevoked := got.Revoked
validateCreatedKey(t, tc.expectedResponse, got)
key, err := am.GetSetupKey(context.Background(), testing_tools.TestAccountId, testing_tools.TestUserId, got.Id)
if err != nil {
return
}
validateCreatedKey(t, tc.expectedResponse, setup_keys.ToResponseBody(key))
// Verify setup key in DB via gorm
db := testing_tools.GetDB(t, am.GetStore())
dbKey := testing_tools.VerifySetupKeyInDB(t, db, gotID)
assert.Equal(t, gotName, dbKey.Name)
assert.Equal(t, gotRevoked, dbKey.Revoked)
select {
case <-done:
@@ -928,15 +931,17 @@ func Test_SetupKeys_GetAll(t *testing.T) {
return tc.expectedResponse[i].UsageLimit < tc.expectedResponse[j].UsageLimit
})
db := testing_tools.GetDB(t, am.GetStore())
for i := range tc.expectedResponse {
gotID := got[i].Id
gotName := got[i].Name
gotRevoked := got[i].Revoked
validateCreatedKey(t, tc.expectedResponse[i], &got[i])
key, err := am.GetSetupKey(context.Background(), testing_tools.TestAccountId, testing_tools.TestUserId, got[i].Id)
if err != nil {
return
}
validateCreatedKey(t, tc.expectedResponse[i], setup_keys.ToResponseBody(key))
// Verify each setup key in DB via gorm
dbKey := testing_tools.VerifySetupKeyInDB(t, db, gotID)
assert.Equal(t, gotName, dbKey.Name)
assert.Equal(t, gotRevoked, dbKey.Revoked)
}
select {
@@ -1104,8 +1109,9 @@ func Test_SetupKeys_Delete(t *testing.T) {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
_, err := am.GetSetupKey(context.Background(), testing_tools.TestAccountId, testing_tools.TestUserId, got.Id)
assert.Errorf(t, err, "Expected error when trying to get deleted key")
// Verify setup key deleted from DB via gorm
db := testing_tools.GetDB(t, am.GetStore())
testing_tools.VerifySetupKeyNotInDB(t, db, got.Id)
select {
case <-done:
@@ -1120,7 +1126,7 @@ func Test_SetupKeys_Delete(t *testing.T) {
func validateCreatedKey(t *testing.T, expectedKey *api.SetupKey, got *api.SetupKey) {
t.Helper()
if got.Expires.After(time.Now().Add(-1*time.Minute)) && got.Expires.Before(time.Now().Add(testing_tools.ExpiresIn*time.Second)) ||
if (got.Expires.After(time.Now().Add(-1*time.Minute)) && got.Expires.Before(time.Now().Add(testing_tools.ExpiresIn*time.Second))) ||
got.Expires.After(time.Date(2300, 01, 01, 0, 0, 0, 0, time.Local)) ||
got.Expires.Before(time.Date(1950, 01, 01, 0, 0, 0, 0, time.Local)) {
got.Expires = time.Time{}

View File

@@ -0,0 +1,701 @@
//go:build integration
package integration
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools/channel"
"github.com/netbirdio/netbird/shared/management/http/api"
)
func Test_Users_GetAll(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, true},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, true},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
for _, user := range users {
t.Run(user.name+" - Get all users", func(t *testing.T) {
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/users_integration.sql", nil, true)
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, "/api/users", user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, user.expectResponse)
if !expectResponse {
return
}
got := []api.User{}
if err := json.Unmarshal(content, &got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
assert.GreaterOrEqual(t, len(got), 1)
select {
case <-done:
case <-time.After(time.Second):
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
}
})
}
}
func Test_Users_GetAll_ServiceUsers(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
for _, user := range users {
t.Run(user.name+" - Get all service users", func(t *testing.T) {
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/users_integration.sql", nil, true)
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, "/api/users?service_user=true", user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, user.expectResponse)
if !expectResponse {
return
}
got := []api.User{}
if err := json.Unmarshal(content, &got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
for _, u := range got {
assert.NotNil(t, u.IsServiceUser)
assert.Equal(t, true, *u.IsServiceUser)
}
select {
case <-done:
case <-time.After(time.Second):
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
}
})
}
}
func Test_Users_Create_ServiceUser(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
tt := []struct {
name string
requestBody *api.UserCreateRequest
expectedStatus int
verifyResponse func(t *testing.T, user *api.User)
}{
{
name: "Create service user with admin role",
requestBody: &api.UserCreateRequest{
Role: "admin",
IsServiceUser: true,
AutoGroups: []string{testing_tools.TestGroupId},
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, user *api.User) {
t.Helper()
assert.NotEmpty(t, user.Id)
assert.Equal(t, "admin", user.Role)
assert.NotNil(t, user.IsServiceUser)
assert.Equal(t, true, *user.IsServiceUser)
},
},
{
name: "Create service user with user role",
requestBody: &api.UserCreateRequest{
Role: "user",
IsServiceUser: true,
AutoGroups: []string{testing_tools.TestGroupId},
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, user *api.User) {
t.Helper()
assert.NotEmpty(t, user.Id)
assert.Equal(t, "user", user.Role)
},
},
{
name: "Create service user with empty auto_groups",
requestBody: &api.UserCreateRequest{
Role: "admin",
IsServiceUser: true,
AutoGroups: []string{},
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, user *api.User) {
t.Helper()
assert.NotEmpty(t, user.Id)
},
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, am, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/users_integration.sql", nil, true)
body, err := json.Marshal(tc.requestBody)
if err != nil {
t.Fatalf("Failed to marshal request body: %v", err)
}
req := testing_tools.BuildRequest(t, body, http.MethodPost, "/api/users", user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
if tc.verifyResponse != nil {
got := &api.User{}
if err := json.Unmarshal(content, got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
tc.verifyResponse(t, got)
// Verify user in DB
db := testing_tools.GetDB(t, am.GetStore())
dbUser := testing_tools.VerifyUserInDB(t, db, got.Id)
assert.True(t, dbUser.IsServiceUser)
assert.Equal(t, string(dbUser.Role), string(tc.requestBody.Role))
}
select {
case <-done:
case <-time.After(time.Second):
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
}
})
}
}
}
func Test_Users_Update(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
tt := []struct {
name string
targetUserId string
requestBody *api.UserRequest
expectedStatus int
verifyResponse func(t *testing.T, user *api.User)
}{
{
name: "Update user role to admin",
targetUserId: testing_tools.TestUserId,
requestBody: &api.UserRequest{
Role: "admin",
AutoGroups: []string{},
IsBlocked: false,
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, user *api.User) {
t.Helper()
assert.Equal(t, "admin", user.Role)
},
},
{
name: "Update user auto_groups",
targetUserId: testing_tools.TestUserId,
requestBody: &api.UserRequest{
Role: "user",
AutoGroups: []string{testing_tools.TestGroupId},
IsBlocked: false,
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, user *api.User) {
t.Helper()
assert.Equal(t, 1, len(user.AutoGroups))
},
},
{
name: "Block user",
targetUserId: testing_tools.TestUserId,
requestBody: &api.UserRequest{
Role: "user",
AutoGroups: []string{},
IsBlocked: true,
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, user *api.User) {
t.Helper()
assert.Equal(t, true, user.IsBlocked)
},
},
{
name: "Update non-existing user",
targetUserId: "nonExistingUserId",
requestBody: &api.UserRequest{
Role: "user",
AutoGroups: []string{},
IsBlocked: false,
},
expectedStatus: http.StatusNotFound,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/users_integration.sql", nil, false)
body, err := json.Marshal(tc.requestBody)
if err != nil {
t.Fatalf("Failed to marshal request body: %v", err)
}
req := testing_tools.BuildRequest(t, body, http.MethodPut, strings.Replace("/api/users/{userId}", "{userId}", tc.targetUserId, 1), user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
if tc.verifyResponse != nil {
got := &api.User{}
if err := json.Unmarshal(content, got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
tc.verifyResponse(t, got)
// Verify updated fields in DB
if tc.expectedStatus == http.StatusOK {
db := testing_tools.GetDB(t, am.GetStore())
dbUser := testing_tools.VerifyUserInDB(t, db, tc.targetUserId)
assert.Equal(t, string(dbUser.Role), string(tc.requestBody.Role))
assert.Equal(t, dbUser.Blocked, tc.requestBody.IsBlocked)
assert.ElementsMatch(t, dbUser.AutoGroups, tc.requestBody.AutoGroups)
}
}
})
}
}
}
func Test_Users_Delete(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
tt := []struct {
name string
targetUserId string
expectedStatus int
}{
{
name: "Delete existing service user",
targetUserId: "deletableServiceUserId",
expectedStatus: http.StatusOK,
},
{
name: "Delete non-existing user",
targetUserId: "nonExistingUserId",
expectedStatus: http.StatusNotFound,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, am, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/users_integration.sql", nil, true)
req := testing_tools.BuildRequest(t, []byte{}, http.MethodDelete, strings.Replace("/api/users/{userId}", "{userId}", tc.targetUserId, 1), user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
_, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
// Verify user deleted from DB for successful deletes
if expectResponse && tc.expectedStatus == http.StatusOK {
db := testing_tools.GetDB(t, am.GetStore())
testing_tools.VerifyUserNotInDB(t, db, tc.targetUserId)
}
select {
case <-done:
case <-time.After(time.Second):
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
}
})
}
}
}
func Test_PATs_GetAll(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
for _, user := range users {
t.Run(user.name+" - Get all PATs for service user", func(t *testing.T) {
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/users_integration.sql", nil, true)
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, strings.Replace("/api/users/{userId}/tokens", "{userId}", testing_tools.TestServiceUserId, 1), user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, user.expectResponse)
if !expectResponse {
return
}
got := []api.PersonalAccessToken{}
if err := json.Unmarshal(content, &got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
assert.Equal(t, 1, len(got))
assert.Equal(t, "serviceToken", got[0].Name)
select {
case <-done:
case <-time.After(time.Second):
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
}
})
}
}
func Test_PATs_GetById(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
tt := []struct {
name string
tokenId string
expectedStatus int
expectToken bool
}{
{
name: "Get existing PAT",
tokenId: "serviceTokenId",
expectedStatus: http.StatusOK,
expectToken: true,
},
{
name: "Get non-existing PAT",
tokenId: "nonExistingTokenId",
expectedStatus: http.StatusNotFound,
expectToken: false,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/users_integration.sql", nil, true)
path := strings.Replace("/api/users/{userId}/tokens/{tokenId}", "{userId}", testing_tools.TestServiceUserId, 1)
path = strings.Replace(path, "{tokenId}", tc.tokenId, 1)
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, path, user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
if tc.expectToken {
got := &api.PersonalAccessToken{}
if err := json.Unmarshal(content, got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
assert.Equal(t, "serviceTokenId", got.Id)
assert.Equal(t, "serviceToken", got.Name)
}
select {
case <-done:
case <-time.After(time.Second):
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
}
})
}
}
}
func Test_PATs_Create(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
tt := []struct {
name string
targetUserId string
requestBody *api.PersonalAccessTokenRequest
expectedStatus int
verifyResponse func(t *testing.T, pat *api.PersonalAccessTokenGenerated)
}{
{
name: "Create PAT with 30 day expiry",
targetUserId: testing_tools.TestServiceUserId,
requestBody: &api.PersonalAccessTokenRequest{
Name: "newPAT",
ExpiresIn: 30,
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, pat *api.PersonalAccessTokenGenerated) {
t.Helper()
assert.NotEmpty(t, pat.PlainToken)
assert.Equal(t, "newPAT", pat.PersonalAccessToken.Name)
},
},
{
name: "Create PAT with 365 day expiry",
targetUserId: testing_tools.TestServiceUserId,
requestBody: &api.PersonalAccessTokenRequest{
Name: "longPAT",
ExpiresIn: 365,
},
expectedStatus: http.StatusOK,
verifyResponse: func(t *testing.T, pat *api.PersonalAccessTokenGenerated) {
t.Helper()
assert.NotEmpty(t, pat.PlainToken)
assert.Equal(t, "longPAT", pat.PersonalAccessToken.Name)
},
},
{
name: "Create PAT with empty name",
targetUserId: testing_tools.TestServiceUserId,
requestBody: &api.PersonalAccessTokenRequest{
Name: "",
ExpiresIn: 30,
},
expectedStatus: http.StatusUnprocessableEntity,
},
{
name: "Create PAT with 0 day expiry",
targetUserId: testing_tools.TestServiceUserId,
requestBody: &api.PersonalAccessTokenRequest{
Name: "zeroPAT",
ExpiresIn: 0,
},
expectedStatus: http.StatusUnprocessableEntity,
},
{
name: "Create PAT with expiry over 365 days",
targetUserId: testing_tools.TestServiceUserId,
requestBody: &api.PersonalAccessTokenRequest{
Name: "tooLongPAT",
ExpiresIn: 400,
},
expectedStatus: http.StatusUnprocessableEntity,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, am, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/users_integration.sql", nil, true)
body, err := json.Marshal(tc.requestBody)
if err != nil {
t.Fatalf("Failed to marshal request body: %v", err)
}
req := testing_tools.BuildRequest(t, body, http.MethodPost, strings.Replace("/api/users/{userId}/tokens", "{userId}", tc.targetUserId, 1), user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
if !expectResponse {
return
}
if tc.verifyResponse != nil {
got := &api.PersonalAccessTokenGenerated{}
if err := json.Unmarshal(content, got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
tc.verifyResponse(t, got)
// Verify PAT in DB
db := testing_tools.GetDB(t, am.GetStore())
dbPAT := testing_tools.VerifyPATInDB(t, db, got.PersonalAccessToken.Id)
assert.Equal(t, tc.requestBody.Name, dbPAT.Name)
}
select {
case <-done:
case <-time.After(time.Second):
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
}
})
}
}
}
func Test_PATs_Delete(t *testing.T) {
users := []struct {
name string
userId string
expectResponse bool
}{
{"Regular user", testing_tools.TestUserId, false},
{"Admin user", testing_tools.TestAdminId, true},
{"Owner user", testing_tools.TestOwnerId, true},
{"Regular service user", testing_tools.TestServiceUserId, false},
{"Admin service user", testing_tools.TestServiceAdminId, true},
{"Blocked user", testing_tools.BlockedUserId, false},
{"Other user", testing_tools.OtherUserId, false},
{"Invalid token", testing_tools.InvalidToken, false},
}
tt := []struct {
name string
tokenId string
expectedStatus int
}{
{
name: "Delete existing PAT",
tokenId: "serviceTokenId",
expectedStatus: http.StatusOK,
},
{
name: "Delete non-existing PAT",
tokenId: "nonExistingTokenId",
expectedStatus: http.StatusNotFound,
},
}
for _, tc := range tt {
for _, user := range users {
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
apiHandler, am, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/users_integration.sql", nil, true)
path := strings.Replace("/api/users/{userId}/tokens/{tokenId}", "{userId}", testing_tools.TestServiceUserId, 1)
path = strings.Replace(path, "{tokenId}", tc.tokenId, 1)
req := testing_tools.BuildRequest(t, []byte{}, http.MethodDelete, path, user.userId)
recorder := httptest.NewRecorder()
apiHandler.ServeHTTP(recorder, req)
_, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
// Verify PAT deleted from DB for successful deletes
if expectResponse && tc.expectedStatus == http.StatusOK {
db := testing_tools.GetDB(t, am.GetStore())
testing_tools.VerifyPATNotInDB(t, db, tc.tokenId)
}
select {
case <-done:
case <-time.After(time.Second):
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
}
})
}
}
}

View File

@@ -0,0 +1,18 @@
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 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 `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 `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,'[]',1,86400000000000,0,0,0,'',NULL,NULL,NULL);
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 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 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);

View File

@@ -0,0 +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 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 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 `name_server_groups` (`id` text,`account_id` text,`name` text,`description` text,`name_servers` text,`groups` text,`primary` numeric,`domains` text,`enabled` numeric,`search_domains_enabled` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_name_server_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,'[]',1,86400000000000,0,0,0,'',NULL,NULL,NULL);
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','["testPeerId"]',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,NULL,'["testGroupId"]',1,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 name_server_groups VALUES('testNSGroupId','testAccountId','testNSGroup','test nameserver group','[{"IP":"1.1.1.1","NSType":1,"Port":53}]','["testGroupId"]',0,'["example.com"]',1,0);

View File

@@ -0,0 +1,18 @@
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 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 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,'[]',1,86400000000000,0,0,0,'',NULL,NULL,NULL);
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,NULL,'["testGroupId"]',1,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);

View File

@@ -0,0 +1,19 @@
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 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 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,'[]',1,86400000000000,0,0,0,'',NULL,NULL,NULL);
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','["testPeerId"]',0,'');
INSERT INTO "groups" VALUES('newGroupId','testAccountId','newGroupName','api','[]',0,'');
INSERT INTO "groups" VALUES('allGroupId','testAccountId','All','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,NULL,'["testGroupId"]',1,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);

View File

@@ -0,0 +1,25 @@
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 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 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 `networks` (`id` text,`account_id` text,`name` text,`description` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_networks` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
CREATE TABLE `network_routers` (`id` text,`network_id` text,`account_id` text,`peer` text,`peer_groups` text,`masquerade` numeric,`metric` integer,`enabled` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_network_routers` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
CREATE TABLE `network_resources` (`id` text,`network_id` text,`account_id` text,`name` text,`description` text,`type` text,`domain` text,`prefix` text,`enabled` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_network_resources` 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,'[]',1,86400000000000,0,0,0,'',NULL,NULL,NULL);
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,'testServiceUser','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
INSERT INTO users VALUES('testServiceAdminId','testAccountId','admin',1,0,'testServiceAdmin','[]',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','["testPeerId"]',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:00',0,0,NULL,'["testGroupId"]',1,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 networks VALUES('testNetworkId','testAccountId','testNetwork','test network description');
INSERT INTO network_routers VALUES('testRouterId','testNetworkId','testAccountId','testPeerId','[]',1,100,1);
INSERT INTO network_resources VALUES('testResourceId','testNetworkId','testAccountId','testResource','test resource description','host','','"3.3.3.3/32"',1);

View File

@@ -0,0 +1,20 @@
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 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 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 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','["testPeerId","testPeerId2"]',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,NULL,'["testGroupId"]',1,0);
INSERT INTO peers VALUES('testPeerId','testAccountId','5rvhvriKJZ3S9oxYToVj5TzDM9u9y8cxg7htIMWlYAg=','72546A29-6BC8-4311-BCFC-9CDBF33F1A48','"100.64.114.31"','test-host-1','linux','Linux','','unknown','Debian GNU/Linux','','0.12.0','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'test-peer-1','test-peer-1','2023-03-02 09:21:02.189035775+01:00',0,0,0,'testUserId','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('testPeerId2','testAccountId','6rvhvriKJZ3S9oxYToVj5TzDM9u9y8cxg7htIMWlYBg=','82546A29-6BC8-4311-BCFC-9CDBF33F1A49','"100.64.114.32"','test-host-2','linux','Linux','','unknown','Ubuntu','','0.12.0','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'test-peer-2','test-peer-2','2023-03-02 09:21:02.189035775+01:00',1,0,0,'testAdminId','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILzUUSYG/LGnV8zarb2SGN+tib/PZ+M7cL4WtTzUrTpk',1,0,'2023-03-01 19:48:19.817799698+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0);

View File

@@ -0,0 +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 `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 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 `policies` (`id` text,`account_id` text,`name` text,`description` text,`enabled` numeric,`source_posture_checks` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_policies_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
CREATE TABLE `policy_rules` (`id` text,`policy_id` text,`name` text,`description` text,`enabled` numeric,`action` text,`protocol` text,`bidirectional` numeric,`sources` text,`destinations` text,`source_resource` text,`destination_resource` text,`ports` text,`port_ranges` text,`authorized_groups` text,`authorized_user` text,PRIMARY KEY (`id`),CONSTRAINT `fk_policies_rules_g` FOREIGN KEY (`policy_id`) REFERENCES `policies`(`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,'[]',1,86400000000000,0,0,0,'',NULL,NULL,NULL);
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','["testPeerId"]',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,NULL,'["testGroupId"]',1,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 policies VALUES('testPolicyId','testAccountId','testPolicy','test policy description',1,NULL);
INSERT INTO policy_rules VALUES('testRuleId','testPolicyId','testRule','test rule',1,'accept','all',1,'["testGroupId"]','["testGroupId"]',NULL,NULL,NULL,NULL,NULL,'');

View File

@@ -0,0 +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 `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 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 `routes` (`id` text,`account_id` text,`network` text,`domains` text,`keep_route` numeric,`net_id` text,`description` text,`peer` text,`peer_groups` text,`network_type` integer,`masquerade` numeric,`metric` integer,`enabled` numeric,`groups` text,`access_control_groups` text,`skip_auto_apply` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_routes_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,'[]',1,86400000000000,0,0,0,'',NULL,NULL,NULL);
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','["testPeerId"]',0,'');
INSERT INTO "groups" VALUES('newGroupId','testAccountId','newGroupName','api','[]',0,'');
INSERT INTO "groups" VALUES('peerGroupId','testAccountId','peerGroupName','api','["testPeerId"]',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,NULL,'["testGroupId"]',1,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 routes VALUES('testRouteId','testAccountId','"10.0.0.0/24"',NULL,0,'testNet','Test Network Route','testPeerId',NULL,1,1,100,1,'["testGroupId"]',NULL,0);
INSERT INTO routes VALUES('testDomainRouteId','testAccountId','"0.0.0.0/0"','["example.com"]',0,'testDomainNet','Test Domain Route','','["peerGroupId"]',3,1,200,1,'["testGroupId"]',NULL,0);

View File

@@ -0,0 +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 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 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 `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 INDEX `idx_personal_access_tokens_user_id` ON `personal_access_tokens`(`user_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,'[]',1,86400000000000,0,0,0,'',NULL,NULL,NULL);
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,'testServiceUser','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
INSERT INTO users VALUES('testServiceAdminId','testAccountId','admin',1,0,'testServiceAdmin','[]',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 users VALUES('deletableServiceUserId','testAccountId','user',1,0,'deletableServiceUser','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
INSERT INTO "groups" VALUES('testGroupId','testAccountId','testGroupName','api','["testPeerId"]',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,NULL,'["testGroupId"]',1,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 personal_access_tokens VALUES('testTokenId','testUserId','testToken','hashedTokenValue123','2325-10-02 16:01:38.000000000+00:00','testUserId','2024-10-02 16:01:38.000000000+00:00',NULL);
INSERT INTO personal_access_tokens VALUES('serviceTokenId','testServiceUserId','serviceToken','hashedServiceTokenValue123','2325-10-02 16:01:38.000000000+00:00','testAdminId','2024-10-02 16:01:38.000000000+00:00',NULL);

View File

@@ -114,13 +114,12 @@ func BuildApiBlackBoxWithDBState(t testing_tools.TB, sqlFile string, expectedPee
if err != nil {
t.Fatalf("Failed to create proxy controller: %v", err)
}
domainManager.SetClusterCapabilities(serviceProxyController)
serviceManager := reverseproxymanager.NewManager(store, am, permissionsManager, serviceProxyController, domainManager)
serviceManager := reverseproxymanager.NewManager(store, am, permissionsManager, serviceProxyController, proxyMgr, domainManager)
proxyServiceServer.SetServiceManager(serviceManager)
am.SetServiceManager(serviceManager)
// @note this is required so that PAT's validate from store, but JWT's are mocked
authManager := serverauth.NewManager(store, "", "", "", "", []string{}, false)
authManager := serverauth.NewManager(store, "", "", "", "", []string{}, false, nil)
authManagerMock := &serverauth.MockManager{
ValidateAndParseTokenFunc: mockValidateAndParseToken,
EnsureUserAccessByJWTGroupsFunc: authManager.EnsureUserAccessByJWTGroups,
@@ -128,14 +127,14 @@ func BuildApiBlackBoxWithDBState(t testing_tools.TB, sqlFile string, expectedPee
GetPATInfoFunc: authManager.GetPATInfo,
}
networksManagerMock := networks.NewManagerMock()
resourcesManagerMock := resources.NewManagerMock()
routersManagerMock := routers.NewManagerMock()
groupsManagerMock := groups.NewManagerMock()
groupsManager := groups.NewManager(store, permissionsManager, am)
routersManager := routers.NewManager(store, permissionsManager, am)
resourcesManager := resources.NewManager(store, permissionsManager, groupsManager, am, serviceManager)
networksManager := networks.NewManager(store, permissionsManager, resourcesManager, routersManager, am)
customZonesManager := zonesManager.NewManager(store, am, permissionsManager, "")
zoneRecordsManager := recordsManager.NewManager(store, am, permissionsManager)
apiHandler, err := http2.NewAPIHandler(context.Background(), am, networksManagerMock, resourcesManagerMock, routersManagerMock, groupsManagerMock, geoMock, authManagerMock, metrics, validatorMock, proxyController, permissionsManager, peersManager, settingsManager, customZonesManager, zoneRecordsManager, networkMapController, nil, serviceManager, nil, nil, nil, nil)
apiHandler, err := http2.NewAPIHandler(context.Background(), am, networksManager, resourcesManager, routersManager, groupsManager, geoMock, authManagerMock, metrics, validatorMock, proxyController, permissionsManager, peersManager, settingsManager, customZonesManager, zoneRecordsManager, networkMapController, nil, serviceManager, nil, nil, nil, nil)
if err != nil {
t.Fatalf("Failed to create API handler: %v", err)
}
@@ -167,6 +166,111 @@ func peerShouldReceiveUpdate(t testing_tools.TB, updateMessage <-chan *network_m
}
}
// PeerShouldReceiveAnyUpdate waits for a peer update message and returns it.
// Fails the test if no update is received within timeout.
func PeerShouldReceiveAnyUpdate(t testing_tools.TB, updateMessage <-chan *network_map.UpdateMessage) *network_map.UpdateMessage {
t.Helper()
select {
case msg := <-updateMessage:
if msg == nil {
t.Errorf("Received nil update message, expected valid message")
}
return msg
case <-time.After(500 * time.Millisecond):
t.Errorf("Timed out waiting for update message")
return nil
}
}
// PeerShouldNotReceiveAnyUpdate verifies no peer update message is received.
func PeerShouldNotReceiveAnyUpdate(t testing_tools.TB, updateMessage <-chan *network_map.UpdateMessage) {
t.Helper()
peerShouldNotReceiveUpdate(t, updateMessage)
}
// BuildApiBlackBoxWithDBStateAndPeerChannel creates the API handler and returns
// the peer update channel directly so tests can verify updates inline.
func BuildApiBlackBoxWithDBStateAndPeerChannel(t testing_tools.TB, sqlFile string) (http.Handler, account.Manager, <-chan *network_map.UpdateMessage) {
store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), sqlFile, t.TempDir())
if err != nil {
t.Fatalf("Failed to create test store: %v", err)
}
t.Cleanup(cleanup)
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
if err != nil {
t.Fatalf("Failed to create metrics: %v", err)
}
peersUpdateManager := update_channel.NewPeersUpdateManager(nil)
updMsg := peersUpdateManager.CreateChannel(context.Background(), testing_tools.TestPeerId)
geoMock := &geolocation.Mock{}
validatorMock := server.MockIntegratedValidator{}
proxyController := integrations.NewController(store)
userManager := users.NewManager(store)
permissionsManager := permissions.NewManager(store)
settingsManager := settings.NewManager(store, userManager, integrations.NewManager(&activity.InMemoryEventStore{}), permissionsManager, settings.IdpConfig{})
peersManager := peers.NewManager(store, permissionsManager)
jobManager := job.NewJobManager(nil, store, peersManager)
ctx := context.Background()
requestBuffer := server.NewAccountRequestBuffer(ctx, store)
networkMapController := controller.NewController(ctx, store, metrics, peersUpdateManager, requestBuffer, server.MockIntegratedValidator{}, settingsManager, "", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManager), &config.Config{})
am, err := server.BuildManager(ctx, nil, store, networkMapController, jobManager, nil, "", &activity.InMemoryEventStore{}, geoMock, false, validatorMock, metrics, proxyController, settingsManager, permissionsManager, false)
if err != nil {
t.Fatalf("Failed to create manager: %v", err)
}
accessLogsManager := accesslogsmanager.NewManager(store, permissionsManager, nil)
proxyTokenStore, err := nbgrpc.NewOneTimeTokenStore(ctx, 5*time.Minute, 10*time.Minute, 100)
if err != nil {
t.Fatalf("Failed to create proxy token store: %v", err)
}
pkceverifierStore, err := nbgrpc.NewPKCEVerifierStore(ctx, 10*time.Minute, 10*time.Minute, 100)
if err != nil {
t.Fatalf("Failed to create PKCE verifier store: %v", err)
}
noopMeter := noop.NewMeterProvider().Meter("")
proxyMgr, err := proxymanager.NewManager(store, noopMeter)
if err != nil {
t.Fatalf("Failed to create proxy manager: %v", err)
}
proxyServiceServer := nbgrpc.NewProxyServiceServer(accessLogsManager, proxyTokenStore, pkceverifierStore, nbgrpc.ProxyOIDCConfig{}, peersManager, userManager, proxyMgr)
domainManager := manager.NewManager(store, proxyMgr, permissionsManager, am)
serviceProxyController, err := proxymanager.NewGRPCController(proxyServiceServer, noopMeter)
if err != nil {
t.Fatalf("Failed to create proxy controller: %v", err)
}
serviceManager := reverseproxymanager.NewManager(store, am, permissionsManager, serviceProxyController, proxyMgr, domainManager)
proxyServiceServer.SetServiceManager(serviceManager)
am.SetServiceManager(serviceManager)
// @note this is required so that PAT's validate from store, but JWT's are mocked
authManager := serverauth.NewManager(store, "", "", "", "", []string{}, false, nil)
authManagerMock := &serverauth.MockManager{
ValidateAndParseTokenFunc: mockValidateAndParseToken,
EnsureUserAccessByJWTGroupsFunc: authManager.EnsureUserAccessByJWTGroups,
MarkPATUsedFunc: authManager.MarkPATUsed,
GetPATInfoFunc: authManager.GetPATInfo,
}
groupsManager := groups.NewManager(store, permissionsManager, am)
routersManager := routers.NewManager(store, permissionsManager, am)
resourcesManager := resources.NewManager(store, permissionsManager, groupsManager, am, serviceManager)
networksManager := networks.NewManager(store, permissionsManager, resourcesManager, routersManager, am)
customZonesManager := zonesManager.NewManager(store, am, permissionsManager, "")
zoneRecordsManager := recordsManager.NewManager(store, am, permissionsManager)
apiHandler, err := http2.NewAPIHandler(context.Background(), am, networksManager, resourcesManager, routersManager, groupsManager, geoMock, authManagerMock, metrics, validatorMock, proxyController, permissionsManager, peersManager, settingsManager, customZonesManager, zoneRecordsManager, networkMapController, nil, serviceManager, nil, nil, nil, nil)
if err != nil {
t.Fatalf("Failed to create API handler: %v", err)
}
return apiHandler, am, updMsg
}
func mockValidateAndParseToken(_ context.Context, token string) (auth.UserAuth, *jwt.Token, error) {
userAuth := auth.UserAuth{}

View File

@@ -0,0 +1,222 @@
package testing_tools
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
nbdns "github.com/netbirdio/netbird/dns"
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/route"
)
// GetDB extracts the *gorm.DB from a store.Store (must be *SqlStore).
func GetDB(t *testing.T, s store.Store) *gorm.DB {
t.Helper()
sqlStore, ok := s.(*store.SqlStore)
require.True(t, ok, "Store is not a *SqlStore, cannot get gorm.DB")
return sqlStore.GetDB()
}
// VerifyGroupInDB reads a group directly from the DB and returns it.
func VerifyGroupInDB(t *testing.T, db *gorm.DB, groupID string) *types.Group {
t.Helper()
var group types.Group
err := db.Where("id = ? AND account_id = ?", groupID, TestAccountId).First(&group).Error
require.NoError(t, err, "Expected group %s to exist in DB", groupID)
return &group
}
// VerifyGroupNotInDB verifies that a group does not exist in the DB.
func VerifyGroupNotInDB(t *testing.T, db *gorm.DB, groupID string) {
t.Helper()
var count int64
db.Model(&types.Group{}).Where("id = ? AND account_id = ?", groupID, TestAccountId).Count(&count)
assert.Equal(t, int64(0), count, "Expected group %s to NOT exist in DB", groupID)
}
// VerifyPolicyInDB reads a policy directly from the DB and returns it.
func VerifyPolicyInDB(t *testing.T, db *gorm.DB, policyID string) *types.Policy {
t.Helper()
var policy types.Policy
err := db.Preload("Rules").Where("id = ? AND account_id = ?", policyID, TestAccountId).First(&policy).Error
require.NoError(t, err, "Expected policy %s to exist in DB", policyID)
return &policy
}
// VerifyPolicyNotInDB verifies that a policy does not exist in the DB.
func VerifyPolicyNotInDB(t *testing.T, db *gorm.DB, policyID string) {
t.Helper()
var count int64
db.Model(&types.Policy{}).Where("id = ? AND account_id = ?", policyID, TestAccountId).Count(&count)
assert.Equal(t, int64(0), count, "Expected policy %s to NOT exist in DB", policyID)
}
// VerifyRouteInDB reads a route directly from the DB and returns it.
func VerifyRouteInDB(t *testing.T, db *gorm.DB, routeID route.ID) *route.Route {
t.Helper()
var r route.Route
err := db.Where("id = ? AND account_id = ?", routeID, TestAccountId).First(&r).Error
require.NoError(t, err, "Expected route %s to exist in DB", routeID)
return &r
}
// VerifyRouteNotInDB verifies that a route does not exist in the DB.
func VerifyRouteNotInDB(t *testing.T, db *gorm.DB, routeID route.ID) {
t.Helper()
var count int64
db.Model(&route.Route{}).Where("id = ? AND account_id = ?", routeID, TestAccountId).Count(&count)
assert.Equal(t, int64(0), count, "Expected route %s to NOT exist in DB", routeID)
}
// VerifyNSGroupInDB reads a nameserver group directly from the DB and returns it.
func VerifyNSGroupInDB(t *testing.T, db *gorm.DB, nsGroupID string) *nbdns.NameServerGroup {
t.Helper()
var nsGroup nbdns.NameServerGroup
err := db.Where("id = ? AND account_id = ?", nsGroupID, TestAccountId).First(&nsGroup).Error
require.NoError(t, err, "Expected NS group %s to exist in DB", nsGroupID)
return &nsGroup
}
// VerifyNSGroupNotInDB verifies that a nameserver group does not exist in the DB.
func VerifyNSGroupNotInDB(t *testing.T, db *gorm.DB, nsGroupID string) {
t.Helper()
var count int64
db.Model(&nbdns.NameServerGroup{}).Where("id = ? AND account_id = ?", nsGroupID, TestAccountId).Count(&count)
assert.Equal(t, int64(0), count, "Expected NS group %s to NOT exist in DB", nsGroupID)
}
// VerifyPeerInDB reads a peer directly from the DB and returns it.
func VerifyPeerInDB(t *testing.T, db *gorm.DB, peerID string) *nbpeer.Peer {
t.Helper()
var peer nbpeer.Peer
err := db.Where("id = ? AND account_id = ?", peerID, TestAccountId).First(&peer).Error
require.NoError(t, err, "Expected peer %s to exist in DB", peerID)
return &peer
}
// VerifyPeerNotInDB verifies that a peer does not exist in the DB.
func VerifyPeerNotInDB(t *testing.T, db *gorm.DB, peerID string) {
t.Helper()
var count int64
db.Model(&nbpeer.Peer{}).Where("id = ? AND account_id = ?", peerID, TestAccountId).Count(&count)
assert.Equal(t, int64(0), count, "Expected peer %s to NOT exist in DB", peerID)
}
// VerifySetupKeyInDB reads a setup key directly from the DB and returns it.
func VerifySetupKeyInDB(t *testing.T, db *gorm.DB, keyID string) *types.SetupKey {
t.Helper()
var key types.SetupKey
err := db.Where("id = ? AND account_id = ?", keyID, TestAccountId).First(&key).Error
require.NoError(t, err, "Expected setup key %s to exist in DB", keyID)
return &key
}
// VerifySetupKeyNotInDB verifies that a setup key does not exist in the DB.
func VerifySetupKeyNotInDB(t *testing.T, db *gorm.DB, keyID string) {
t.Helper()
var count int64
db.Model(&types.SetupKey{}).Where("id = ? AND account_id = ?", keyID, TestAccountId).Count(&count)
assert.Equal(t, int64(0), count, "Expected setup key %s to NOT exist in DB", keyID)
}
// VerifyUserInDB reads a user directly from the DB and returns it.
func VerifyUserInDB(t *testing.T, db *gorm.DB, userID string) *types.User {
t.Helper()
var user types.User
err := db.Where("id = ? AND account_id = ?", userID, TestAccountId).First(&user).Error
require.NoError(t, err, "Expected user %s to exist in DB", userID)
return &user
}
// VerifyUserNotInDB verifies that a user does not exist in the DB.
func VerifyUserNotInDB(t *testing.T, db *gorm.DB, userID string) {
t.Helper()
var count int64
db.Model(&types.User{}).Where("id = ? AND account_id = ?", userID, TestAccountId).Count(&count)
assert.Equal(t, int64(0), count, "Expected user %s to NOT exist in DB", userID)
}
// VerifyPATInDB reads a PAT directly from the DB and returns it.
func VerifyPATInDB(t *testing.T, db *gorm.DB, tokenID string) *types.PersonalAccessToken {
t.Helper()
var pat types.PersonalAccessToken
err := db.Where("id = ?", tokenID).First(&pat).Error
require.NoError(t, err, "Expected PAT %s to exist in DB", tokenID)
return &pat
}
// VerifyPATNotInDB verifies that a PAT does not exist in the DB.
func VerifyPATNotInDB(t *testing.T, db *gorm.DB, tokenID string) {
t.Helper()
var count int64
db.Model(&types.PersonalAccessToken{}).Where("id = ?", tokenID).Count(&count)
assert.Equal(t, int64(0), count, "Expected PAT %s to NOT exist in DB", tokenID)
}
// VerifyAccountSettings reads the account and returns its settings from the DB.
func VerifyAccountSettings(t *testing.T, db *gorm.DB) *types.Account {
t.Helper()
var account types.Account
err := db.Where("id = ?", TestAccountId).First(&account).Error
require.NoError(t, err, "Expected account %s to exist in DB", TestAccountId)
return &account
}
// VerifyNetworkInDB reads a network directly from the store and returns it.
func VerifyNetworkInDB(t *testing.T, db *gorm.DB, networkID string) *networkTypes.Network {
t.Helper()
var network networkTypes.Network
err := db.Where("id = ? AND account_id = ?", networkID, TestAccountId).First(&network).Error
require.NoError(t, err, "Expected network %s to exist in DB", networkID)
return &network
}
// VerifyNetworkNotInDB verifies that a network does not exist in the DB.
func VerifyNetworkNotInDB(t *testing.T, db *gorm.DB, networkID string) {
t.Helper()
var count int64
db.Model(&networkTypes.Network{}).Where("id = ? AND account_id = ?", networkID, TestAccountId).Count(&count)
assert.Equal(t, int64(0), count, "Expected network %s to NOT exist in DB", networkID)
}
// VerifyNetworkResourceInDB reads a network resource directly from the DB and returns it.
func VerifyNetworkResourceInDB(t *testing.T, db *gorm.DB, resourceID string) *resourceTypes.NetworkResource {
t.Helper()
var resource resourceTypes.NetworkResource
err := db.Where("id = ? AND account_id = ?", resourceID, TestAccountId).First(&resource).Error
require.NoError(t, err, "Expected network resource %s to exist in DB", resourceID)
return &resource
}
// VerifyNetworkResourceNotInDB verifies that a network resource does not exist in the DB.
func VerifyNetworkResourceNotInDB(t *testing.T, db *gorm.DB, resourceID string) {
t.Helper()
var count int64
db.Model(&resourceTypes.NetworkResource{}).Where("id = ? AND account_id = ?", resourceID, TestAccountId).Count(&count)
assert.Equal(t, int64(0), count, "Expected network resource %s to NOT exist in DB", resourceID)
}
// VerifyNetworkRouterInDB reads a network router directly from the DB and returns it.
func VerifyNetworkRouterInDB(t *testing.T, db *gorm.DB, routerID string) *routerTypes.NetworkRouter {
t.Helper()
var router routerTypes.NetworkRouter
err := db.Where("id = ? AND account_id = ?", routerID, TestAccountId).First(&router).Error
require.NoError(t, err, "Expected network router %s to exist in DB", routerID)
return &router
}
// VerifyNetworkRouterNotInDB verifies that a network router does not exist in the DB.
func VerifyNetworkRouterNotInDB(t *testing.T, db *gorm.DB, routerID string) {
t.Helper()
var count int64
db.Model(&routerTypes.NetworkRouter{}).Where("id = ? AND account_id = ?", routerID, TestAccountId).Count(&count)
assert.Equal(t, int64(0), count, "Expected network router %s to NOT exist in DB", routerID)
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/netbirdio/netbird/idp/dex"
"github.com/netbirdio/netbird/management/server/telemetry"
nbjwt "github.com/netbirdio/netbird/shared/auth/jwt"
)
const (
@@ -48,6 +49,8 @@ type EmbeddedIdPConfig struct {
// Existing local users are preserved and will be able to login again if re-enabled.
// Cannot be enabled if no external identity provider connectors are configured.
LocalAuthDisabled bool
// StaticConnectors are additional connectors to seed during initialization
StaticConnectors []dex.Connector
}
// EmbeddedStorageConfig holds storage configuration for the embedded IdP.
@@ -157,6 +160,7 @@ func (c *EmbeddedIdPConfig) ToYAMLConfig() (*dex.YAMLConfig, error) {
RedirectURIs: cliRedirectURIs,
},
},
StaticConnectors: c.StaticConnectors,
}
// Add owner user if provided
@@ -193,6 +197,9 @@ type OAuthConfigProvider interface {
// Management server has embedded Dex and can validate tokens via localhost,
// avoiding external network calls and DNS resolution issues during startup.
GetLocalKeysLocation() string
// GetKeyFetcher returns a KeyFetcher that reads keys directly from the IDP storage,
// or nil if direct key fetching is not supported (falls back to HTTP).
GetKeyFetcher() nbjwt.KeyFetcher
GetClientIDs() []string
GetUserIDClaim() string
GetTokenEndpoint() string
@@ -593,6 +600,11 @@ func (m *EmbeddedIdPManager) GetCLIRedirectURLs() []string {
return m.config.CLIRedirectURIs
}
// GetKeyFetcher returns a KeyFetcher that reads keys directly from Dex storage.
func (m *EmbeddedIdPManager) GetKeyFetcher() nbjwt.KeyFetcher {
return m.provider.GetJWKS
}
// GetKeysLocation returns the JWKS endpoint URL for token validation.
func (m *EmbeddedIdPManager) GetKeysLocation() string {
return m.provider.GetKeysLocation()

View File

@@ -0,0 +1,235 @@
// Package migration provides utility functions for migrating from the external IdP solution in pre v0.62.0
// to the new embedded IdP manager (Dex based), which is the default in v0.62.0 and later.
// It includes functions to seed connectors and migrate existing users to use these connectors.
package migration
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"os"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/idp/dex"
"github.com/netbirdio/netbird/management/server/idp"
"github.com/netbirdio/netbird/management/server/types"
)
// Server is the dependency interface that migration functions use to access
// the main data store and the activity event store.
type Server interface {
Store() Store
EventStore() EventStore // may return nil
}
const idpSeedInfoKey = "IDP_SEED_INFO"
const dryRunEnvKey = "NB_IDP_MIGRATION_DRY_RUN"
func isDryRun() bool {
return os.Getenv(dryRunEnvKey) == "true"
}
var ErrNoSeedInfo = errors.New("no seed info found in environment")
// SeedConnectorFromEnv reads the IDP_SEED_INFO env var, base64-decodes it,
// and JSON-unmarshals it into a dex.Connector. Returns nil if not set.
func SeedConnectorFromEnv() (*dex.Connector, error) {
val, ok := os.LookupEnv(idpSeedInfoKey)
if !ok || val == "" {
return nil, ErrNoSeedInfo
}
decoded, err := base64.StdEncoding.DecodeString(val)
if err != nil {
return nil, fmt.Errorf("base64 decode: %w", err)
}
var conn dex.Connector
if err := json.Unmarshal(decoded, &conn); err != nil {
return nil, fmt.Errorf("json unmarshal: %w", err)
}
return &conn, nil
}
// MigrateUsersToStaticConnectors re-keys every user ID in the main store (and
// the activity store, if present) so that it encodes the given connector ID,
// skipping users that have already been migrated. Set NB_IDP_MIGRATION_DRY_RUN=true
// to log what would happen without writing any changes.
func MigrateUsersToStaticConnectors(s Server, conn *dex.Connector) error {
ctx := context.Background()
if isDryRun() {
log.Info("[DRY RUN] migration dry-run mode enabled, no changes will be written")
}
users, err := s.Store().ListUsers(ctx)
if err != nil {
return fmt.Errorf("failed to list users: %w", err)
}
// Reconciliation pass: fix activity store for users already migrated in main DB
// but whose activity references may still use old IDs (from a previous partial failure).
if s.EventStore() != nil && !isDryRun() {
if err := reconcileActivityStore(ctx, s.EventStore(), users); err != nil {
return err
}
}
var migratedCount, skippedCount int
for _, user := range users {
_, _, decErr := dex.DecodeDexUserID(user.Id)
if decErr == nil {
skippedCount++
continue
}
newUserID := dex.EncodeDexUserID(user.Id, conn.ID)
if isDryRun() {
log.Infof("[DRY RUN] would migrate user %s -> %s (account: %s)", user.Id, newUserID, user.AccountID)
migratedCount++
continue
}
if err := migrateUser(ctx, s, user.Id, user.AccountID, newUserID); err != nil {
return err
}
migratedCount++
}
if isDryRun() {
log.Infof("[DRY RUN] migration summary: %d users would be migrated, %d already migrated", migratedCount, skippedCount)
} else {
log.Infof("migration complete: %d users migrated, %d already migrated", migratedCount, skippedCount)
}
return nil
}
// reconcileActivityStore updates activity store references for users already migrated
// in the main DB whose activity entries may still use old IDs from a previous partial failure.
func reconcileActivityStore(ctx context.Context, eventStore EventStore, users []*types.User) error {
for _, user := range users {
originalID, _, err := dex.DecodeDexUserID(user.Id)
if err != nil {
// skip users that aren't migrated, they will be handled in the main migration loop
continue
}
if err := eventStore.UpdateUserID(ctx, originalID, user.Id); err != nil {
return fmt.Errorf("reconcile activity store for user %s: %w", user.Id, err)
}
}
return nil
}
// migrateUser updates a single user's ID in both the main store and the activity store.
func migrateUser(ctx context.Context, s Server, oldID, accountID, newID string) error {
if err := s.Store().UpdateUserID(ctx, accountID, oldID, newID); err != nil {
return fmt.Errorf("failed to update user ID for user %s: %w", oldID, err)
}
if s.EventStore() == nil {
return nil
}
if err := s.EventStore().UpdateUserID(ctx, oldID, newID); err != nil {
return fmt.Errorf("failed to update activity store user ID for user %s: %w", oldID, err)
}
return nil
}
// PopulateUserInfo fetches user email and name from the external IDP and updates
// the store for users that are missing this information.
func PopulateUserInfo(s Server, idpManager idp.Manager, dryRun bool) error {
ctx := context.Background()
users, err := s.Store().ListUsers(ctx)
if err != nil {
return fmt.Errorf("failed to list users: %w", err)
}
// Build a map of IDP user ID -> UserData from the external IDP
allAccounts, err := idpManager.GetAllAccounts(ctx)
if err != nil {
return fmt.Errorf("failed to fetch accounts from IDP: %w", err)
}
idpUsers := make(map[string]*idp.UserData)
for _, accountUsers := range allAccounts {
for _, userData := range accountUsers {
idpUsers[userData.ID] = userData
}
}
log.Infof("fetched %d users from IDP", len(idpUsers))
var updatedCount, skippedCount, notFoundCount int
for _, user := range users {
if user.IsServiceUser {
skippedCount++
continue
}
if user.Email != "" && user.Name != "" {
skippedCount++
continue
}
// The user ID in the store may be the original IDP ID or a Dex-encoded ID.
// Try to decode the Dex format first to get the original IDP ID.
lookupID := user.Id
if originalID, _, decErr := dex.DecodeDexUserID(user.Id); decErr == nil {
lookupID = originalID
}
idpUser, found := idpUsers[lookupID]
if !found {
notFoundCount++
log.Debugf("user %s (lookup: %s) not found in IDP, skipping", user.Id, lookupID)
continue
}
email := user.Email
name := user.Name
if email == "" && idpUser.Email != "" {
email = idpUser.Email
}
if name == "" && idpUser.Name != "" {
name = idpUser.Name
}
if email == user.Email && name == user.Name {
skippedCount++
continue
}
if dryRun {
log.Infof("[DRY RUN] would update user %s: email=%q, name=%q", user.Id, email, name)
updatedCount++
continue
}
if err := s.Store().UpdateUserInfo(ctx, user.Id, email, name); err != nil {
return fmt.Errorf("failed to update user info for %s: %w", user.Id, err)
}
log.Infof("updated user %s: email=%q, name=%q", user.Id, email, name)
updatedCount++
}
if dryRun {
log.Infof("[DRY RUN] user info summary: %d would be updated, %d skipped, %d not found in IDP", updatedCount, skippedCount, notFoundCount)
} else {
log.Infof("user info population complete: %d updated, %d skipped, %d not found in IDP", updatedCount, skippedCount, notFoundCount)
}
return nil
}

View File

@@ -0,0 +1,828 @@
package migration
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/idp/dex"
"github.com/netbirdio/netbird/management/server/idp"
"github.com/netbirdio/netbird/management/server/types"
)
// testStore is a hand-written mock for MigrationStore.
type testStore struct {
listUsersFunc func(ctx context.Context) ([]*types.User, error)
updateUserIDFunc func(ctx context.Context, accountID, oldUserID, newUserID string) error
updateUserInfoFunc func(ctx context.Context, userID, email, name string) error
checkSchemaFunc func(checks []SchemaCheck) []SchemaError
updateCalls []updateUserIDCall
updateInfoCalls []updateUserInfoCall
}
type updateUserIDCall struct {
AccountID string
OldUserID string
NewUserID string
}
type updateUserInfoCall struct {
UserID string
Email string
Name string
}
func (s *testStore) ListUsers(ctx context.Context) ([]*types.User, error) {
return s.listUsersFunc(ctx)
}
func (s *testStore) UpdateUserID(ctx context.Context, accountID, oldUserID, newUserID string) error {
s.updateCalls = append(s.updateCalls, updateUserIDCall{accountID, oldUserID, newUserID})
return s.updateUserIDFunc(ctx, accountID, oldUserID, newUserID)
}
func (s *testStore) UpdateUserInfo(ctx context.Context, userID, email, name string) error {
s.updateInfoCalls = append(s.updateInfoCalls, updateUserInfoCall{userID, email, name})
if s.updateUserInfoFunc != nil {
return s.updateUserInfoFunc(ctx, userID, email, name)
}
return nil
}
func (s *testStore) CheckSchema(checks []SchemaCheck) []SchemaError {
if s.checkSchemaFunc != nil {
return s.checkSchemaFunc(checks)
}
return nil
}
type testServer struct {
store Store
eventStore EventStore
}
func (s *testServer) Store() Store { return s.store }
func (s *testServer) EventStore() EventStore { return s.eventStore }
func TestSeedConnectorFromEnv(t *testing.T) {
t.Run("returns ErrNoSeedInfo when env var is not set", func(t *testing.T) {
os.Unsetenv(idpSeedInfoKey)
conn, err := SeedConnectorFromEnv()
assert.ErrorIs(t, err, ErrNoSeedInfo)
assert.Nil(t, conn)
})
t.Run("returns ErrNoSeedInfo when env var is empty", func(t *testing.T) {
t.Setenv(idpSeedInfoKey, "")
conn, err := SeedConnectorFromEnv()
assert.ErrorIs(t, err, ErrNoSeedInfo)
assert.Nil(t, conn)
})
t.Run("returns error on invalid base64", func(t *testing.T) {
t.Setenv(idpSeedInfoKey, "not-valid-base64!!!")
conn, err := SeedConnectorFromEnv()
assert.NotErrorIs(t, err, ErrNoSeedInfo)
assert.Error(t, err)
assert.Nil(t, conn)
assert.Contains(t, err.Error(), "base64 decode")
})
t.Run("returns error on invalid JSON", func(t *testing.T) {
encoded := base64.StdEncoding.EncodeToString([]byte("not json"))
t.Setenv(idpSeedInfoKey, encoded)
conn, err := SeedConnectorFromEnv()
assert.NotErrorIs(t, err, ErrNoSeedInfo)
assert.Error(t, err)
assert.Nil(t, conn)
assert.Contains(t, err.Error(), "json unmarshal")
})
t.Run("successfully decodes valid connector", func(t *testing.T) {
expected := dex.Connector{
Type: "oidc",
Name: "Test Provider",
ID: "test-provider",
Config: map[string]any{
"issuer": "https://example.com",
"clientID": "my-client-id",
"clientSecret": "my-secret",
},
}
data, err := json.Marshal(expected)
require.NoError(t, err)
encoded := base64.StdEncoding.EncodeToString(data)
t.Setenv(idpSeedInfoKey, encoded)
conn, err := SeedConnectorFromEnv()
assert.NoError(t, err)
require.NotNil(t, conn)
assert.Equal(t, expected.Type, conn.Type)
assert.Equal(t, expected.Name, conn.Name)
assert.Equal(t, expected.ID, conn.ID)
assert.Equal(t, expected.Config["issuer"], conn.Config["issuer"])
})
}
func TestMigrateUsersToStaticConnectors(t *testing.T) {
connector := &dex.Connector{
Type: "oidc",
Name: "Test Provider",
ID: "test-connector",
}
t.Run("succeeds with no users", func(t *testing.T) {
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) { return nil, nil },
updateUserIDFunc: func(ctx context.Context, accountID, oldUserID, newUserID string) error { return nil },
}
srv := &testServer{store: ms}
err := MigrateUsersToStaticConnectors(srv, connector)
assert.NoError(t, err)
})
t.Run("returns error when ListUsers fails", func(t *testing.T) {
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return nil, fmt.Errorf("db error")
},
updateUserIDFunc: func(ctx context.Context, accountID, oldUserID, newUserID string) error { return nil },
}
srv := &testServer{store: ms}
err := MigrateUsersToStaticConnectors(srv, connector)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to list users")
})
t.Run("migrates single user with correct encoded ID", func(t *testing.T) {
user := &types.User{Id: "user-1", AccountID: "account-1"}
expectedNewID := dex.EncodeDexUserID("user-1", "test-connector")
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return []*types.User{user}, nil
},
updateUserIDFunc: func(ctx context.Context, accountID, oldUserID, newUserID string) error {
return nil
},
}
srv := &testServer{store: ms}
err := MigrateUsersToStaticConnectors(srv, connector)
assert.NoError(t, err)
require.Len(t, ms.updateCalls, 1)
assert.Equal(t, "account-1", ms.updateCalls[0].AccountID)
assert.Equal(t, "user-1", ms.updateCalls[0].OldUserID)
assert.Equal(t, expectedNewID, ms.updateCalls[0].NewUserID)
})
t.Run("migrates multiple users", func(t *testing.T) {
users := []*types.User{
{Id: "user-1", AccountID: "account-1"},
{Id: "user-2", AccountID: "account-1"},
{Id: "user-3", AccountID: "account-2"},
}
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return users, nil
},
updateUserIDFunc: func(ctx context.Context, accountID, oldUserID, newUserID string) error {
return nil
},
}
srv := &testServer{store: ms}
err := MigrateUsersToStaticConnectors(srv, connector)
assert.NoError(t, err)
assert.Len(t, ms.updateCalls, 3)
})
t.Run("returns error when UpdateUserID fails", func(t *testing.T) {
users := []*types.User{
{Id: "user-1", AccountID: "account-1"},
{Id: "user-2", AccountID: "account-1"},
}
callCount := 0
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return users, nil
},
updateUserIDFunc: func(ctx context.Context, accountID, oldUserID, newUserID string) error {
callCount++
if callCount == 2 {
return fmt.Errorf("update failed")
}
return nil
},
}
srv := &testServer{store: ms}
err := MigrateUsersToStaticConnectors(srv, connector)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to update user ID for user user-2")
})
t.Run("stops on first UpdateUserID error", func(t *testing.T) {
users := []*types.User{
{Id: "user-1", AccountID: "account-1"},
{Id: "user-2", AccountID: "account-1"},
}
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return users, nil
},
updateUserIDFunc: func(ctx context.Context, accountID, oldUserID, newUserID string) error {
return fmt.Errorf("update failed")
},
}
srv := &testServer{store: ms}
err := MigrateUsersToStaticConnectors(srv, connector)
assert.Error(t, err)
assert.Len(t, ms.updateCalls, 1) // stopped after first error
})
t.Run("skips already migrated users", func(t *testing.T) {
alreadyMigratedID := dex.EncodeDexUserID("user-1", "test-connector")
users := []*types.User{
{Id: alreadyMigratedID, AccountID: "account-1"},
}
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return users, nil
},
updateUserIDFunc: func(ctx context.Context, accountID, oldUserID, newUserID string) error {
return nil
},
}
srv := &testServer{store: ms}
err := MigrateUsersToStaticConnectors(srv, connector)
assert.NoError(t, err)
assert.Len(t, ms.updateCalls, 0)
})
t.Run("migrates only non-migrated users in mixed state", func(t *testing.T) {
alreadyMigratedID := dex.EncodeDexUserID("user-1", "test-connector")
users := []*types.User{
{Id: alreadyMigratedID, AccountID: "account-1"},
{Id: "user-2", AccountID: "account-1"},
{Id: "user-3", AccountID: "account-2"},
}
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return users, nil
},
updateUserIDFunc: func(ctx context.Context, accountID, oldUserID, newUserID string) error {
return nil
},
}
srv := &testServer{store: ms}
err := MigrateUsersToStaticConnectors(srv, connector)
assert.NoError(t, err)
// Only user-2 and user-3 should be migrated
assert.Len(t, ms.updateCalls, 2)
assert.Equal(t, "user-2", ms.updateCalls[0].OldUserID)
assert.Equal(t, "user-3", ms.updateCalls[1].OldUserID)
})
t.Run("dry run does not call UpdateUserID", func(t *testing.T) {
t.Setenv(dryRunEnvKey, "true")
users := []*types.User{
{Id: "user-1", AccountID: "account-1"},
{Id: "user-2", AccountID: "account-1"},
}
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return users, nil
},
updateUserIDFunc: func(ctx context.Context, accountID, oldUserID, newUserID string) error {
t.Fatal("UpdateUserID should not be called in dry-run mode")
return nil
},
}
srv := &testServer{store: ms}
err := MigrateUsersToStaticConnectors(srv, connector)
assert.NoError(t, err)
assert.Len(t, ms.updateCalls, 0)
})
t.Run("dry run skips already migrated users", func(t *testing.T) {
t.Setenv(dryRunEnvKey, "true")
alreadyMigratedID := dex.EncodeDexUserID("user-1", "test-connector")
users := []*types.User{
{Id: alreadyMigratedID, AccountID: "account-1"},
{Id: "user-2", AccountID: "account-1"},
}
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return users, nil
},
updateUserIDFunc: func(ctx context.Context, accountID, oldUserID, newUserID string) error {
t.Fatal("UpdateUserID should not be called in dry-run mode")
return nil
},
}
srv := &testServer{store: ms}
err := MigrateUsersToStaticConnectors(srv, connector)
assert.NoError(t, err)
})
t.Run("dry run disabled by default", func(t *testing.T) {
user := &types.User{Id: "user-1", AccountID: "account-1"}
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return []*types.User{user}, nil
},
updateUserIDFunc: func(ctx context.Context, accountID, oldUserID, newUserID string) error {
return nil
},
}
srv := &testServer{store: ms}
err := MigrateUsersToStaticConnectors(srv, connector)
assert.NoError(t, err)
assert.Len(t, ms.updateCalls, 1) // proves it's not in dry-run
})
}
func TestPopulateUserInfo(t *testing.T) {
noopUpdateID := func(ctx context.Context, accountID, oldUserID, newUserID string) error { return nil }
t.Run("succeeds with no users", func(t *testing.T) {
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) { return nil, nil },
updateUserIDFunc: noopUpdateID,
}
mockIDP := &idp.MockIDP{
GetAllAccountsFunc: func(ctx context.Context) (map[string][]*idp.UserData, error) {
return map[string][]*idp.UserData{}, nil
},
}
srv := &testServer{store: ms}
err := PopulateUserInfo(srv, mockIDP, false)
assert.NoError(t, err)
assert.Empty(t, ms.updateInfoCalls)
})
t.Run("returns error when ListUsers fails", func(t *testing.T) {
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return nil, fmt.Errorf("db error")
},
updateUserIDFunc: noopUpdateID,
}
mockIDP := &idp.MockIDP{}
srv := &testServer{store: ms}
err := PopulateUserInfo(srv, mockIDP, false)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to list users")
})
t.Run("returns error when GetAllAccounts fails", func(t *testing.T) {
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return []*types.User{{Id: "user-1", AccountID: "acc-1"}}, nil
},
updateUserIDFunc: noopUpdateID,
}
mockIDP := &idp.MockIDP{
GetAllAccountsFunc: func(ctx context.Context) (map[string][]*idp.UserData, error) {
return nil, fmt.Errorf("idp error")
},
}
srv := &testServer{store: ms}
err := PopulateUserInfo(srv, mockIDP, false)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to fetch accounts from IDP")
})
t.Run("updates user with missing email and name", func(t *testing.T) {
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return []*types.User{
{Id: "user-1", AccountID: "acc-1", Email: "", Name: ""},
}, nil
},
updateUserIDFunc: noopUpdateID,
}
mockIDP := &idp.MockIDP{
GetAllAccountsFunc: func(ctx context.Context) (map[string][]*idp.UserData, error) {
return map[string][]*idp.UserData{
"acc-1": {
{ID: "user-1", Email: "user1@example.com", Name: "User One"},
},
}, nil
},
}
srv := &testServer{store: ms}
err := PopulateUserInfo(srv, mockIDP, false)
assert.NoError(t, err)
require.Len(t, ms.updateInfoCalls, 1)
assert.Equal(t, "user-1", ms.updateInfoCalls[0].UserID)
assert.Equal(t, "user1@example.com", ms.updateInfoCalls[0].Email)
assert.Equal(t, "User One", ms.updateInfoCalls[0].Name)
})
t.Run("updates only missing email when name exists", func(t *testing.T) {
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return []*types.User{
{Id: "user-1", AccountID: "acc-1", Email: "", Name: "Existing Name"},
}, nil
},
updateUserIDFunc: noopUpdateID,
}
mockIDP := &idp.MockIDP{
GetAllAccountsFunc: func(ctx context.Context) (map[string][]*idp.UserData, error) {
return map[string][]*idp.UserData{
"acc-1": {{ID: "user-1", Email: "user1@example.com", Name: "IDP Name"}},
}, nil
},
}
srv := &testServer{store: ms}
err := PopulateUserInfo(srv, mockIDP, false)
assert.NoError(t, err)
require.Len(t, ms.updateInfoCalls, 1)
assert.Equal(t, "user1@example.com", ms.updateInfoCalls[0].Email)
assert.Equal(t, "Existing Name", ms.updateInfoCalls[0].Name)
})
t.Run("updates only missing name when email exists", func(t *testing.T) {
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return []*types.User{
{Id: "user-1", AccountID: "acc-1", Email: "existing@example.com", Name: ""},
}, nil
},
updateUserIDFunc: noopUpdateID,
}
mockIDP := &idp.MockIDP{
GetAllAccountsFunc: func(ctx context.Context) (map[string][]*idp.UserData, error) {
return map[string][]*idp.UserData{
"acc-1": {{ID: "user-1", Email: "idp@example.com", Name: "IDP Name"}},
}, nil
},
}
srv := &testServer{store: ms}
err := PopulateUserInfo(srv, mockIDP, false)
assert.NoError(t, err)
require.Len(t, ms.updateInfoCalls, 1)
assert.Equal(t, "existing@example.com", ms.updateInfoCalls[0].Email)
assert.Equal(t, "IDP Name", ms.updateInfoCalls[0].Name)
})
t.Run("skips users that already have both email and name", func(t *testing.T) {
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return []*types.User{
{Id: "user-1", AccountID: "acc-1", Email: "user1@example.com", Name: "User One"},
}, nil
},
updateUserIDFunc: noopUpdateID,
}
mockIDP := &idp.MockIDP{
GetAllAccountsFunc: func(ctx context.Context) (map[string][]*idp.UserData, error) {
return map[string][]*idp.UserData{
"acc-1": {{ID: "user-1", Email: "different@example.com", Name: "Different Name"}},
}, nil
},
}
srv := &testServer{store: ms}
err := PopulateUserInfo(srv, mockIDP, false)
assert.NoError(t, err)
assert.Empty(t, ms.updateInfoCalls)
})
t.Run("skips service users", func(t *testing.T) {
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return []*types.User{
{Id: "svc-1", AccountID: "acc-1", Email: "", Name: "", IsServiceUser: true},
}, nil
},
updateUserIDFunc: noopUpdateID,
}
mockIDP := &idp.MockIDP{
GetAllAccountsFunc: func(ctx context.Context) (map[string][]*idp.UserData, error) {
return map[string][]*idp.UserData{
"acc-1": {{ID: "svc-1", Email: "svc@example.com", Name: "Service"}},
}, nil
},
}
srv := &testServer{store: ms}
err := PopulateUserInfo(srv, mockIDP, false)
assert.NoError(t, err)
assert.Empty(t, ms.updateInfoCalls)
})
t.Run("skips users not found in IDP", func(t *testing.T) {
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return []*types.User{
{Id: "user-1", AccountID: "acc-1", Email: "", Name: ""},
}, nil
},
updateUserIDFunc: noopUpdateID,
}
mockIDP := &idp.MockIDP{
GetAllAccountsFunc: func(ctx context.Context) (map[string][]*idp.UserData, error) {
return map[string][]*idp.UserData{
"acc-1": {{ID: "different-user", Email: "other@example.com", Name: "Other"}},
}, nil
},
}
srv := &testServer{store: ms}
err := PopulateUserInfo(srv, mockIDP, false)
assert.NoError(t, err)
assert.Empty(t, ms.updateInfoCalls)
})
t.Run("looks up dex-encoded user IDs by original ID", func(t *testing.T) {
dexEncodedID := dex.EncodeDexUserID("original-idp-id", "my-connector")
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return []*types.User{
{Id: dexEncodedID, AccountID: "acc-1", Email: "", Name: ""},
}, nil
},
updateUserIDFunc: noopUpdateID,
}
mockIDP := &idp.MockIDP{
GetAllAccountsFunc: func(ctx context.Context) (map[string][]*idp.UserData, error) {
return map[string][]*idp.UserData{
"acc-1": {{ID: "original-idp-id", Email: "user@example.com", Name: "User"}},
}, nil
},
}
srv := &testServer{store: ms}
err := PopulateUserInfo(srv, mockIDP, false)
assert.NoError(t, err)
require.Len(t, ms.updateInfoCalls, 1)
assert.Equal(t, dexEncodedID, ms.updateInfoCalls[0].UserID)
assert.Equal(t, "user@example.com", ms.updateInfoCalls[0].Email)
assert.Equal(t, "User", ms.updateInfoCalls[0].Name)
})
t.Run("handles multiple users across multiple accounts", func(t *testing.T) {
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return []*types.User{
{Id: "user-1", AccountID: "acc-1", Email: "", Name: ""},
{Id: "user-2", AccountID: "acc-1", Email: "already@set.com", Name: "Already Set"},
{Id: "user-3", AccountID: "acc-2", Email: "", Name: ""},
}, nil
},
updateUserIDFunc: noopUpdateID,
}
mockIDP := &idp.MockIDP{
GetAllAccountsFunc: func(ctx context.Context) (map[string][]*idp.UserData, error) {
return map[string][]*idp.UserData{
"acc-1": {
{ID: "user-1", Email: "u1@example.com", Name: "User 1"},
{ID: "user-2", Email: "u2@example.com", Name: "User 2"},
},
"acc-2": {
{ID: "user-3", Email: "u3@example.com", Name: "User 3"},
},
}, nil
},
}
srv := &testServer{store: ms}
err := PopulateUserInfo(srv, mockIDP, false)
assert.NoError(t, err)
require.Len(t, ms.updateInfoCalls, 2)
assert.Equal(t, "user-1", ms.updateInfoCalls[0].UserID)
assert.Equal(t, "u1@example.com", ms.updateInfoCalls[0].Email)
assert.Equal(t, "user-3", ms.updateInfoCalls[1].UserID)
assert.Equal(t, "u3@example.com", ms.updateInfoCalls[1].Email)
})
t.Run("returns error when UpdateUserInfo fails", func(t *testing.T) {
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return []*types.User{
{Id: "user-1", AccountID: "acc-1", Email: "", Name: ""},
}, nil
},
updateUserIDFunc: noopUpdateID,
updateUserInfoFunc: func(ctx context.Context, userID, email, name string) error {
return fmt.Errorf("db write error")
},
}
mockIDP := &idp.MockIDP{
GetAllAccountsFunc: func(ctx context.Context) (map[string][]*idp.UserData, error) {
return map[string][]*idp.UserData{
"acc-1": {{ID: "user-1", Email: "u1@example.com", Name: "User 1"}},
}, nil
},
}
srv := &testServer{store: ms}
err := PopulateUserInfo(srv, mockIDP, false)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to update user info for user-1")
})
t.Run("stops on first UpdateUserInfo error", func(t *testing.T) {
callCount := 0
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return []*types.User{
{Id: "user-1", AccountID: "acc-1", Email: "", Name: ""},
{Id: "user-2", AccountID: "acc-1", Email: "", Name: ""},
}, nil
},
updateUserIDFunc: noopUpdateID,
updateUserInfoFunc: func(ctx context.Context, userID, email, name string) error {
callCount++
return fmt.Errorf("db write error")
},
}
mockIDP := &idp.MockIDP{
GetAllAccountsFunc: func(ctx context.Context) (map[string][]*idp.UserData, error) {
return map[string][]*idp.UserData{
"acc-1": {
{ID: "user-1", Email: "u1@example.com", Name: "U1"},
{ID: "user-2", Email: "u2@example.com", Name: "U2"},
},
}, nil
},
}
srv := &testServer{store: ms}
err := PopulateUserInfo(srv, mockIDP, false)
assert.Error(t, err)
assert.Equal(t, 1, callCount)
})
t.Run("dry run does not call UpdateUserInfo", func(t *testing.T) {
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return []*types.User{
{Id: "user-1", AccountID: "acc-1", Email: "", Name: ""},
{Id: "user-2", AccountID: "acc-1", Email: "", Name: ""},
}, nil
},
updateUserIDFunc: noopUpdateID,
updateUserInfoFunc: func(ctx context.Context, userID, email, name string) error {
t.Fatal("UpdateUserInfo should not be called in dry-run mode")
return nil
},
}
mockIDP := &idp.MockIDP{
GetAllAccountsFunc: func(ctx context.Context) (map[string][]*idp.UserData, error) {
return map[string][]*idp.UserData{
"acc-1": {
{ID: "user-1", Email: "u1@example.com", Name: "U1"},
{ID: "user-2", Email: "u2@example.com", Name: "U2"},
},
}, nil
},
}
srv := &testServer{store: ms}
err := PopulateUserInfo(srv, mockIDP, true)
assert.NoError(t, err)
assert.Empty(t, ms.updateInfoCalls)
})
t.Run("skips user when IDP has empty email and name too", func(t *testing.T) {
ms := &testStore{
listUsersFunc: func(ctx context.Context) ([]*types.User, error) {
return []*types.User{
{Id: "user-1", AccountID: "acc-1", Email: "", Name: ""},
}, nil
},
updateUserIDFunc: noopUpdateID,
}
mockIDP := &idp.MockIDP{
GetAllAccountsFunc: func(ctx context.Context) (map[string][]*idp.UserData, error) {
return map[string][]*idp.UserData{
"acc-1": {{ID: "user-1", Email: "", Name: ""}},
}, nil
},
}
srv := &testServer{store: ms}
err := PopulateUserInfo(srv, mockIDP, false)
assert.NoError(t, err)
assert.Empty(t, ms.updateInfoCalls)
})
}
func TestSchemaError_String(t *testing.T) {
t.Run("missing table", func(t *testing.T) {
e := SchemaError{Table: "jobs"}
assert.Equal(t, `table "jobs" is missing`, e.String())
})
t.Run("missing column", func(t *testing.T) {
e := SchemaError{Table: "users", Column: "email"}
assert.Equal(t, `column "email" on table "users" is missing`, e.String())
})
}
func TestRequiredSchema(t *testing.T) {
// Verify RequiredSchema covers all the tables touched by UpdateUserID and UpdateUserInfo.
expectedTables := []string{
"users",
"personal_access_tokens",
"peers",
"accounts",
"user_invites",
"proxy_access_tokens",
"jobs",
}
schemaTableNames := make([]string, len(RequiredSchema))
for i, s := range RequiredSchema {
schemaTableNames[i] = s.Table
}
for _, expected := range expectedTables {
assert.Contains(t, schemaTableNames, expected, "RequiredSchema should include table %q", expected)
}
}
func TestCheckSchema_MockStore(t *testing.T) {
t.Run("returns nil when all schema exists", func(t *testing.T) {
ms := &testStore{
checkSchemaFunc: func(checks []SchemaCheck) []SchemaError {
return nil
},
}
errs := ms.CheckSchema(RequiredSchema)
assert.Empty(t, errs)
})
t.Run("returns errors for missing tables", func(t *testing.T) {
ms := &testStore{
checkSchemaFunc: func(checks []SchemaCheck) []SchemaError {
return []SchemaError{
{Table: "jobs"},
{Table: "proxy_access_tokens"},
}
},
}
errs := ms.CheckSchema(RequiredSchema)
require.Len(t, errs, 2)
assert.Equal(t, "jobs", errs[0].Table)
assert.Equal(t, "", errs[0].Column)
assert.Equal(t, "proxy_access_tokens", errs[1].Table)
})
t.Run("returns errors for missing columns", func(t *testing.T) {
ms := &testStore{
checkSchemaFunc: func(checks []SchemaCheck) []SchemaError {
return []SchemaError{
{Table: "users", Column: "email"},
{Table: "users", Column: "name"},
}
},
}
errs := ms.CheckSchema(RequiredSchema)
require.Len(t, errs, 2)
assert.Equal(t, "users", errs[0].Table)
assert.Equal(t, "email", errs[0].Column)
})
}

View File

@@ -0,0 +1,82 @@
package migration
import (
"context"
"fmt"
"github.com/netbirdio/netbird/management/server/types"
)
// SchemaCheck represents a table and the columns required on it.
type SchemaCheck struct {
Table string
Columns []string
}
// RequiredSchema lists all tables and columns that the migration tool needs.
// If any are missing, the user must upgrade their management server first so
// that the automatic GORM migrations create them.
var RequiredSchema = []SchemaCheck{
{Table: "users", Columns: []string{"id", "email", "name", "account_id"}},
{Table: "personal_access_tokens", Columns: []string{"user_id", "created_by"}},
{Table: "peers", Columns: []string{"user_id"}},
{Table: "accounts", Columns: []string{"created_by"}},
{Table: "user_invites", Columns: []string{"created_by"}},
{Table: "proxy_access_tokens", Columns: []string{"created_by"}},
{Table: "jobs", Columns: []string{"triggered_by"}},
}
// SchemaError describes a single missing table or column.
type SchemaError struct {
Table string
Column string // empty when the whole table is missing
}
func (e SchemaError) String() string {
if e.Column == "" {
return fmt.Sprintf("table %q is missing", e.Table)
}
return fmt.Sprintf("column %q on table %q is missing", e.Column, e.Table)
}
// Store defines the data store operations required for IdP user migration.
// This interface is separate from the main store.Store interface because these methods
// are only used during one-time migration and should be removed once migration tooling
// is no longer needed.
//
// The SQL store implementations (SqlStore) already have these methods on their concrete
// types, so they satisfy this interface via Go's structural typing with zero code changes.
type Store interface {
// ListUsers returns all users across all accounts.
ListUsers(ctx context.Context) ([]*types.User, error)
// UpdateUserID atomically updates a user's ID and all foreign key references
// across the database (peers, groups, policies, PATs, etc.).
UpdateUserID(ctx context.Context, accountID, oldUserID, newUserID string) error
// UpdateUserInfo updates a user's email and name in the store.
UpdateUserInfo(ctx context.Context, userID, email, name string) error
// CheckSchema verifies that all tables and columns required by the migration
// exist in the database. Returns a list of problems; an empty slice means OK.
CheckSchema(checks []SchemaCheck) []SchemaError
}
// RequiredEventSchema lists all tables and columns that the migration tool needs
// in the activity/event store.
var RequiredEventSchema = []SchemaCheck{
{Table: "events", Columns: []string{"initiator_id", "target_id"}},
{Table: "deleted_users", Columns: []string{"id"}},
}
// EventStore defines the activity event store operations required for migration.
// Like Store, this is a temporary interface for migration tooling only.
type EventStore interface {
// CheckSchema verifies that all tables and columns required by the migration
// exist in the event database. Returns a list of problems; an empty slice means OK.
CheckSchema(checks []SchemaCheck) []SchemaError
// UpdateUserID updates all event references (initiator_id, target_id) and
// deleted_users records to use the new user ID format.
UpdateUserID(ctx context.Context, oldUserID, newUserID string) error
}

View File

@@ -64,10 +64,19 @@ type Manager interface {
GetVersionInfo(ctx context.Context) (*VersionInfo, error)
}
type instanceStore interface {
GetAccountsCounter(ctx context.Context) (int64, error)
}
type embeddedIdP interface {
CreateUserWithPassword(ctx context.Context, email, password, name string) (*idp.UserData, error)
GetAllAccounts(ctx context.Context) (map[string][]*idp.UserData, error)
}
// DefaultManager is the default implementation of Manager.
type DefaultManager struct {
store store.Store
embeddedIdpManager *idp.EmbeddedIdPManager
store instanceStore
embeddedIdpManager embeddedIdP
setupRequired bool
setupMu sync.RWMutex
@@ -82,18 +91,18 @@ type DefaultManager struct {
// NewManager creates a new instance manager.
// If idpManager is not an EmbeddedIdPManager, setup-related operations will return appropriate defaults.
func NewManager(ctx context.Context, store store.Store, idpManager idp.Manager) (Manager, error) {
embeddedIdp, _ := idpManager.(*idp.EmbeddedIdPManager)
embeddedIdp, ok := idpManager.(*idp.EmbeddedIdPManager)
m := &DefaultManager{
store: store,
embeddedIdpManager: embeddedIdp,
setupRequired: false,
store: store,
setupRequired: false,
httpClient: &http.Client{
Timeout: httpTimeout,
},
}
if embeddedIdp != nil {
if ok && embeddedIdp != nil {
m.embeddedIdpManager = embeddedIdp
err := m.loadSetupRequired(ctx)
if err != nil {
return nil, err
@@ -143,36 +152,61 @@ func (m *DefaultManager) IsSetupRequired(_ context.Context) (bool, error) {
// CreateOwnerUser creates the initial owner user in the embedded IDP.
func (m *DefaultManager) CreateOwnerUser(ctx context.Context, email, password, name string) (*idp.UserData, error) {
if err := m.validateSetupInfo(email, password, name); err != nil {
return nil, err
}
if m.embeddedIdpManager == nil {
return nil, errors.New("embedded IDP is not enabled")
}
m.setupMu.RLock()
setupRequired := m.setupRequired
m.setupMu.RUnlock()
if err := m.validateSetupInfo(email, password, name); err != nil {
return nil, err
}
if !setupRequired {
m.setupMu.Lock()
defer m.setupMu.Unlock()
if !m.setupRequired {
return nil, status.Errorf(status.PreconditionFailed, "setup already completed")
}
if err := m.checkSetupRequiredFromDB(ctx); err != nil {
var sErr *status.Error
if errors.As(err, &sErr) && sErr.Type() == status.PreconditionFailed {
m.setupRequired = false
}
return nil, err
}
userData, err := m.embeddedIdpManager.CreateUserWithPassword(ctx, email, password, name)
if err != nil {
return nil, fmt.Errorf("failed to create user in embedded IdP: %w", err)
}
m.setupMu.Lock()
m.setupRequired = false
m.setupMu.Unlock()
log.WithContext(ctx).Infof("created owner user %s in embedded IdP", email)
return userData, nil
}
func (m *DefaultManager) checkSetupRequiredFromDB(ctx context.Context) error {
numAccounts, err := m.store.GetAccountsCounter(ctx)
if err != nil {
return fmt.Errorf("failed to check accounts: %w", err)
}
if numAccounts > 0 {
return status.Errorf(status.PreconditionFailed, "setup already completed")
}
users, err := m.embeddedIdpManager.GetAllAccounts(ctx)
if err != nil {
return fmt.Errorf("failed to check IdP users: %w", err)
}
if len(users) > 0 {
return status.Errorf(status.PreconditionFailed, "setup already completed")
}
return nil
}
func (m *DefaultManager) validateSetupInfo(email, password, name string) error {
if email == "" {
return status.Errorf(status.InvalidArgument, "email is required")
@@ -189,6 +223,9 @@ func (m *DefaultManager) validateSetupInfo(email, password, name string) error {
if len(password) < 8 {
return status.Errorf(status.InvalidArgument, "password must be at least 8 characters")
}
if len(password) > 72 {
return status.Errorf(status.InvalidArgument, "password must be at most 72 characters")
}
return nil
}

View File

@@ -3,7 +3,12 @@ package instance
import (
"context"
"errors"
"fmt"
"net/http"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -11,173 +16,215 @@ import (
"github.com/netbirdio/netbird/management/server/idp"
)
// mockStore implements a minimal store.Store for testing
type mockIdP struct {
mu sync.Mutex
createUserFunc func(ctx context.Context, email, password, name string) (*idp.UserData, error)
users map[string][]*idp.UserData
getAllAccountsErr error
}
func (m *mockIdP) CreateUserWithPassword(ctx context.Context, email, password, name string) (*idp.UserData, error) {
if m.createUserFunc != nil {
return m.createUserFunc(ctx, email, password, name)
}
return &idp.UserData{ID: "test-user-id", Email: email, Name: name}, nil
}
func (m *mockIdP) GetAllAccounts(_ context.Context) (map[string][]*idp.UserData, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.getAllAccountsErr != nil {
return nil, m.getAllAccountsErr
}
return m.users, nil
}
type mockStore struct {
accountsCount int64
err error
}
func (m *mockStore) GetAccountsCounter(ctx context.Context) (int64, error) {
func (m *mockStore) GetAccountsCounter(_ context.Context) (int64, error) {
if m.err != nil {
return 0, m.err
}
return m.accountsCount, nil
}
// mockEmbeddedIdPManager wraps the real EmbeddedIdPManager for testing
type mockEmbeddedIdPManager struct {
createUserFunc func(ctx context.Context, email, password, name string) (*idp.UserData, error)
}
func (m *mockEmbeddedIdPManager) CreateUserWithPassword(ctx context.Context, email, password, name string) (*idp.UserData, error) {
if m.createUserFunc != nil {
return m.createUserFunc(ctx, email, password, name)
func newTestManager(idpMock *mockIdP, storeMock *mockStore) *DefaultManager {
return &DefaultManager{
store: storeMock,
embeddedIdpManager: idpMock,
setupRequired: true,
httpClient: &http.Client{Timeout: httpTimeout},
}
return &idp.UserData{
ID: "test-user-id",
Email: email,
Name: name,
}, nil
}
// testManager is a test implementation that accepts our mock types
type testManager struct {
store *mockStore
embeddedIdpManager *mockEmbeddedIdPManager
}
func (m *testManager) IsSetupRequired(ctx context.Context) (bool, error) {
if m.embeddedIdpManager == nil {
return false, nil
}
count, err := m.store.GetAccountsCounter(ctx)
if err != nil {
return false, err
}
return count == 0, nil
}
func (m *testManager) CreateOwnerUser(ctx context.Context, email, password, name string) (*idp.UserData, error) {
if m.embeddedIdpManager == nil {
return nil, errors.New("embedded IDP is not enabled")
}
return m.embeddedIdpManager.CreateUserWithPassword(ctx, email, password, name)
}
func TestIsSetupRequired_EmbeddedIdPDisabled(t *testing.T) {
manager := &testManager{
store: &mockStore{accountsCount: 0},
embeddedIdpManager: nil, // No embedded IDP
}
required, err := manager.IsSetupRequired(context.Background())
require.NoError(t, err)
assert.False(t, required, "setup should not be required when embedded IDP is disabled")
}
func TestIsSetupRequired_NoAccounts(t *testing.T) {
manager := &testManager{
store: &mockStore{accountsCount: 0},
embeddedIdpManager: &mockEmbeddedIdPManager{},
}
required, err := manager.IsSetupRequired(context.Background())
require.NoError(t, err)
assert.True(t, required, "setup should be required when no accounts exist")
}
func TestIsSetupRequired_AccountsExist(t *testing.T) {
manager := &testManager{
store: &mockStore{accountsCount: 1},
embeddedIdpManager: &mockEmbeddedIdPManager{},
}
required, err := manager.IsSetupRequired(context.Background())
require.NoError(t, err)
assert.False(t, required, "setup should not be required when accounts exist")
}
func TestIsSetupRequired_MultipleAccounts(t *testing.T) {
manager := &testManager{
store: &mockStore{accountsCount: 5},
embeddedIdpManager: &mockEmbeddedIdPManager{},
}
required, err := manager.IsSetupRequired(context.Background())
require.NoError(t, err)
assert.False(t, required, "setup should not be required when multiple accounts exist")
}
func TestIsSetupRequired_StoreError(t *testing.T) {
manager := &testManager{
store: &mockStore{err: errors.New("database error")},
embeddedIdpManager: &mockEmbeddedIdPManager{},
}
_, err := manager.IsSetupRequired(context.Background())
assert.Error(t, err, "should return error when store fails")
}
func TestCreateOwnerUser_Success(t *testing.T) {
expectedEmail := "admin@example.com"
expectedName := "Admin User"
expectedPassword := "securepassword123"
idpMock := &mockIdP{}
mgr := newTestManager(idpMock, &mockStore{})
manager := &testManager{
store: &mockStore{accountsCount: 0},
embeddedIdpManager: &mockEmbeddedIdPManager{
createUserFunc: func(ctx context.Context, email, password, name string) (*idp.UserData, error) {
assert.Equal(t, expectedEmail, email)
assert.Equal(t, expectedPassword, password)
assert.Equal(t, expectedName, name)
return &idp.UserData{
ID: "created-user-id",
Email: email,
Name: name,
}, nil
},
},
}
userData, err := manager.CreateOwnerUser(context.Background(), expectedEmail, expectedPassword, expectedName)
userData, err := mgr.CreateOwnerUser(context.Background(), "admin@example.com", "password123", "Admin")
require.NoError(t, err)
assert.Equal(t, "created-user-id", userData.ID)
assert.Equal(t, expectedEmail, userData.Email)
assert.Equal(t, expectedName, userData.Name)
assert.Equal(t, "admin@example.com", userData.Email)
_, err = mgr.CreateOwnerUser(context.Background(), "admin2@example.com", "password123", "Admin2")
require.Error(t, err)
assert.Contains(t, err.Error(), "setup already completed")
}
func TestCreateOwnerUser_SetupAlreadyCompleted(t *testing.T) {
mgr := newTestManager(&mockIdP{}, &mockStore{})
mgr.setupRequired = false
_, err := mgr.CreateOwnerUser(context.Background(), "admin@example.com", "password123", "Admin")
require.Error(t, err)
assert.Contains(t, err.Error(), "setup already completed")
}
func TestCreateOwnerUser_EmbeddedIdPDisabled(t *testing.T) {
manager := &testManager{
store: &mockStore{accountsCount: 0},
embeddedIdpManager: nil,
}
mgr := &DefaultManager{setupRequired: true}
_, err := manager.CreateOwnerUser(context.Background(), "admin@example.com", "password123", "Admin")
assert.Error(t, err, "should return error when embedded IDP is disabled")
_, err := mgr.CreateOwnerUser(context.Background(), "admin@example.com", "password123", "Admin")
require.Error(t, err)
assert.Contains(t, err.Error(), "embedded IDP is not enabled")
}
func TestCreateOwnerUser_IdPError(t *testing.T) {
manager := &testManager{
store: &mockStore{accountsCount: 0},
embeddedIdpManager: &mockEmbeddedIdPManager{
createUserFunc: func(ctx context.Context, email, password, name string) (*idp.UserData, error) {
return nil, errors.New("user already exists")
},
idpMock := &mockIdP{
createUserFunc: func(_ context.Context, _, _, _ string) (*idp.UserData, error) {
return nil, errors.New("provider error")
},
}
mgr := newTestManager(idpMock, &mockStore{})
_, err := manager.CreateOwnerUser(context.Background(), "admin@example.com", "password123", "Admin")
assert.Error(t, err, "should return error when IDP fails")
_, err := mgr.CreateOwnerUser(context.Background(), "admin@example.com", "password123", "Admin")
require.Error(t, err)
assert.Contains(t, err.Error(), "provider error")
required, _ := mgr.IsSetupRequired(context.Background())
assert.True(t, required, "setup should still be required after IdP error")
}
func TestCreateOwnerUser_TransientDBError_DoesNotBlockSetup(t *testing.T) {
mgr := newTestManager(&mockIdP{}, &mockStore{err: errors.New("connection refused")})
_, err := mgr.CreateOwnerUser(context.Background(), "admin@example.com", "password123", "Admin")
require.Error(t, err)
assert.Contains(t, err.Error(), "connection refused")
required, _ := mgr.IsSetupRequired(context.Background())
assert.True(t, required, "setup should still be required after transient DB error")
mgr.store = &mockStore{}
userData, err := mgr.CreateOwnerUser(context.Background(), "admin@example.com", "password123", "Admin")
require.NoError(t, err)
assert.Equal(t, "admin@example.com", userData.Email)
}
func TestCreateOwnerUser_TransientIdPError_DoesNotBlockSetup(t *testing.T) {
idpMock := &mockIdP{getAllAccountsErr: errors.New("connection reset")}
mgr := newTestManager(idpMock, &mockStore{})
_, err := mgr.CreateOwnerUser(context.Background(), "admin@example.com", "password123", "Admin")
require.Error(t, err)
assert.Contains(t, err.Error(), "connection reset")
required, _ := mgr.IsSetupRequired(context.Background())
assert.True(t, required, "setup should still be required after transient IdP error")
idpMock.getAllAccountsErr = nil
userData, err := mgr.CreateOwnerUser(context.Background(), "admin@example.com", "password123", "Admin")
require.NoError(t, err)
assert.Equal(t, "admin@example.com", userData.Email)
}
func TestCreateOwnerUser_DBCheckBlocksConcurrent(t *testing.T) {
idpMock := &mockIdP{
users: map[string][]*idp.UserData{
"acc1": {{ID: "existing-user"}},
},
}
mgr := newTestManager(idpMock, &mockStore{})
_, err := mgr.CreateOwnerUser(context.Background(), "admin@example.com", "password123", "Admin")
require.Error(t, err)
assert.Contains(t, err.Error(), "setup already completed")
}
func TestCreateOwnerUser_DBCheckBlocksWhenAccountsExist(t *testing.T) {
mgr := newTestManager(&mockIdP{}, &mockStore{accountsCount: 1})
_, err := mgr.CreateOwnerUser(context.Background(), "admin@example.com", "password123", "Admin")
require.Error(t, err)
assert.Contains(t, err.Error(), "setup already completed")
}
func TestCreateOwnerUser_ConcurrentRequests(t *testing.T) {
var idpCallCount atomic.Int32
var successCount atomic.Int32
var failCount atomic.Int32
idpMock := &mockIdP{
createUserFunc: func(_ context.Context, email, _, _ string) (*idp.UserData, error) {
idpCallCount.Add(1)
time.Sleep(50 * time.Millisecond)
return &idp.UserData{ID: "user-1", Email: email, Name: "Owner"}, nil
},
}
mgr := newTestManager(idpMock, &mockStore{})
var wg sync.WaitGroup
for i := range 10 {
wg.Add(1)
go func(idx int) {
defer wg.Done()
_, err := mgr.CreateOwnerUser(
context.Background(),
fmt.Sprintf("owner%d@example.com", idx),
"password1234",
fmt.Sprintf("Owner%d", idx),
)
if err != nil {
failCount.Add(1)
} else {
successCount.Add(1)
}
}(i)
}
wg.Wait()
assert.Equal(t, int32(1), successCount.Load(), "exactly one concurrent setup request should succeed")
assert.Equal(t, int32(9), failCount.Load(), "remaining concurrent requests should fail")
assert.Equal(t, int32(1), idpCallCount.Load(), "IdP CreateUser should be called exactly once")
}
func TestIsSetupRequired_EmbeddedIdPDisabled(t *testing.T) {
mgr := &DefaultManager{}
required, err := mgr.IsSetupRequired(context.Background())
require.NoError(t, err)
assert.False(t, required)
}
func TestIsSetupRequired_ReturnsFlag(t *testing.T) {
mgr := newTestManager(&mockIdP{}, &mockStore{})
required, err := mgr.IsSetupRequired(context.Background())
require.NoError(t, err)
assert.True(t, required)
mgr.setupMu.Lock()
mgr.setupRequired = false
mgr.setupMu.Unlock()
required, err = mgr.IsSetupRequired(context.Background())
require.NoError(t, err)
assert.False(t, required)
}
func TestDefaultManager_ValidateSetupRequest(t *testing.T) {
manager := &DefaultManager{
setupRequired: true,
}
manager := &DefaultManager{setupRequired: true}
tests := []struct {
name string
@@ -188,11 +235,10 @@ func TestDefaultManager_ValidateSetupRequest(t *testing.T) {
errorMsg string
}{
{
name: "valid request",
email: "admin@example.com",
password: "password123",
userName: "Admin User",
expectError: false,
name: "valid request",
email: "admin@example.com",
password: "password123",
userName: "Admin User",
},
{
name: "empty email",
@@ -235,11 +281,24 @@ func TestDefaultManager_ValidateSetupRequest(t *testing.T) {
errorMsg: "password must be at least 8 characters",
},
{
name: "password exactly 8 characters",
name: "password exactly 8 characters",
email: "admin@example.com",
password: "12345678",
userName: "Admin User",
},
{
name: "password exactly 72 characters",
email: "admin@example.com",
password: "aaaaaaaabbbbbbbbccccccccddddddddeeeeeeeeffffffffgggggggghhhhhhhhiiiiiiii",
userName: "Admin User",
},
{
name: "password too long",
email: "admin@example.com",
password: "12345678",
password: "aaaaaaaabbbbbbbbccccccccddddddddeeeeeeeeffffffffgggggggghhhhhhhhiiiiiiiij",
userName: "Admin User",
expectError: false,
expectError: true,
errorMsg: "password must be at most 72 characters",
},
}
@@ -255,14 +314,3 @@ func TestDefaultManager_ValidateSetupRequest(t *testing.T) {
})
}
}
func TestDefaultManager_CreateOwnerUser_SetupAlreadyCompleted(t *testing.T) {
manager := &DefaultManager{
setupRequired: false,
embeddedIdpManager: &idp.EmbeddedIdPManager{},
}
_, err := manager.CreateOwnerUser(context.Background(), "admin@example.com", "password123", "Admin")
require.Error(t, err)
assert.Contains(t, err.Error(), "setup already completed")
}

View File

@@ -46,7 +46,7 @@ type MockAccountManager struct {
AddPeerFunc func(ctx context.Context, accountID string, setupKey string, userId string, peer *nbpeer.Peer, temporary bool) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error)
GetGroupFunc func(ctx context.Context, accountID, groupID, userID string) (*types.Group, error)
GetAllGroupsFunc func(ctx context.Context, accountID, userID string) ([]*types.Group, error)
GetGroupByNameFunc func(ctx context.Context, accountID, groupName string) (*types.Group, error)
GetGroupByNameFunc func(ctx context.Context, groupName, accountID, userID string) (*types.Group, error)
SaveGroupFunc func(ctx context.Context, accountID, userID string, group *types.Group, create bool) error
SaveGroupsFunc func(ctx context.Context, accountID, userID string, groups []*types.Group, create bool) error
DeleteGroupFunc func(ctx context.Context, accountID, userId, groupID string) error
@@ -406,9 +406,9 @@ func (am *MockAccountManager) AddPeer(
}
// GetGroupByName mock implementation of GetGroupByName from server.AccountManager interface
func (am *MockAccountManager) GetGroupByName(ctx context.Context, accountID, groupName string) (*types.Group, error) {
func (am *MockAccountManager) GetGroupByName(ctx context.Context, groupName, accountID, userID string) (*types.Group, error) {
if am.GetGroupByNameFunc != nil {
return am.GetGroupByNameFunc(ctx, accountID, groupName)
return am.GetGroupByNameFunc(ctx, groupName, accountID, userID)
}
return nil, status.Errorf(codes.Unimplemented, "method GetGroupByName is not implemented")
}

View File

@@ -859,7 +859,9 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe
opEvent.Meta["setup_key_name"] = peerAddConfig.SetupKeyName
}
am.StoreEvent(ctx, opEvent.InitiatorID, opEvent.TargetID, opEvent.AccountID, opEvent.Activity, opEvent.Meta)
if !temporary {
am.StoreEvent(ctx, opEvent.InitiatorID, opEvent.TargetID, opEvent.AccountID, opEvent.Activity, opEvent.Meta)
}
if err := am.networkMapController.OnPeersAdded(ctx, accountID, []string{newPeer.ID}); err != nil {
log.WithContext(ctx).Errorf("failed to update network map cache for peer %s: %v", newPeer.ID, err)
@@ -1480,9 +1482,11 @@ func deletePeers(ctx context.Context, am *DefaultAccountManager, transaction sto
if err = transaction.DeletePeer(ctx, accountID, peer.ID); err != nil {
return nil, err
}
peerDeletedEvents = append(peerDeletedEvents, func() {
am.StoreEvent(ctx, userID, peer.ID, accountID, activity.PeerRemovedByUser, peer.EventMeta(dnsDomain))
})
if !(peer.ProxyMeta.Embedded || peer.Meta.KernelVersion == "wasm") {
peerDeletedEvents = append(peerDeletedEvents, func() {
am.StoreEvent(ctx, userID, peer.ID, accountID, activity.PeerRemovedByUser, peer.EventMeta(dnsDomain))
})
}
}
return peerDeletedEvents, nil

View File

@@ -84,7 +84,7 @@ func (am *DefaultAccountManager) SavePostureChecks(ctx context.Context, accountI
// DeletePostureChecks deletes a posture check by ID.
func (am *DefaultAccountManager) DeletePostureChecks(ctx context.Context, accountID, postureChecksID, userID string) error {
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Routes, operations.Read)
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Policies, operations.Delete)
if err != nil {
return status.NewPermissionValidationError(err)
}

View File

@@ -2080,7 +2080,8 @@ func (s *SqlStore) getPostureChecks(ctx context.Context, accountID string) ([]*p
func (s *SqlStore) getServices(ctx context.Context, accountID string) ([]*rpservice.Service, error) {
const serviceQuery = `SELECT id, account_id, name, domain, enabled, auth,
meta_created_at, meta_certificate_issued_at, meta_status, proxy_cluster,
pass_host_header, rewrite_redirects, session_private_key, session_public_key
pass_host_header, rewrite_redirects, session_private_key, session_public_key,
mode, listen_port, port_auto_assigned, source, source_peer, terminated
FROM services WHERE account_id = $1`
const targetsQuery = `SELECT id, account_id, service_id, path, host, port, protocol,
@@ -2097,6 +2098,7 @@ func (s *SqlStore) getServices(ctx context.Context, accountID string) ([]*rpserv
var auth []byte
var createdAt, certIssuedAt sql.NullTime
var status, proxyCluster, sessionPrivateKey, sessionPublicKey sql.NullString
var mode, source, sourcePeer sql.NullString
err := row.Scan(
&s.ID,
&s.AccountID,
@@ -2112,6 +2114,12 @@ func (s *SqlStore) getServices(ctx context.Context, accountID string) ([]*rpserv
&s.RewriteRedirects,
&sessionPrivateKey,
&sessionPublicKey,
&mode,
&s.ListenPort,
&s.PortAutoAssigned,
&source,
&sourcePeer,
&s.Terminated,
)
if err != nil {
return nil, err
@@ -2143,6 +2151,15 @@ func (s *SqlStore) getServices(ctx context.Context, accountID string) ([]*rpserv
if sessionPublicKey.Valid {
s.SessionPublicKey = sessionPublicKey.String
}
if mode.Valid {
s.Mode = mode.String
}
if source.Valid {
s.Source = source.String
}
if sourcePeer.Valid {
s.SourcePeer = sourcePeer.String
}
s.Targets = []*rpservice.Target{}
return &s, nil
@@ -5445,7 +5462,7 @@ func (s *SqlStore) GetActiveProxyClusterAddresses(ctx context.Context) ([]string
result := s.db.WithContext(ctx).
Model(&proxy.Proxy{}).
Where("status = ? AND last_seen > ?", "connected", time.Now().Add(-2*time.Minute)).
Where("status = ? AND last_seen > ?", "connected", time.Now().Add(-proxyActiveThreshold)).
Distinct("cluster_address").
Pluck("cluster_address", &addresses)
@@ -5463,7 +5480,7 @@ func (s *SqlStore) GetActiveProxyClusters(ctx context.Context) ([]proxy.Cluster,
result := s.db.Model(&proxy.Proxy{}).
Select("cluster_address as address, COUNT(*) as connected_proxies").
Where("status = ? AND last_seen > ?", "connected", time.Now().Add(-2*time.Minute)).
Where("status = ? AND last_seen > ?", "connected", time.Now().Add(-proxyActiveThreshold)).
Group("cluster_address").
Scan(&clusters)
@@ -5475,6 +5492,63 @@ func (s *SqlStore) GetActiveProxyClusters(ctx context.Context) ([]proxy.Cluster,
return clusters, nil
}
// proxyActiveThreshold is the maximum age of a heartbeat for a proxy to be
// considered active. Must be at least 2x the heartbeat interval (1 min).
const proxyActiveThreshold = 2 * time.Minute
var validCapabilityColumns = map[string]struct{}{
"supports_custom_ports": {},
"require_subdomain": {},
}
// GetClusterSupportsCustomPorts returns whether any active proxy in the cluster
// supports custom ports. Returns nil when no proxy reported the capability.
func (s *SqlStore) GetClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool {
return s.getClusterCapability(ctx, clusterAddr, "supports_custom_ports")
}
// GetClusterRequireSubdomain returns whether any active proxy in the cluster
// requires a subdomain. Returns nil when no proxy reported the capability.
func (s *SqlStore) GetClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool {
return s.getClusterCapability(ctx, clusterAddr, "require_subdomain")
}
// getClusterCapability returns an aggregated boolean capability for the given
// cluster. It checks active (connected, recently seen) proxies and returns:
// - *true if any proxy in the cluster has the capability set to true,
// - *false if at least one proxy reported but none set it to true,
// - nil if no proxy reported the capability at all.
func (s *SqlStore) getClusterCapability(ctx context.Context, clusterAddr, column string) *bool {
if _, ok := validCapabilityColumns[column]; !ok {
log.WithContext(ctx).Errorf("invalid capability column: %s", column)
return nil
}
var result struct {
HasCapability bool
AnyTrue bool
}
err := s.db.WithContext(ctx).
Model(&proxy.Proxy{}).
Select("COUNT(CASE WHEN "+column+" IS NOT NULL THEN 1 END) > 0 AS has_capability, "+
"COALESCE(MAX(CASE WHEN "+column+" = true THEN 1 ELSE 0 END), 0) = 1 AS any_true").
Where("cluster_address = ? AND status = ? AND last_seen > ?",
clusterAddr, "connected", time.Now().Add(-proxyActiveThreshold)).
Scan(&result).Error
if err != nil {
log.WithContext(ctx).Errorf("query cluster capability %s for %s: %v", column, clusterAddr, err)
return nil
}
if !result.HasCapability {
return nil
}
return &result.AnyTrue
}
// CleanupStaleProxies deletes proxies that haven't sent heartbeat in the specified duration
func (s *SqlStore) CleanupStaleProxies(ctx context.Context, inactivityDuration time.Duration) error {
cutoffTime := time.Now().Add(-inactivityDuration)
@@ -5494,3 +5568,61 @@ func (s *SqlStore) CleanupStaleProxies(ctx context.Context, inactivityDuration t
return nil
}
// GetRoutingPeerNetworks returns the distinct network names where the peer is assigned as a routing peer
// in an enabled network router, either directly or via peer groups.
func (s *SqlStore) GetRoutingPeerNetworks(_ context.Context, accountID, peerID string) ([]string, error) {
var routers []*routerTypes.NetworkRouter
if err := s.db.Select("peer, peer_groups, network_id").Where("account_id = ? AND enabled = true", accountID).Find(&routers).Error; err != nil {
return nil, status.Errorf(status.Internal, "failed to get enabled routers: %v", err)
}
if len(routers) == 0 {
return nil, nil
}
var groupPeers []types.GroupPeer
if err := s.db.Select("group_id").Where("account_id = ? AND peer_id = ?", accountID, peerID).Find(&groupPeers).Error; err != nil {
return nil, status.Errorf(status.Internal, "failed to get peer group memberships: %v", err)
}
groupSet := make(map[string]struct{}, len(groupPeers))
for _, gp := range groupPeers {
groupSet[gp.GroupID] = struct{}{}
}
networkIDs := make(map[string]struct{})
for _, r := range routers {
if r.Peer == peerID {
networkIDs[r.NetworkID] = struct{}{}
} else if r.Peer == "" {
for _, pg := range r.PeerGroups {
if _, ok := groupSet[pg]; ok {
networkIDs[r.NetworkID] = struct{}{}
break
}
}
}
}
if len(networkIDs) == 0 {
return nil, nil
}
ids := make([]string, 0, len(networkIDs))
for id := range networkIDs {
ids = append(ids, id)
}
var networks []*networkTypes.Network
if err := s.db.Select("name").Where("account_id = ? AND id IN ?", accountID, ids).Find(&networks).Error; err != nil {
return nil, status.Errorf(status.Internal, "failed to get networks: %v", err)
}
names := make([]string, 0, len(networks))
for _, n := range networks {
names = append(names, n.Name)
}
return names, nil
}

View File

@@ -0,0 +1,177 @@
package store
// This file contains migration-only methods on SqlStore.
// They satisfy the migration.Store interface via duck typing.
// Delete this file when migration tooling is no longer needed.
import (
"context"
"fmt"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
"github.com/netbirdio/netbird/management/server/idp/migration"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/shared/management/status"
)
func (s *SqlStore) CheckSchema(checks []migration.SchemaCheck) []migration.SchemaError {
migrator := s.db.Migrator()
var errs []migration.SchemaError
for _, check := range checks {
if !migrator.HasTable(check.Table) {
errs = append(errs, migration.SchemaError{Table: check.Table})
continue
}
for _, col := range check.Columns {
if !migrator.HasColumn(check.Table, col) {
errs = append(errs, migration.SchemaError{Table: check.Table, Column: col})
}
}
}
return errs
}
func (s *SqlStore) ListUsers(ctx context.Context) ([]*types.User, error) {
tx := s.db
var users []*types.User
result := tx.Find(&users)
if result.Error != nil {
log.WithContext(ctx).Errorf("error when listing users from the store: %s", result.Error)
return nil, status.Errorf(status.Internal, "issue listing users from store")
}
for _, user := range users {
if err := user.DecryptSensitiveData(s.fieldEncrypt); err != nil {
return nil, fmt.Errorf("decrypt user: %w", err)
}
}
return users, nil
}
// txDeferFKConstraints defers foreign key constraint checks for the duration of the transaction.
// MySQL is already handled by s.transaction (SET FOREIGN_KEY_CHECKS = 0).
func (s *SqlStore) txDeferFKConstraints(tx *gorm.DB) error {
if s.storeEngine == types.SqliteStoreEngine {
return tx.Exec("PRAGMA defer_foreign_keys = ON").Error
}
if s.storeEngine != types.PostgresStoreEngine {
return nil
}
// GORM creates FK constraints as NOT DEFERRABLE by default, so
// SET CONSTRAINTS ALL DEFERRED is a no-op unless we ALTER them first.
err := tx.Exec(`
DO $$ DECLARE r RECORD;
BEGIN
FOR r IN SELECT conname, conrelid::regclass AS tbl
FROM pg_constraint WHERE contype = 'f' AND NOT condeferrable
LOOP
EXECUTE format('ALTER TABLE %s ALTER CONSTRAINT %I DEFERRABLE INITIALLY IMMEDIATE', r.tbl, r.conname);
END LOOP;
END $$
`).Error
if err != nil {
return fmt.Errorf("make FK constraints deferrable: %w", err)
}
return tx.Exec("SET CONSTRAINTS ALL DEFERRED").Error
}
// txRestoreFKConstraints reverts FK constraints back to NOT DEFERRABLE after the
// deferred updates are done but before the transaction commits.
func (s *SqlStore) txRestoreFKConstraints(tx *gorm.DB) error {
if s.storeEngine != types.PostgresStoreEngine {
return nil
}
return tx.Exec(`
DO $$ DECLARE r RECORD;
BEGIN
FOR r IN SELECT conname, conrelid::regclass AS tbl
FROM pg_constraint WHERE contype = 'f' AND condeferrable
LOOP
EXECUTE format('ALTER TABLE %s ALTER CONSTRAINT %I NOT DEFERRABLE', r.tbl, r.conname);
END LOOP;
END $$
`).Error
}
func (s *SqlStore) UpdateUserInfo(ctx context.Context, userID, email, name string) error {
user := &types.User{Email: email, Name: name}
if err := user.EncryptSensitiveData(s.fieldEncrypt); err != nil {
return fmt.Errorf("encrypt user info: %w", err)
}
result := s.db.Model(&types.User{}).Where("id = ?", userID).Updates(map[string]any{
"email": user.Email,
"name": user.Name,
})
if result.Error != nil {
log.WithContext(ctx).Errorf("error updating user info for %s: %s", userID, result.Error)
return status.Errorf(status.Internal, "failed to update user info")
}
return nil
}
func (s *SqlStore) UpdateUserID(ctx context.Context, accountID, oldUserID, newUserID string) error {
type fkUpdate struct {
model any
column string
where string
}
updates := []fkUpdate{
{&types.PersonalAccessToken{}, "user_id", "user_id = ?"},
{&types.PersonalAccessToken{}, "created_by", "created_by = ?"},
{&nbpeer.Peer{}, "user_id", "user_id = ?"},
{&types.UserInviteRecord{}, "created_by", "created_by = ?"},
{&types.Account{}, "created_by", "created_by = ?"},
{&types.ProxyAccessToken{}, "created_by", "created_by = ?"},
{&types.Job{}, "triggered_by", "triggered_by = ?"},
}
log.Info("Updating user ID in the store")
err := s.transaction(func(tx *gorm.DB) error {
if err := s.txDeferFKConstraints(tx); err != nil {
return err
}
for _, u := range updates {
if err := tx.Model(u.model).Where(u.where, oldUserID).Update(u.column, newUserID).Error; err != nil {
return fmt.Errorf("update %s: %w", u.column, err)
}
}
if err := tx.Model(&types.User{}).Where(accountAndIDQueryCondition, accountID, oldUserID).Update("id", newUserID).Error; err != nil {
return fmt.Errorf("update users: %w", err)
}
return nil
})
if err != nil {
log.WithContext(ctx).Errorf("failed to update user ID in the store: %s", err)
return status.Errorf(status.Internal, "failed to update user ID in store")
}
log.Info("Restoring FK constraints")
err = s.transaction(func(tx *gorm.DB) error {
if err := s.txRestoreFKConstraints(tx); err != nil {
return fmt.Errorf("restore FK constraints: %w", err)
}
return nil
})
if err != nil {
log.WithContext(ctx).Errorf("failed to restore FK constraints after user ID update: %s", err)
return status.Errorf(status.Internal, "failed to restore FK constraints after user ID update")
}
return nil
}

View File

@@ -121,7 +121,7 @@ type Store interface {
GetAccountGroups(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.Group, error)
GetResourceGroups(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) ([]*types.Group, error)
GetGroupByID(ctx context.Context, lockStrength LockingStrength, accountID, groupID string) (*types.Group, error)
GetGroupByName(ctx context.Context, lockStrength LockingStrength, groupName, accountID string) (*types.Group, error)
GetGroupByName(ctx context.Context, lockStrength LockingStrength, accountID, groupName string) (*types.Group, error)
GetGroupsByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, groupIDs []string) (map[string]*types.Group, error)
CreateGroups(ctx context.Context, accountID string, groups []*types.Group) error
UpdateGroups(ctx context.Context, accountID string, groups []*types.Group) error
@@ -287,9 +287,13 @@ type Store interface {
UpdateProxyHeartbeat(ctx context.Context, proxyID, clusterAddress, ipAddress string) error
GetActiveProxyClusterAddresses(ctx context.Context) ([]string, error)
GetActiveProxyClusters(ctx context.Context) ([]proxy.Cluster, error)
GetClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool
GetClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool
CleanupStaleProxies(ctx context.Context, inactivityDuration time.Duration) error
GetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error)
GetRoutingPeerNetworks(ctx context.Context, accountID, peerID string) ([]string, error)
}
const (

View File

@@ -1361,6 +1361,34 @@ func (mr *MockStoreMockRecorder) GetAnyAccountID(ctx interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAnyAccountID", reflect.TypeOf((*MockStore)(nil).GetAnyAccountID), ctx)
}
// GetClusterRequireSubdomain mocks base method.
func (m *MockStore) GetClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetClusterRequireSubdomain", ctx, clusterAddr)
ret0, _ := ret[0].(*bool)
return ret0
}
// GetClusterRequireSubdomain indicates an expected call of GetClusterRequireSubdomain.
func (mr *MockStoreMockRecorder) GetClusterRequireSubdomain(ctx, clusterAddr interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClusterRequireSubdomain", reflect.TypeOf((*MockStore)(nil).GetClusterRequireSubdomain), ctx, clusterAddr)
}
// GetClusterSupportsCustomPorts mocks base method.
func (m *MockStore) GetClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetClusterSupportsCustomPorts", ctx, clusterAddr)
ret0, _ := ret[0].(*bool)
return ret0
}
// GetClusterSupportsCustomPorts indicates an expected call of GetClusterSupportsCustomPorts.
func (mr *MockStoreMockRecorder) GetClusterSupportsCustomPorts(ctx, clusterAddr interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClusterSupportsCustomPorts", reflect.TypeOf((*MockStore)(nil).GetClusterSupportsCustomPorts), ctx, clusterAddr)
}
// GetCustomDomain mocks base method.
func (m *MockStore) GetCustomDomain(ctx context.Context, accountID, domainID string) (*domain.Domain, error) {
m.ctrl.T.Helper()
@@ -1438,18 +1466,18 @@ func (mr *MockStoreMockRecorder) GetGroupByID(ctx, lockStrength, accountID, grou
}
// GetGroupByName mocks base method.
func (m *MockStore) GetGroupByName(ctx context.Context, lockStrength LockingStrength, groupName, accountID string) (*types2.Group, error) {
func (m *MockStore) GetGroupByName(ctx context.Context, lockStrength LockingStrength, accountID, groupName string) (*types2.Group, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetGroupByName", ctx, lockStrength, groupName, accountID)
ret := m.ctrl.Call(m, "GetGroupByName", ctx, lockStrength, accountID, groupName)
ret0, _ := ret[0].(*types2.Group)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetGroupByName indicates an expected call of GetGroupByName.
func (mr *MockStoreMockRecorder) GetGroupByName(ctx, lockStrength, groupName, accountID interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetGroupByName(ctx, lockStrength, accountID, groupName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupByName", reflect.TypeOf((*MockStore)(nil).GetGroupByName), ctx, lockStrength, groupName, accountID)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupByName", reflect.TypeOf((*MockStore)(nil).GetGroupByName), ctx, lockStrength, accountID, groupName)
}
// GetGroupsByIDs mocks base method.
@@ -1946,6 +1974,21 @@ func (mr *MockStoreMockRecorder) GetRouteByID(ctx, lockStrength, accountID, rout
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRouteByID", reflect.TypeOf((*MockStore)(nil).GetRouteByID), ctx, lockStrength, accountID, routeID)
}
// GetRoutingPeerNetworks mocks base method.
func (m *MockStore) GetRoutingPeerNetworks(ctx context.Context, accountID, peerID string) ([]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetRoutingPeerNetworks", ctx, accountID, peerID)
ret0, _ := ret[0].([]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetRoutingPeerNetworks indicates an expected call of GetRoutingPeerNetworks.
func (mr *MockStoreMockRecorder) GetRoutingPeerNetworks(ctx, accountID, peerID interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRoutingPeerNetworks", reflect.TypeOf((*MockStore)(nil).GetRoutingPeerNetworks), ctx, accountID, peerID)
}
// GetServiceByDomain mocks base method.
func (m *MockStore) GetServiceByDomain(ctx context.Context, domain string) (*service.Service, error) {
m.ctrl.T.Helper()

View File

@@ -84,6 +84,12 @@ func setupTestAccount() *Account {
},
},
Groups: map[string]*Group{
"groupAll": {
ID: "groupAll",
Name: "All",
Peers: []string{"peer1", "peer2", "peer3", "peer11", "peer12", "peer21", "peer31", "peer32", "peer41", "peer51", "peer61"},
Issued: GroupIssuedAPI,
},
"group1": {
ID: "group1",
Peers: []string{"peer11", "peer12"},

View File

@@ -417,6 +417,10 @@ func (am *DefaultAccountManager) CreatePAT(ctx context.Context, accountID string
return nil, err
}
if targetUser.AccountID != accountID {
return nil, status.NewPermissionDeniedError()
}
// @note this is essential to prevent non admin users with Pats create permission frpm creating one for a service user
if initiatorUserID != targetUserID && !(initiatorUser.HasAdminPower() && targetUser.IsServiceUser) {
return nil, status.NewAdminPermissionError()
@@ -457,6 +461,10 @@ func (am *DefaultAccountManager) DeletePAT(ctx context.Context, accountID string
return err
}
if targetUser.AccountID != accountID {
return status.NewPermissionDeniedError()
}
if initiatorUserID != targetUserID && !(initiatorUser.HasAdminPower() && targetUser.IsServiceUser) {
return status.NewAdminPermissionError()
}
@@ -496,6 +504,10 @@ func (am *DefaultAccountManager) GetPAT(ctx context.Context, accountID string, i
return nil, err
}
if targetUser.AccountID != accountID {
return nil, status.NewPermissionDeniedError()
}
if initiatorUserID != targetUserID && !(initiatorUser.HasAdminPower() && targetUser.IsServiceUser) {
return nil, status.NewAdminPermissionError()
}
@@ -523,6 +535,10 @@ func (am *DefaultAccountManager) GetAllPATs(ctx context.Context, accountID strin
return nil, err
}
if targetUser.AccountID != accountID {
return nil, status.NewPermissionDeniedError()
}
if initiatorUserID != targetUserID && !(initiatorUser.HasAdminPower() && targetUser.IsServiceUser) {
return nil, status.NewAdminPermissionError()
}
@@ -764,9 +780,15 @@ func (am *DefaultAccountManager) processUserUpdate(ctx context.Context, transact
updatedUser.Role = update.Role
updatedUser.Blocked = update.Blocked
updatedUser.AutoGroups = update.AutoGroups
// these two fields can't be set via API, only via direct call to the method
// these fields can't be set via API, only via direct call to the method
updatedUser.Issued = update.Issued
updatedUser.IntegrationReference = update.IntegrationReference
if update.Name != "" {
updatedUser.Name = update.Name
}
if update.Email != "" {
updatedUser.Email = update.Email
}
var transferredOwnerRole bool
result, err := handleOwnerRoleTransfer(ctx, transaction, initiatorUser, update)

View File

@@ -336,6 +336,104 @@ func TestUser_GetAllPATs(t *testing.T) {
assert.Equal(t, 2, len(pats))
}
func TestUser_PAT_CrossAccountProtection(t *testing.T) {
const (
accountAID = "accountA"
accountBID = "accountB"
userAID = "userA"
adminBID = "adminB"
serviceUserBID = "serviceUserB"
regularUserBID = "regularUserB"
tokenBID = "tokenB1"
hashedTokenB = "SoMeHaShEdToKeNB"
)
setupStore := func(t *testing.T) (*DefaultAccountManager, func()) {
t.Helper()
s, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
require.NoError(t, err, "creating store")
accountA := newAccountWithId(context.Background(), accountAID, userAID, "", "", "", false)
require.NoError(t, s.SaveAccount(context.Background(), accountA))
accountB := newAccountWithId(context.Background(), accountBID, adminBID, "", "", "", false)
accountB.Users[serviceUserBID] = &types.User{
Id: serviceUserBID,
AccountID: accountBID,
IsServiceUser: true,
ServiceUserName: "svcB",
Role: types.UserRoleAdmin,
PATs: map[string]*types.PersonalAccessToken{
tokenBID: {
ID: tokenBID,
HashedToken: hashedTokenB,
},
},
}
accountB.Users[regularUserBID] = &types.User{
Id: regularUserBID,
AccountID: accountBID,
Role: types.UserRoleUser,
}
require.NoError(t, s.SaveAccount(context.Background(), accountB))
pm := permissions.NewManager(s)
am := &DefaultAccountManager{
Store: s,
eventStore: &activity.InMemoryEventStore{},
permissionsManager: pm,
}
return am, cleanup
}
t.Run("CreatePAT for user in different account is denied", func(t *testing.T) {
am, cleanup := setupStore(t)
t.Cleanup(cleanup)
_, err := am.CreatePAT(context.Background(), accountAID, userAID, serviceUserBID, "xss-token", 7)
require.Error(t, err, "cross-account CreatePAT must fail")
_, err = am.CreatePAT(context.Background(), accountAID, userAID, regularUserBID, "xss-token", 7)
require.Error(t, err, "cross-account CreatePAT for regular user must fail")
_, err = am.CreatePAT(context.Background(), accountBID, adminBID, serviceUserBID, "legit-token", 7)
require.NoError(t, err, "same-account CreatePAT should succeed")
})
t.Run("DeletePAT for user in different account is denied", func(t *testing.T) {
am, cleanup := setupStore(t)
t.Cleanup(cleanup)
err := am.DeletePAT(context.Background(), accountAID, userAID, serviceUserBID, tokenBID)
require.Error(t, err, "cross-account DeletePAT must fail")
})
t.Run("GetPAT for user in different account is denied", func(t *testing.T) {
am, cleanup := setupStore(t)
t.Cleanup(cleanup)
_, err := am.GetPAT(context.Background(), accountAID, userAID, serviceUserBID, tokenBID)
require.Error(t, err, "cross-account GetPAT must fail")
})
t.Run("GetAllPATs for user in different account is denied", func(t *testing.T) {
am, cleanup := setupStore(t)
t.Cleanup(cleanup)
_, err := am.GetAllPATs(context.Background(), accountAID, userAID, serviceUserBID)
require.Error(t, err, "cross-account GetAllPATs must fail")
})
t.Run("CreatePAT with forged accountID targeting foreign user is denied", func(t *testing.T) {
am, cleanup := setupStore(t)
t.Cleanup(cleanup)
_, err := am.CreatePAT(context.Background(), accountAID, userAID, adminBID, "forged", 7)
require.Error(t, err, "forged accountID CreatePAT must fail")
})
}
func TestUser_Copy(t *testing.T) {
// this is an imaginary case which will never be in DB this way
user := types.User{