[management, infrastructure, idp] Simplified IdP Management - Embedded IdP (#5008)

Embed Dex as a built-in IdP to simplify self-hosting setup.
Adds an embedded OIDC Identity Provider (Dex) with local user management and optional external IdP connectors (Google/GitHub/OIDC/SAML), plus device-auth flow for CLI login. Introduces instance onboarding/setup endpoints (including owner creation), field-level encryption for sensitive user data, a streamlined self-hosting provisioning script, and expanded APIs + test coverage for IdP management.

more at https://github.com/netbirdio/netbird/pull/5008#issuecomment-3718987393
This commit is contained in:
Misha Bragin
2026-01-07 08:52:32 -05:00
committed by GitHub
parent 5393ad948f
commit e586c20e36
90 changed files with 7702 additions and 517 deletions

View File

@@ -3,6 +3,7 @@ package server
import (
"context"
"fmt"
"os"
"reflect"
"testing"
"time"
@@ -29,6 +30,7 @@ import (
"github.com/stretchr/testify/require"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/netbirdio/netbird/idp/dex"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/management/server/idp"
"github.com/netbirdio/netbird/management/server/integration_reference"
@@ -58,7 +60,7 @@ func TestUser_CreatePAT_ForSameUser(t *testing.T) {
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
err = s.SaveAccount(context.Background(), account)
if err != nil {
@@ -105,7 +107,7 @@ func TestUser_CreatePAT_ForDifferentUser(t *testing.T) {
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
account.Users[mockTargetUserId] = &types.User{
Id: mockTargetUserId,
IsServiceUser: false,
@@ -133,7 +135,7 @@ func TestUser_CreatePAT_ForServiceUser(t *testing.T) {
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
account.Users[mockTargetUserId] = &types.User{
Id: mockTargetUserId,
IsServiceUser: true,
@@ -165,7 +167,7 @@ func TestUser_CreatePAT_WithWrongExpiration(t *testing.T) {
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
err = store.SaveAccount(context.Background(), account)
if err != nil {
@@ -190,7 +192,7 @@ func TestUser_CreatePAT_WithEmptyName(t *testing.T) {
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
err = store.SaveAccount(context.Background(), account)
if err != nil {
@@ -215,7 +217,7 @@ func TestUser_DeletePAT(t *testing.T) {
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
account.Users[mockUserID] = &types.User{
Id: mockUserID,
PATs: map[string]*types.PersonalAccessToken{
@@ -258,7 +260,7 @@ func TestUser_GetPAT(t *testing.T) {
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
account.Users[mockUserID] = &types.User{
Id: mockUserID,
AccountID: mockAccountID,
@@ -298,7 +300,7 @@ func TestUser_GetAllPATs(t *testing.T) {
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
account.Users[mockUserID] = &types.User{
Id: mockUserID,
AccountID: mockAccountID,
@@ -362,6 +364,8 @@ func TestUser_Copy(t *testing.T) {
ID: 0,
IntegrationType: "test",
},
Email: "whatever@gmail.com",
Name: "John Doe",
}
err := validateStruct(user)
@@ -408,7 +412,7 @@ func TestUser_CreateServiceUser(t *testing.T) {
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
err = store.SaveAccount(context.Background(), account)
if err != nil {
@@ -455,7 +459,7 @@ func TestUser_CreateUser_ServiceUser(t *testing.T) {
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
err = store.SaveAccount(context.Background(), account)
if err != nil {
@@ -503,7 +507,7 @@ func TestUser_CreateUser_RegularUser(t *testing.T) {
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
err = store.SaveAccount(context.Background(), account)
if err != nil {
@@ -534,7 +538,7 @@ func TestUser_InviteNewUser(t *testing.T) {
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
err = store.SaveAccount(context.Background(), account)
if err != nil {
@@ -641,7 +645,7 @@ func TestUser_DeleteUser_ServiceUser(t *testing.T) {
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
account.Users[mockServiceUserID] = tt.serviceUser
err = store.SaveAccount(context.Background(), account)
@@ -680,7 +684,7 @@ func TestUser_DeleteUser_SelfDelete(t *testing.T) {
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
err = store.SaveAccount(context.Background(), account)
if err != nil {
@@ -707,7 +711,7 @@ func TestUser_DeleteUser_regularUser(t *testing.T) {
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
targetId := "user2"
account.Users[targetId] = &types.User{
@@ -801,7 +805,7 @@ func TestUser_DeleteUser_RegularUsers(t *testing.T) {
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
targetId := "user2"
account.Users[targetId] = &types.User{
@@ -969,7 +973,7 @@ func TestDefaultAccountManager_GetUser(t *testing.T) {
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
err = store.SaveAccount(context.Background(), account)
if err != nil {
@@ -1005,9 +1009,9 @@ func TestDefaultAccountManager_ListUsers(t *testing.T) {
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
account.Users["normal_user1"] = types.NewRegularUser("normal_user1")
account.Users["normal_user2"] = types.NewRegularUser("normal_user2")
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
account.Users["normal_user1"] = types.NewRegularUser("normal_user1", "", "")
account.Users["normal_user2"] = types.NewRegularUser("normal_user2", "", "")
err = store.SaveAccount(context.Background(), account)
if err != nil {
@@ -1047,7 +1051,7 @@ func TestDefaultAccountManager_ExternalCache(t *testing.T) {
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
externalUser := &types.User{
Id: "externalUser",
Role: types.UserRoleUser,
@@ -1104,7 +1108,7 @@ func TestUser_IsAdmin(t *testing.T) {
user := types.NewAdminUser(mockUserID)
assert.True(t, user.HasAdminPower())
user = types.NewRegularUser(mockUserID)
user = types.NewRegularUser(mockUserID, "", "")
assert.False(t, user.HasAdminPower())
}
@@ -1115,7 +1119,7 @@ func TestUser_GetUsersFromAccount_ForAdmin(t *testing.T) {
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
account.Users[mockServiceUserID] = &types.User{
Id: mockServiceUserID,
Role: "user",
@@ -1149,7 +1153,7 @@ func TestUser_GetUsersFromAccount_ForUser(t *testing.T) {
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
account.Users[mockServiceUserID] = &types.User{
Id: mockServiceUserID,
Role: "user",
@@ -1320,13 +1324,13 @@ func TestDefaultAccountManager_SaveUser(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
// create an account and an admin user
account, err := manager.GetOrCreateAccountByUser(context.Background(), ownerUserID, "netbird.io")
account, err := manager.GetOrCreateAccountByUser(context.Background(), auth.UserAuth{UserId: ownerUserID, Domain: "netbird.io"})
if err != nil {
t.Fatal(err)
}
// create other users
account.Users[regularUserID] = types.NewRegularUser(regularUserID)
account.Users[regularUserID] = types.NewRegularUser(regularUserID, "", "")
account.Users[adminUserID] = types.NewAdminUser(adminUserID)
account.Users[serviceUserID] = &types.User{IsServiceUser: true, Id: serviceUserID, Role: types.UserRoleAdmin, ServiceUserName: "service"}
err = manager.Store.SaveAccount(context.Background(), account)
@@ -1516,7 +1520,7 @@ func TestSaveOrAddUser_PreventAccountSwitch(t *testing.T) {
}
t.Cleanup(cleanup)
account1 := newAccountWithId(context.Background(), "account1", "ownerAccount1", "", false)
account1 := newAccountWithId(context.Background(), "account1", "ownerAccount1", "", "", "", false)
targetId := "user2"
account1.Users[targetId] = &types.User{
Id: targetId,
@@ -1525,7 +1529,7 @@ func TestSaveOrAddUser_PreventAccountSwitch(t *testing.T) {
}
require.NoError(t, s.SaveAccount(context.Background(), account1))
account2 := newAccountWithId(context.Background(), "account2", "ownerAccount2", "", false)
account2 := newAccountWithId(context.Background(), "account2", "ownerAccount2", "", "", "", false)
require.NoError(t, s.SaveAccount(context.Background(), account2))
permissionsManager := permissions.NewManager(s)
@@ -1552,7 +1556,7 @@ func TestDefaultAccountManager_GetCurrentUserInfo(t *testing.T) {
}
t.Cleanup(cleanup)
account1 := newAccountWithId(context.Background(), "account1", "account1Owner", "", false)
account1 := newAccountWithId(context.Background(), "account1", "account1Owner", "", "", "", false)
account1.Settings.RegularUsersViewBlocked = false
account1.Users["blocked-user"] = &types.User{
Id: "blocked-user",
@@ -1574,7 +1578,7 @@ func TestDefaultAccountManager_GetCurrentUserInfo(t *testing.T) {
}
require.NoError(t, store.SaveAccount(context.Background(), account1))
account2 := newAccountWithId(context.Background(), "account2", "account2Owner", "", false)
account2 := newAccountWithId(context.Background(), "account2", "account2Owner", "", "", "", false)
account2.Users["settings-blocked-user"] = &types.User{
Id: "settings-blocked-user",
Role: types.UserRoleUser,
@@ -1771,7 +1775,7 @@ func TestApproveUser(t *testing.T) {
}
// Create account with admin and pending approval user
account := newAccountWithId(context.Background(), "account-1", "admin-user", "example.com", false)
account := newAccountWithId(context.Background(), "account-1", "admin-user", "example.com", "", "", false)
err = manager.Store.SaveAccount(context.Background(), account)
require.NoError(t, err)
@@ -1782,7 +1786,7 @@ func TestApproveUser(t *testing.T) {
require.NoError(t, err)
// Create user pending approval
pendingUser := types.NewRegularUser("pending-user")
pendingUser := types.NewRegularUser("pending-user", "", "")
pendingUser.AccountID = account.Id
pendingUser.Blocked = true
pendingUser.PendingApproval = true
@@ -1807,12 +1811,12 @@ func TestApproveUser(t *testing.T) {
assert.Contains(t, err.Error(), "not pending approval")
// Test approval by non-admin should fail
regularUser := types.NewRegularUser("regular-user")
regularUser := types.NewRegularUser("regular-user", "", "")
regularUser.AccountID = account.Id
err = manager.Store.SaveUser(context.Background(), regularUser)
require.NoError(t, err)
pendingUser2 := types.NewRegularUser("pending-user-2")
pendingUser2 := types.NewRegularUser("pending-user-2", "", "")
pendingUser2.AccountID = account.Id
pendingUser2.Blocked = true
pendingUser2.PendingApproval = true
@@ -1830,7 +1834,7 @@ func TestRejectUser(t *testing.T) {
}
// Create account with admin and pending approval user
account := newAccountWithId(context.Background(), "account-1", "admin-user", "example.com", false)
account := newAccountWithId(context.Background(), "account-1", "admin-user", "example.com", "", "", false)
err = manager.Store.SaveAccount(context.Background(), account)
require.NoError(t, err)
@@ -1841,7 +1845,7 @@ func TestRejectUser(t *testing.T) {
require.NoError(t, err)
// Create user pending approval
pendingUser := types.NewRegularUser("pending-user")
pendingUser := types.NewRegularUser("pending-user", "", "")
pendingUser.AccountID = account.Id
pendingUser.Blocked = true
pendingUser.PendingApproval = true
@@ -1857,7 +1861,7 @@ func TestRejectUser(t *testing.T) {
require.Error(t, err)
// Test rejection of non-pending user should fail
regularUser := types.NewRegularUser("regular-user")
regularUser := types.NewRegularUser("regular-user", "", "")
regularUser.AccountID = account.Id
err = manager.Store.SaveUser(context.Background(), regularUser)
require.NoError(t, err)
@@ -1867,7 +1871,7 @@ func TestRejectUser(t *testing.T) {
assert.Contains(t, err.Error(), "not pending approval")
// Test rejection by non-admin should fail
pendingUser2 := types.NewRegularUser("pending-user-2")
pendingUser2 := types.NewRegularUser("pending-user-2", "", "")
pendingUser2.AccountID = account.Id
pendingUser2.Blocked = true
pendingUser2.PendingApproval = true
@@ -1877,3 +1881,149 @@ func TestRejectUser(t *testing.T) {
err = manager.RejectUser(context.Background(), account.Id, regularUser.Id, pendingUser2.Id)
require.Error(t, err)
}
func TestUser_Operations_WithEmbeddedIDP(t *testing.T) {
ctx := context.Background()
// Create temporary directory for Dex
tmpDir := t.TempDir()
dexDataDir := tmpDir + "/dex"
require.NoError(t, os.MkdirAll(dexDataDir, 0700))
// Create embedded IDP config
embeddedIdPConfig := &idp.EmbeddedIdPConfig{
Enabled: true,
Issuer: "http://localhost:5556/dex",
Storage: idp.EmbeddedStorageConfig{
Type: "sqlite3",
Config: idp.EmbeddedStorageTypeConfig{
File: dexDataDir + "/dex.db",
},
},
}
// Create embedded IDP manager
embeddedIdp, err := idp.NewEmbeddedIdPManager(ctx, embeddedIdPConfig, nil)
require.NoError(t, err)
defer func() { _ = embeddedIdp.Stop(ctx) }()
// Create test store
testStore, cleanup, err := store.NewTestStoreFromSQL(ctx, "", tmpDir)
require.NoError(t, err)
defer cleanup()
// Create account with owner user
account := newAccountWithId(ctx, mockAccountID, mockUserID, "", "owner@test.com", "Owner User", false)
require.NoError(t, testStore.SaveAccount(ctx, account))
// Create mock network map controller
ctrl := gomock.NewController(t)
networkMapControllerMock := network_map.NewMockController(ctrl)
networkMapControllerMock.EXPECT().
OnPeersDeleted(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil).
AnyTimes()
// Create account manager with embedded IDP
permissionsManager := permissions.NewManager(testStore)
am := DefaultAccountManager{
Store: testStore,
eventStore: &activity.InMemoryEventStore{},
permissionsManager: permissionsManager,
idpManager: embeddedIdp,
cacheLoading: map[string]chan struct{}{},
networkMapController: networkMapControllerMock,
}
// Initialize cache manager
cacheStore, err := nbcache.NewStore(ctx, nbcache.DefaultIDPCacheExpirationMax, nbcache.DefaultIDPCacheCleanupInterval, nbcache.DefaultIDPCacheOpenConn)
require.NoError(t, err)
am.cacheManager = nbcache.NewAccountUserDataCache(am.loadAccount, cacheStore)
am.externalCacheManager = nbcache.NewUserDataCache(cacheStore)
t.Run("create regular user returns password", func(t *testing.T) {
userInfo, err := am.CreateUser(ctx, mockAccountID, mockUserID, &types.UserInfo{
Email: "newuser@test.com",
Name: "New User",
Role: "user",
AutoGroups: []string{},
IsServiceUser: false,
})
require.NoError(t, err)
require.NotNil(t, userInfo)
// Verify user data
assert.Equal(t, "newuser@test.com", userInfo.Email)
assert.Equal(t, "New User", userInfo.Name)
assert.Equal(t, "user", userInfo.Role)
assert.NotEmpty(t, userInfo.ID)
// IMPORTANT: Password should be returned for embedded IDP
assert.NotEmpty(t, userInfo.Password, "Password should be returned for embedded IDP user")
t.Logf("Created user: ID=%s, Email=%s, Password=%s", userInfo.ID, userInfo.Email, userInfo.Password)
// Verify user ID is in Dex encoded format
rawUserID, connectorID, err := dex.DecodeDexUserID(userInfo.ID)
require.NoError(t, err)
assert.NotEmpty(t, rawUserID)
assert.Equal(t, "local", connectorID)
t.Logf("Decoded user ID: rawUserID=%s, connectorID=%s", rawUserID, connectorID)
// Verify user exists in database with correct data
dbUser, err := testStore.GetUserByUserID(ctx, store.LockingStrengthNone, userInfo.ID)
require.NoError(t, err)
assert.Equal(t, "newuser@test.com", dbUser.Email)
assert.Equal(t, "New User", dbUser.Name)
// Store user ID for delete test
createdUserID := userInfo.ID
t.Run("delete user works", func(t *testing.T) {
err := am.DeleteUser(ctx, mockAccountID, mockUserID, createdUserID)
require.NoError(t, err)
// Verify user is deleted from database
_, err = testStore.GetUserByUserID(ctx, store.LockingStrengthNone, createdUserID)
assert.Error(t, err, "User should be deleted from database")
})
})
t.Run("create service user does not return password", func(t *testing.T) {
userInfo, err := am.CreateUser(ctx, mockAccountID, mockUserID, &types.UserInfo{
Name: "Service User",
Role: "user",
AutoGroups: []string{},
IsServiceUser: true,
})
require.NoError(t, err)
require.NotNil(t, userInfo)
assert.True(t, userInfo.IsServiceUser)
assert.Equal(t, "Service User", userInfo.Name)
// Service users don't have passwords
assert.Empty(t, userInfo.Password, "Service users should not have passwords")
})
t.Run("duplicate email fails", func(t *testing.T) {
// Create first user
_, err := am.CreateUser(ctx, mockAccountID, mockUserID, &types.UserInfo{
Email: "duplicate@test.com",
Name: "First User",
Role: "user",
AutoGroups: []string{},
IsServiceUser: false,
})
require.NoError(t, err)
// Try to create second user with same email
_, err = am.CreateUser(ctx, mockAccountID, mockUserID, &types.UserInfo{
Email: "duplicate@test.com",
Name: "Second User",
Role: "user",
AutoGroups: []string{},
IsServiceUser: false,
})
assert.Error(t, err, "Creating user with duplicate email should fail")
t.Logf("Duplicate email error: %v", err)
})
}