mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 15:26:40 +00:00
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
269 lines
7.4 KiB
Go
269 lines
7.4 KiB
Go
package instance
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/netbirdio/netbird/management/server/idp"
|
|
)
|
|
|
|
// mockStore implements a minimal store.Store for testing
|
|
type mockStore struct {
|
|
accountsCount int64
|
|
err error
|
|
}
|
|
|
|
func (m *mockStore) GetAccountsCounter(ctx 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)
|
|
}
|
|
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"
|
|
|
|
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)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "created-user-id", userData.ID)
|
|
assert.Equal(t, expectedEmail, userData.Email)
|
|
assert.Equal(t, expectedName, userData.Name)
|
|
}
|
|
|
|
func TestCreateOwnerUser_EmbeddedIdPDisabled(t *testing.T) {
|
|
manager := &testManager{
|
|
store: &mockStore{accountsCount: 0},
|
|
embeddedIdpManager: nil,
|
|
}
|
|
|
|
_, err := manager.CreateOwnerUser(context.Background(), "admin@example.com", "password123", "Admin")
|
|
assert.Error(t, err, "should return error when embedded IDP is disabled")
|
|
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")
|
|
},
|
|
},
|
|
}
|
|
|
|
_, err := manager.CreateOwnerUser(context.Background(), "admin@example.com", "password123", "Admin")
|
|
assert.Error(t, err, "should return error when IDP fails")
|
|
}
|
|
|
|
func TestDefaultManager_ValidateSetupRequest(t *testing.T) {
|
|
manager := &DefaultManager{
|
|
setupRequired: true,
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
email string
|
|
password string
|
|
userName string
|
|
expectError bool
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "valid request",
|
|
email: "admin@example.com",
|
|
password: "password123",
|
|
userName: "Admin User",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "empty email",
|
|
email: "",
|
|
password: "password123",
|
|
userName: "Admin User",
|
|
expectError: true,
|
|
errorMsg: "email is required",
|
|
},
|
|
{
|
|
name: "invalid email format",
|
|
email: "not-an-email",
|
|
password: "password123",
|
|
userName: "Admin User",
|
|
expectError: true,
|
|
errorMsg: "invalid email format",
|
|
},
|
|
{
|
|
name: "empty name",
|
|
email: "admin@example.com",
|
|
password: "password123",
|
|
userName: "",
|
|
expectError: true,
|
|
errorMsg: "name is required",
|
|
},
|
|
{
|
|
name: "empty password",
|
|
email: "admin@example.com",
|
|
password: "",
|
|
userName: "Admin User",
|
|
expectError: true,
|
|
errorMsg: "password is required",
|
|
},
|
|
{
|
|
name: "password too short",
|
|
email: "admin@example.com",
|
|
password: "short",
|
|
userName: "Admin User",
|
|
expectError: true,
|
|
errorMsg: "password must be at least 8 characters",
|
|
},
|
|
{
|
|
name: "password exactly 8 characters",
|
|
email: "admin@example.com",
|
|
password: "12345678",
|
|
userName: "Admin User",
|
|
expectError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := manager.validateSetupInfo(tt.email, tt.password, tt.userName)
|
|
if tt.expectError {
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.errorMsg)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|