mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 07:16:38 +00:00
1011 lines
30 KiB
Go
1011 lines
30 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/netbirdio/netbird/management/server/activity"
|
|
"github.com/netbirdio/netbird/management/server/idp"
|
|
"github.com/netbirdio/netbird/management/server/permissions"
|
|
"github.com/netbirdio/netbird/management/server/store"
|
|
"github.com/netbirdio/netbird/management/server/types"
|
|
"github.com/netbirdio/netbird/shared/management/status"
|
|
"github.com/netbirdio/netbird/util/crypt"
|
|
)
|
|
|
|
const (
|
|
testAccountID = "testAccountID"
|
|
testAdminUserID = "testAdminUserID"
|
|
testRegularUserID = "testRegularUserID"
|
|
)
|
|
|
|
// setupInviteTestManagerWithEmbeddedIdP creates a test manager with a real embedded IdP
|
|
// and store encryption enabled. This is required for tests that need to pass the IsEmbeddedIdp check.
|
|
func setupInviteTestManagerWithEmbeddedIdP(t *testing.T) (*DefaultAccountManager, func()) {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
|
|
tmpDir := t.TempDir()
|
|
dexDataDir := tmpDir + "/dex"
|
|
require.NoError(t, os.MkdirAll(dexDataDir, 0700))
|
|
|
|
// Create test store
|
|
s, cleanup, err := store.NewTestStoreFromSQL(ctx, "", tmpDir)
|
|
require.NoError(t, err, "Error when creating store")
|
|
|
|
// Enable encryption
|
|
key, err := crypt.GenerateKey()
|
|
require.NoError(t, err)
|
|
fieldEncrypt, err := crypt.NewFieldEncrypt(key)
|
|
require.NoError(t, err)
|
|
s.SetFieldEncrypt(fieldEncrypt)
|
|
|
|
// 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)
|
|
|
|
account := newAccountWithId(ctx, testAccountID, testAdminUserID, "", "admin@test.com", "Admin User", false)
|
|
account.Users[testRegularUserID] = &types.User{
|
|
Id: testRegularUserID,
|
|
AccountID: testAccountID,
|
|
Role: types.UserRoleUser,
|
|
Email: "regular@test.com",
|
|
Name: "Regular User",
|
|
}
|
|
|
|
err = s.SaveAccount(ctx, account)
|
|
require.NoError(t, err, "Error when saving account")
|
|
|
|
permissionsManager := permissions.NewManager(s)
|
|
|
|
am := DefaultAccountManager{
|
|
Store: s,
|
|
eventStore: &activity.InMemoryEventStore{},
|
|
permissionsManager: permissionsManager,
|
|
idpManager: embeddedIdp,
|
|
}
|
|
|
|
cleanupFunc := func() {
|
|
_ = embeddedIdp.Stop(ctx)
|
|
cleanup()
|
|
}
|
|
|
|
return &am, cleanupFunc
|
|
}
|
|
|
|
func TestCreateUserInvite_Success(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
invite := &types.UserInfo{
|
|
Email: "newuser@test.com",
|
|
Name: "New User",
|
|
Role: "user",
|
|
AutoGroups: []string{},
|
|
}
|
|
|
|
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result)
|
|
|
|
assert.Equal(t, "newuser@test.com", result.UserInfo.Email)
|
|
assert.Equal(t, "New User", result.UserInfo.Name)
|
|
assert.Equal(t, "user", result.UserInfo.Role)
|
|
assert.Equal(t, string(types.UserStatusInvited), result.UserInfo.Status)
|
|
assert.NotEmpty(t, result.InviteToken)
|
|
assert.True(t, result.InviteExpiresAt.After(time.Now()))
|
|
|
|
// Verify invite is stored in DB
|
|
invites, err := am.Store.GetAccountUserInvites(context.Background(), store.LockingStrengthNone, testAccountID)
|
|
require.NoError(t, err)
|
|
assert.Len(t, invites, 1)
|
|
assert.Equal(t, "newuser@test.com", invites[0].Email)
|
|
}
|
|
|
|
func TestCreateUserInvite_DuplicateEmail(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
invite := &types.UserInfo{
|
|
Email: "newuser@test.com",
|
|
Name: "New User",
|
|
Role: "user",
|
|
AutoGroups: []string{},
|
|
}
|
|
|
|
// Create first invite
|
|
_, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
|
|
require.NoError(t, err)
|
|
|
|
// Try to create duplicate invite
|
|
_, err = am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
|
|
require.Error(t, err)
|
|
|
|
sErr, ok := status.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, status.AlreadyExists, sErr.Type())
|
|
}
|
|
|
|
func TestCreateUserInvite_ExistingUserEmail(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
// Try to invite with an email that already exists as a user
|
|
invite := &types.UserInfo{
|
|
Email: "regular@test.com", // Already exists as a user
|
|
Name: "Duplicate User",
|
|
Role: "user",
|
|
AutoGroups: []string{},
|
|
}
|
|
|
|
_, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
|
|
require.Error(t, err)
|
|
|
|
sErr, ok := status.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, status.UserAlreadyExists, sErr.Type())
|
|
}
|
|
|
|
func TestCreateUserInvite_PermissionDenied(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
invite := &types.UserInfo{
|
|
Email: "newuser@test.com",
|
|
Name: "New User",
|
|
Role: "user",
|
|
AutoGroups: []string{},
|
|
}
|
|
|
|
// Regular user should not be able to create invites
|
|
_, err := am.CreateUserInvite(context.Background(), testAccountID, testRegularUserID, invite, 0)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCreateUserInvite_InvalidEmail(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
invite := &types.UserInfo{
|
|
Email: "",
|
|
Name: "New User",
|
|
Role: "user",
|
|
AutoGroups: []string{},
|
|
}
|
|
|
|
_, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
|
|
require.Error(t, err)
|
|
|
|
sErr, ok := status.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, status.InvalidArgument, sErr.Type())
|
|
}
|
|
|
|
func TestCreateUserInvite_InvalidName(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
invite := &types.UserInfo{
|
|
Email: "newuser@test.com",
|
|
Name: "",
|
|
Role: "user",
|
|
AutoGroups: []string{},
|
|
}
|
|
|
|
_, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
|
|
require.Error(t, err)
|
|
|
|
sErr, ok := status.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, status.InvalidArgument, sErr.Type())
|
|
}
|
|
|
|
func TestCreateUserInvite_OwnerRole(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
invite := &types.UserInfo{
|
|
Email: "newowner@test.com",
|
|
Name: "New Owner",
|
|
Role: "owner",
|
|
AutoGroups: []string{},
|
|
}
|
|
|
|
_, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
|
|
require.Error(t, err)
|
|
|
|
sErr, ok := status.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, status.InvalidArgument, sErr.Type())
|
|
}
|
|
|
|
func TestCreateUserInvite_ExpirationTooShort(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
invite := &types.UserInfo{
|
|
Email: "newuser@test.com",
|
|
Name: "New User",
|
|
Role: "user",
|
|
AutoGroups: []string{},
|
|
}
|
|
|
|
// Try to create with expiration less than 1 hour
|
|
_, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 1800) // 30 minutes
|
|
require.Error(t, err)
|
|
|
|
sErr, ok := status.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, status.InvalidArgument, sErr.Type())
|
|
assert.Contains(t, err.Error(), "at least 1 hour")
|
|
}
|
|
|
|
func TestCreateUserInvite_CustomExpiration(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
invite := &types.UserInfo{
|
|
Email: "newuser@test.com",
|
|
Name: "New User",
|
|
Role: "user",
|
|
AutoGroups: []string{},
|
|
}
|
|
|
|
expiresIn := 7200 // 2 hours
|
|
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, expiresIn)
|
|
require.NoError(t, err)
|
|
|
|
// Verify expiration is approximately 2 hours from now
|
|
expectedExpiration := time.Now().Add(time.Duration(expiresIn) * time.Second)
|
|
assert.WithinDuration(t, expectedExpiration, result.InviteExpiresAt, time.Minute)
|
|
}
|
|
|
|
func TestCreateUserInvite_WithAutoGroups(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
invite := &types.UserInfo{
|
|
Email: "newuser@test.com",
|
|
Name: "New User",
|
|
Role: "user",
|
|
AutoGroups: []string{"group1", "group2"},
|
|
}
|
|
|
|
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"group1", "group2"}, result.UserInfo.AutoGroups)
|
|
|
|
// Verify invite in DB has auto groups
|
|
invites, err := am.Store.GetAccountUserInvites(context.Background(), store.LockingStrengthNone, testAccountID)
|
|
require.NoError(t, err)
|
|
require.Len(t, invites, 1)
|
|
assert.Equal(t, []string{"group1", "group2"}, invites[0].AutoGroups)
|
|
}
|
|
|
|
func TestGetUserInviteInfo_Success(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
// Create an invite first
|
|
invite := &types.UserInfo{
|
|
Email: "newuser@test.com",
|
|
Name: "New User",
|
|
Role: "user",
|
|
AutoGroups: []string{},
|
|
}
|
|
|
|
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
|
|
require.NoError(t, err)
|
|
|
|
// Get the invite info using the token
|
|
info, err := am.GetUserInviteInfo(context.Background(), result.InviteToken)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, info)
|
|
|
|
assert.Equal(t, "newuser@test.com", info.Email)
|
|
assert.Equal(t, "New User", info.Name)
|
|
assert.True(t, info.Valid)
|
|
assert.Equal(t, "Admin User", info.InvitedBy)
|
|
}
|
|
|
|
func TestGetUserInviteInfo_InvalidToken(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
_, err := am.GetUserInviteInfo(context.Background(), "invalid_token")
|
|
require.Error(t, err)
|
|
|
|
sErr, ok := status.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, status.InvalidArgument, sErr.Type())
|
|
}
|
|
|
|
func TestGetUserInviteInfo_TokenNotFound(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
// Generate a valid token format that doesn't exist in DB
|
|
_, validToken, err := types.GenerateInviteToken()
|
|
require.NoError(t, err)
|
|
|
|
_, err = am.GetUserInviteInfo(context.Background(), validToken)
|
|
require.Error(t, err)
|
|
|
|
sErr, ok := status.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, status.NotFound, sErr.Type())
|
|
}
|
|
|
|
func TestGetUserInviteInfo_ExpiredInvite(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
// Create an invite with valid expiration
|
|
invite := &types.UserInfo{
|
|
Email: "newuser@test.com",
|
|
Name: "New User",
|
|
Role: "user",
|
|
AutoGroups: []string{},
|
|
}
|
|
|
|
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
|
|
require.NoError(t, err)
|
|
|
|
// Manually set the invite to expired by updating the store directly
|
|
inviteRecord, err := am.Store.GetUserInviteByID(context.Background(), store.LockingStrengthUpdate, testAccountID, result.UserInfo.ID)
|
|
require.NoError(t, err)
|
|
inviteRecord.ExpiresAt = time.Now().Add(-time.Hour) // Set to 1 hour ago
|
|
err = am.Store.SaveUserInvite(context.Background(), inviteRecord)
|
|
require.NoError(t, err)
|
|
|
|
// Get the invite info - should still return info but Valid should be false
|
|
info, err := am.GetUserInviteInfo(context.Background(), result.InviteToken)
|
|
require.NoError(t, err)
|
|
assert.False(t, info.Valid)
|
|
}
|
|
|
|
func TestListUserInvites_Success(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
// Create multiple invites
|
|
for i, email := range []string{"user1@test.com", "user2@test.com", "user3@test.com"} {
|
|
invite := &types.UserInfo{
|
|
Email: email,
|
|
Name: "User " + string(rune('1'+i)),
|
|
Role: "user",
|
|
AutoGroups: []string{},
|
|
}
|
|
_, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// List invites
|
|
invites, err := am.ListUserInvites(context.Background(), testAccountID, testAdminUserID)
|
|
require.NoError(t, err)
|
|
assert.Len(t, invites, 3)
|
|
}
|
|
|
|
func TestListUserInvites_Empty(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
invites, err := am.ListUserInvites(context.Background(), testAccountID, testAdminUserID)
|
|
require.NoError(t, err)
|
|
assert.Len(t, invites, 0)
|
|
}
|
|
|
|
func TestListUserInvites_PermissionDenied(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
_, err := am.ListUserInvites(context.Background(), testAccountID, testRegularUserID)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestRegenerateUserInvite_Success(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
// Create an invite first
|
|
invite := &types.UserInfo{
|
|
Email: "newuser@test.com",
|
|
Name: "New User",
|
|
Role: "user",
|
|
AutoGroups: []string{},
|
|
}
|
|
|
|
originalResult, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
|
|
require.NoError(t, err)
|
|
|
|
// Regenerate the invite
|
|
newResult, err := am.RegenerateUserInvite(context.Background(), testAccountID, testAdminUserID, originalResult.UserInfo.ID, 0)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, newResult)
|
|
|
|
// Verify invite ID remains the same (stable ID for clients)
|
|
assert.Equal(t, originalResult.UserInfo.ID, newResult.UserInfo.ID)
|
|
|
|
// Verify new token is different
|
|
assert.NotEqual(t, originalResult.InviteToken, newResult.InviteToken)
|
|
assert.Equal(t, "newuser@test.com", newResult.UserInfo.Email)
|
|
assert.Equal(t, "New User", newResult.UserInfo.Name)
|
|
|
|
// Verify old token no longer works
|
|
_, err = am.GetUserInviteInfo(context.Background(), originalResult.InviteToken)
|
|
require.Error(t, err)
|
|
|
|
// Verify new token works
|
|
info, err := am.GetUserInviteInfo(context.Background(), newResult.InviteToken)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "newuser@test.com", info.Email)
|
|
}
|
|
|
|
func TestRegenerateUserInvite_NotFound(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
_, err := am.RegenerateUserInvite(context.Background(), testAccountID, testAdminUserID, "nonexistent-id", 0)
|
|
require.Error(t, err)
|
|
|
|
sErr, ok := status.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, status.NotFound, sErr.Type())
|
|
}
|
|
|
|
func TestRegenerateUserInvite_PermissionDenied(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
// Create an invite first
|
|
invite := &types.UserInfo{
|
|
Email: "newuser@test.com",
|
|
Name: "New User",
|
|
Role: "user",
|
|
AutoGroups: []string{},
|
|
}
|
|
|
|
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
|
|
require.NoError(t, err)
|
|
|
|
// Regular user should not be able to regenerate
|
|
_, err = am.RegenerateUserInvite(context.Background(), testAccountID, testRegularUserID, result.UserInfo.ID, 0)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestDeleteUserInvite_Success(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
// Create an invite first
|
|
invite := &types.UserInfo{
|
|
Email: "newuser@test.com",
|
|
Name: "New User",
|
|
Role: "user",
|
|
AutoGroups: []string{},
|
|
}
|
|
|
|
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
|
|
require.NoError(t, err)
|
|
|
|
// Delete the invite
|
|
err = am.DeleteUserInvite(context.Background(), testAccountID, testAdminUserID, result.UserInfo.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify invite is deleted
|
|
invites, err := am.Store.GetAccountUserInvites(context.Background(), store.LockingStrengthNone, testAccountID)
|
|
require.NoError(t, err)
|
|
assert.Len(t, invites, 0)
|
|
|
|
// Verify token no longer works
|
|
_, err = am.GetUserInviteInfo(context.Background(), result.InviteToken)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestDeleteUserInvite_NotFound(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
err := am.DeleteUserInvite(context.Background(), testAccountID, testAdminUserID, "nonexistent-id")
|
|
require.Error(t, err)
|
|
|
|
sErr, ok := status.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, status.NotFound, sErr.Type())
|
|
}
|
|
|
|
func TestDeleteUserInvite_PermissionDenied(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
// Create an invite first
|
|
invite := &types.UserInfo{
|
|
Email: "newuser@test.com",
|
|
Name: "New User",
|
|
Role: "user",
|
|
AutoGroups: []string{},
|
|
}
|
|
|
|
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
|
|
require.NoError(t, err)
|
|
|
|
// Regular user should not be able to delete
|
|
err = am.DeleteUserInvite(context.Background(), testAccountID, testRegularUserID, result.UserInfo.ID)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestDeleteUserInvite_WrongAccount(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
// Create an invite
|
|
invite := &types.UserInfo{
|
|
Email: "newuser@test.com",
|
|
Name: "New User",
|
|
Role: "user",
|
|
AutoGroups: []string{},
|
|
}
|
|
|
|
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
|
|
require.NoError(t, err)
|
|
|
|
// Create another account
|
|
anotherAccountID := "anotherAccountID"
|
|
anotherAdminID := "anotherAdminID"
|
|
anotherAccount := newAccountWithId(context.Background(), anotherAccountID, anotherAdminID, "", "otheradmin@test.com", "Other Admin", false)
|
|
err = am.Store.SaveAccount(context.Background(), anotherAccount)
|
|
require.NoError(t, err)
|
|
|
|
// Try to delete from wrong account
|
|
err = am.DeleteUserInvite(context.Background(), anotherAccountID, anotherAdminID, result.UserInfo.ID)
|
|
require.Error(t, err)
|
|
|
|
sErr, ok := status.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, status.NotFound, sErr.Type())
|
|
}
|
|
|
|
func TestAcceptUserInvite_Success(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
// Create an invite
|
|
invite := &types.UserInfo{
|
|
Email: "newuser@test.com",
|
|
Name: "New User",
|
|
Role: "user",
|
|
AutoGroups: []string{},
|
|
}
|
|
|
|
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
|
|
require.NoError(t, err)
|
|
|
|
// Accept the invite with a valid password
|
|
err = am.AcceptUserInvite(context.Background(), result.InviteToken, "Password1!")
|
|
require.NoError(t, err)
|
|
|
|
// Verify user is created in DB
|
|
users, err := am.Store.GetAccountUsers(context.Background(), store.LockingStrengthNone, testAccountID)
|
|
require.NoError(t, err)
|
|
|
|
var foundUser *types.User
|
|
for _, u := range users {
|
|
if u.Email == "newuser@test.com" {
|
|
foundUser = u
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, foundUser, "User should be created in DB")
|
|
assert.Equal(t, "New User", foundUser.Name)
|
|
assert.Equal(t, types.UserRoleUser, foundUser.Role)
|
|
|
|
// Verify invite is deleted
|
|
invites, err := am.Store.GetAccountUserInvites(context.Background(), store.LockingStrengthNone, testAccountID)
|
|
require.NoError(t, err)
|
|
assert.Len(t, invites, 0)
|
|
}
|
|
|
|
func TestAcceptUserInvite_InvalidToken(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
err := am.AcceptUserInvite(context.Background(), "invalid_token", "Password1!")
|
|
require.Error(t, err)
|
|
|
|
sErr, ok := status.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, status.InvalidArgument, sErr.Type())
|
|
}
|
|
|
|
func TestAcceptUserInvite_TokenNotFound(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
// Generate a valid token format that doesn't exist in DB
|
|
_, validToken, err := types.GenerateInviteToken()
|
|
require.NoError(t, err)
|
|
|
|
err = am.AcceptUserInvite(context.Background(), validToken, "Password1!")
|
|
require.Error(t, err)
|
|
|
|
sErr, ok := status.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, status.NotFound, sErr.Type())
|
|
}
|
|
|
|
func TestAcceptUserInvite_ExpiredToken(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
// Create an invite with valid expiration
|
|
invite := &types.UserInfo{
|
|
Email: "newuser@test.com",
|
|
Name: "New User",
|
|
Role: "user",
|
|
AutoGroups: []string{},
|
|
}
|
|
|
|
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
|
|
require.NoError(t, err)
|
|
|
|
// Manually set the invite to expired by updating the store directly
|
|
inviteRecord, err := am.Store.GetUserInviteByID(context.Background(), store.LockingStrengthUpdate, testAccountID, result.UserInfo.ID)
|
|
require.NoError(t, err)
|
|
inviteRecord.ExpiresAt = time.Now().Add(-time.Hour) // Set to 1 hour ago
|
|
err = am.Store.SaveUserInvite(context.Background(), inviteRecord)
|
|
require.NoError(t, err)
|
|
|
|
err = am.AcceptUserInvite(context.Background(), result.InviteToken, "Password1!")
|
|
require.Error(t, err)
|
|
|
|
sErr, ok := status.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, status.InvalidArgument, sErr.Type())
|
|
assert.Contains(t, err.Error(), "expired")
|
|
}
|
|
|
|
func TestAcceptUserInvite_EmptyPassword(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
// Create an invite
|
|
invite := &types.UserInfo{
|
|
Email: "newuser@test.com",
|
|
Name: "New User",
|
|
Role: "user",
|
|
AutoGroups: []string{},
|
|
}
|
|
|
|
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
|
|
require.NoError(t, err)
|
|
|
|
err = am.AcceptUserInvite(context.Background(), result.InviteToken, "")
|
|
require.Error(t, err)
|
|
|
|
sErr, ok := status.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, status.InvalidArgument, sErr.Type())
|
|
assert.Contains(t, err.Error(), "password is required")
|
|
}
|
|
|
|
func TestAcceptUserInvite_WeakPassword(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
// Create an invite
|
|
invite := &types.UserInfo{
|
|
Email: "newuser@test.com",
|
|
Name: "New User",
|
|
Role: "user",
|
|
AutoGroups: []string{},
|
|
}
|
|
|
|
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
|
|
require.NoError(t, err)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
password string
|
|
expectedMsg string
|
|
}{
|
|
{"too short", "Pass1!", "at least 8 characters"},
|
|
{"no digit", "Password!", "one digit"},
|
|
{"no uppercase", "password1!", "one uppercase"},
|
|
{"no special", "Password1", "one special character"},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := am.AcceptUserInvite(context.Background(), result.InviteToken, tc.password)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tc.expectedMsg)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidatePassword(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
password string
|
|
expectError bool
|
|
errorMsg string
|
|
}{
|
|
{"valid password", "Password1!", false, ""},
|
|
{"valid complex password", "MyP@ssw0rd#2024", false, ""},
|
|
{"too short", "Pass1!", true, "at least 8 characters"},
|
|
{"no digit", "Password!", true, "one digit"},
|
|
{"no uppercase", "password1!", true, "one uppercase"},
|
|
{"no special", "Password1", true, "one special character"},
|
|
{"only lowercase", "password", true, "one digit"},
|
|
{"no uppercase no special", "password1", true, "one uppercase"},
|
|
{"all lowercase short", "pass", true, "at least 8 characters"},
|
|
{"empty", "", true, "at least 8 characters"},
|
|
{"spaces count as special", "Pass word1", false, ""},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := validatePassword(tc.password)
|
|
if tc.expectError {
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tc.errorMsg)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInviteToken_GenerateAndValidate(t *testing.T) {
|
|
hashedToken, plainToken, err := types.GenerateInviteToken()
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, hashedToken)
|
|
require.NotEmpty(t, plainToken)
|
|
|
|
// Validate token format
|
|
assert.Len(t, plainToken, types.InviteTokenLength)
|
|
assert.True(t, len(plainToken) > len(types.InviteTokenPrefix))
|
|
assert.Equal(t, types.InviteTokenPrefix, plainToken[:len(types.InviteTokenPrefix)])
|
|
|
|
// Validate checksum
|
|
err = types.ValidateInviteToken(plainToken)
|
|
require.NoError(t, err)
|
|
|
|
// Verify hashing is consistent
|
|
hashedAgain := types.HashInviteToken(plainToken)
|
|
assert.Equal(t, hashedToken, hashedAgain)
|
|
}
|
|
|
|
func TestInviteToken_ValidateInvalid(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
token string
|
|
}{
|
|
{"empty", ""},
|
|
{"too short", "nbi_abc"},
|
|
{"wrong prefix", "xyz_123456789012345678901234567890"},
|
|
{"invalid checksum", "nbi_123456789012345678901234567890abcdef"},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := types.ValidateInviteToken(tc.token)
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUserInviteRecord_IsExpired(t *testing.T) {
|
|
// Not expired
|
|
invite := &types.UserInviteRecord{
|
|
ExpiresAt: time.Now().Add(time.Hour),
|
|
}
|
|
assert.False(t, invite.IsExpired())
|
|
|
|
// Expired
|
|
invite = &types.UserInviteRecord{
|
|
ExpiresAt: time.Now().Add(-time.Hour),
|
|
}
|
|
assert.True(t, invite.IsExpired())
|
|
}
|
|
|
|
func TestUserInviteRecord_Copy(t *testing.T) {
|
|
original := &types.UserInviteRecord{
|
|
ID: "invite-id",
|
|
AccountID: "account-id",
|
|
Email: "test@example.com",
|
|
Name: "Test User",
|
|
Role: "user",
|
|
AutoGroups: []string{"group1", "group2"},
|
|
HashedToken: "hashed-token",
|
|
ExpiresAt: time.Now().Add(time.Hour),
|
|
CreatedAt: time.Now(),
|
|
CreatedBy: "creator-id",
|
|
}
|
|
|
|
copied := original.Copy()
|
|
|
|
assert.Equal(t, original.ID, copied.ID)
|
|
assert.Equal(t, original.AccountID, copied.AccountID)
|
|
assert.Equal(t, original.Email, copied.Email)
|
|
assert.Equal(t, original.Name, copied.Name)
|
|
assert.Equal(t, original.Role, copied.Role)
|
|
assert.Equal(t, original.AutoGroups, copied.AutoGroups)
|
|
assert.Equal(t, original.HashedToken, copied.HashedToken)
|
|
assert.Equal(t, original.ExpiresAt, copied.ExpiresAt)
|
|
assert.Equal(t, original.CreatedAt, copied.CreatedAt)
|
|
assert.Equal(t, original.CreatedBy, copied.CreatedBy)
|
|
|
|
// Verify deep copy of AutoGroups
|
|
copied.AutoGroups[0] = "modified"
|
|
assert.NotEqual(t, original.AutoGroups[0], copied.AutoGroups[0])
|
|
}
|
|
|
|
func TestCreateUserInvite_NonEmbeddedIdP(t *testing.T) {
|
|
s, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
|
|
require.NoError(t, err)
|
|
defer cleanup()
|
|
|
|
account := newAccountWithId(context.Background(), testAccountID, testAdminUserID, "", "admin@test.com", "Admin User", false)
|
|
err = s.SaveAccount(context.Background(), account)
|
|
require.NoError(t, err)
|
|
|
|
permissionsManager := permissions.NewManager(s)
|
|
|
|
// Use nil IDP manager (non-embedded)
|
|
am := DefaultAccountManager{
|
|
Store: s,
|
|
eventStore: &activity.InMemoryEventStore{},
|
|
permissionsManager: permissionsManager,
|
|
idpManager: nil,
|
|
}
|
|
|
|
invite := &types.UserInfo{
|
|
Email: "newuser@test.com",
|
|
Name: "New User",
|
|
Role: "user",
|
|
AutoGroups: []string{},
|
|
}
|
|
|
|
_, err = am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
|
|
require.Error(t, err)
|
|
|
|
sErr, ok := status.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, status.PreconditionFailed, sErr.Type())
|
|
assert.Contains(t, err.Error(), "embedded identity provider")
|
|
}
|
|
|
|
func TestAcceptUserInvite_WithAutoGroups(t *testing.T) {
|
|
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
|
|
defer cleanup()
|
|
|
|
// Create an invite with auto groups
|
|
invite := &types.UserInfo{
|
|
Email: "newuser@test.com",
|
|
Name: "New User",
|
|
Role: "admin",
|
|
AutoGroups: []string{"group1", "group2"},
|
|
}
|
|
|
|
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
|
|
require.NoError(t, err)
|
|
|
|
// Accept the invite
|
|
err = am.AcceptUserInvite(context.Background(), result.InviteToken, "Password1!")
|
|
require.NoError(t, err)
|
|
|
|
// Verify user has the auto groups and role
|
|
users, err := am.Store.GetAccountUsers(context.Background(), store.LockingStrengthNone, testAccountID)
|
|
require.NoError(t, err)
|
|
|
|
var foundUser *types.User
|
|
for _, u := range users {
|
|
if u.Email == "newuser@test.com" {
|
|
foundUser = u
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, foundUser)
|
|
assert.Equal(t, types.UserRoleAdmin, foundUser.Role)
|
|
assert.Equal(t, []string{"group1", "group2"}, foundUser.AutoGroups)
|
|
}
|
|
|
|
func TestUserInvite_EncryptDecryptSensitiveData(t *testing.T) {
|
|
key, err := crypt.GenerateKey()
|
|
require.NoError(t, err)
|
|
fieldEncrypt, err := crypt.NewFieldEncrypt(key)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("encrypt and decrypt", func(t *testing.T) {
|
|
invite := &types.UserInviteRecord{
|
|
ID: "test-invite",
|
|
AccountID: "test-account",
|
|
Email: "test@example.com",
|
|
Name: "Test User",
|
|
Role: "user",
|
|
}
|
|
|
|
// Encrypt
|
|
err := invite.EncryptSensitiveData(fieldEncrypt)
|
|
require.NoError(t, err)
|
|
|
|
// Verify encrypted values are different from original
|
|
assert.NotEqual(t, "test@example.com", invite.Email)
|
|
assert.NotEqual(t, "Test User", invite.Name)
|
|
|
|
// Decrypt
|
|
err = invite.DecryptSensitiveData(fieldEncrypt)
|
|
require.NoError(t, err)
|
|
|
|
// Verify decrypted values match original
|
|
assert.Equal(t, "test@example.com", invite.Email)
|
|
assert.Equal(t, "Test User", invite.Name)
|
|
})
|
|
|
|
t.Run("encrypt empty fields", func(t *testing.T) {
|
|
invite := &types.UserInviteRecord{
|
|
ID: "test-invite",
|
|
AccountID: "test-account",
|
|
Email: "",
|
|
Name: "",
|
|
Role: "user",
|
|
}
|
|
|
|
// Encrypt empty fields
|
|
err := invite.EncryptSensitiveData(fieldEncrypt)
|
|
require.NoError(t, err)
|
|
|
|
// Empty strings should remain empty
|
|
assert.Equal(t, "", invite.Email)
|
|
assert.Equal(t, "", invite.Name)
|
|
|
|
// Decrypt empty fields
|
|
err = invite.DecryptSensitiveData(fieldEncrypt)
|
|
require.NoError(t, err)
|
|
|
|
// Should still be empty
|
|
assert.Equal(t, "", invite.Email)
|
|
assert.Equal(t, "", invite.Name)
|
|
})
|
|
|
|
t.Run("nil encryptor", func(t *testing.T) {
|
|
invite := &types.UserInviteRecord{
|
|
ID: "test-invite",
|
|
AccountID: "test-account",
|
|
Email: "test@example.com",
|
|
Name: "Test User",
|
|
Role: "user",
|
|
}
|
|
|
|
// Encrypt with nil encryptor should be no-op
|
|
err := invite.EncryptSensitiveData(nil)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "test@example.com", invite.Email)
|
|
assert.Equal(t, "Test User", invite.Name)
|
|
|
|
// Decrypt with nil encryptor should be no-op
|
|
err = invite.DecryptSensitiveData(nil)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "test@example.com", invite.Email)
|
|
assert.Equal(t, "Test User", invite.Name)
|
|
})
|
|
}
|