mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-20 20:39:08 +02:00
Merge branch 'refs/heads/main' into prototype/reverse-proxy
# Conflicts: # management/server/activity/codes.go # management/server/http/handler.go # management/server/store/sql_store.go # management/server/store/store.go # shared/management/http/api/openapi.yml # shared/management/http/api/types.gen.go # shared/management/proto/management.pb.go
This commit is contained in:
@@ -44,14 +44,15 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
storeSqliteFileName = "store.db"
|
||||
idQueryCondition = "id = ?"
|
||||
keyQueryCondition = "key = ?"
|
||||
mysqlKeyQueryCondition = "`key` = ?"
|
||||
accountAndIDQueryCondition = "account_id = ? and id = ?"
|
||||
accountAndIDsQueryCondition = "account_id = ? AND id IN ?"
|
||||
accountIDCondition = "account_id = ?"
|
||||
peerNotFoundFMT = "peer %s not found"
|
||||
storeSqliteFileName = "store.db"
|
||||
idQueryCondition = "id = ?"
|
||||
keyQueryCondition = "key = ?"
|
||||
mysqlKeyQueryCondition = "`key` = ?"
|
||||
accountAndIDQueryCondition = "account_id = ? and id = ?"
|
||||
accountAndPeerIDQueryCondition = "account_id = ? and peer_id = ?"
|
||||
accountAndIDsQueryCondition = "account_id = ? AND id IN ?"
|
||||
accountIDCondition = "account_id = ?"
|
||||
peerNotFoundFMT = "peer %s not found"
|
||||
|
||||
pgMaxConnections = 30
|
||||
pgMinConnections = 1
|
||||
@@ -126,7 +127,7 @@ func NewSqlStore(ctx context.Context, db *gorm.DB, storeEngine types.Engine, met
|
||||
&types.Account{}, &types.Policy{}, &types.PolicyRule{}, &route.Route{}, &nbdns.NameServerGroup{},
|
||||
&installation{}, &types.ExtraSettings{}, &posture.Checks{}, &nbpeer.NetworkAddress{},
|
||||
&networkTypes.Network{}, &routerTypes.NetworkRouter{}, &resourceTypes.NetworkResource{}, &types.AccountOnboarding{},
|
||||
&zones.Zone{}, &records.Record{}, &services.Service{},
|
||||
&types.Job{}, &zones.Zone{}, &records.Record{}, &types.UserInviteRecord{}, &services.Service{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("auto migratePreAuto: %w", err)
|
||||
@@ -145,6 +146,97 @@ func GetKeyQueryCondition(s *SqlStore) string {
|
||||
return keyQueryCondition
|
||||
}
|
||||
|
||||
// SaveJob persists a job in DB
|
||||
func (s *SqlStore) CreatePeerJob(ctx context.Context, job *types.Job) error {
|
||||
result := s.db.Create(job)
|
||||
if result.Error != nil {
|
||||
log.WithContext(ctx).Errorf("failed to create job in store: %s", result.Error)
|
||||
return status.Errorf(status.Internal, "failed to create job in store")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SqlStore) CompletePeerJob(ctx context.Context, job *types.Job) error {
|
||||
result := s.db.
|
||||
Model(&types.Job{}).
|
||||
Where(idQueryCondition, job.ID).
|
||||
Updates(job)
|
||||
|
||||
if result.Error != nil {
|
||||
log.WithContext(ctx).Errorf("failed to update job in store: %s", result.Error)
|
||||
return status.Errorf(status.Internal, "failed to update job in store")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// job was pending for too long and has been cancelled
|
||||
func (s *SqlStore) MarkPendingJobsAsFailed(ctx context.Context, accountID, peerID, jobID, reason string) error {
|
||||
now := time.Now().UTC()
|
||||
result := s.db.
|
||||
Model(&types.Job{}).
|
||||
Where(accountAndPeerIDQueryCondition+" AND id = ?"+" AND status = ?", accountID, peerID, jobID, types.JobStatusPending).
|
||||
Updates(types.Job{
|
||||
Status: types.JobStatusFailed,
|
||||
FailedReason: reason,
|
||||
CompletedAt: &now,
|
||||
})
|
||||
if result.Error != nil {
|
||||
log.WithContext(ctx).Errorf("failed to mark pending jobs as Failed job in store: %s", result.Error)
|
||||
return status.Errorf(status.Internal, "failed to mark pending job as Failed in store")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// job was pending for too long and has been cancelled
|
||||
func (s *SqlStore) MarkAllPendingJobsAsFailed(ctx context.Context, accountID, peerID, reason string) error {
|
||||
now := time.Now().UTC()
|
||||
result := s.db.
|
||||
Model(&types.Job{}).
|
||||
Where(accountAndPeerIDQueryCondition+" AND status = ?", accountID, peerID, types.JobStatusPending).
|
||||
Updates(types.Job{
|
||||
Status: types.JobStatusFailed,
|
||||
FailedReason: reason,
|
||||
CompletedAt: &now,
|
||||
})
|
||||
if result.Error != nil {
|
||||
log.WithContext(ctx).Errorf("failed to mark pending jobs as Failed job in store: %s", result.Error)
|
||||
return status.Errorf(status.Internal, "failed to mark pending job as Failed in store")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetJobByID fetches job by ID
|
||||
func (s *SqlStore) GetPeerJobByID(ctx context.Context, accountID, jobID string) (*types.Job, error) {
|
||||
var job types.Job
|
||||
err := s.db.
|
||||
Where(accountAndIDQueryCondition, accountID, jobID).
|
||||
First(&job).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, status.Errorf(status.NotFound, "job %s not found", jobID)
|
||||
}
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("failed to fetch job from store: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
return &job, nil
|
||||
}
|
||||
|
||||
// get all jobs
|
||||
func (s *SqlStore) GetPeerJobs(ctx context.Context, accountID, peerID string) ([]*types.Job, error) {
|
||||
var jobs []*types.Job
|
||||
err := s.db.
|
||||
Where(accountAndPeerIDQueryCondition, accountID, peerID).
|
||||
Order("created_at DESC").
|
||||
Find(&jobs).Error
|
||||
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("failed to fetch jobs from store: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return jobs, nil
|
||||
}
|
||||
|
||||
// AcquireGlobalLock acquires global lock across all the accounts and returns a function that releases the lock
|
||||
func (s *SqlStore) AcquireGlobalLock(ctx context.Context) (unlock func()) {
|
||||
log.WithContext(ctx).Tracef("acquiring global lock")
|
||||
@@ -724,6 +816,130 @@ func (s *SqlStore) GetAccountOwner(ctx context.Context, lockStrength LockingStre
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// SaveUserInvite saves a user invite to the database
|
||||
func (s *SqlStore) SaveUserInvite(ctx context.Context, invite *types.UserInviteRecord) error {
|
||||
inviteCopy := invite.Copy()
|
||||
if err := inviteCopy.EncryptSensitiveData(s.fieldEncrypt); err != nil {
|
||||
return fmt.Errorf("encrypt invite: %w", err)
|
||||
}
|
||||
|
||||
result := s.db.Save(inviteCopy)
|
||||
if result.Error != nil {
|
||||
log.WithContext(ctx).Errorf("failed to save user invite to store: %s", result.Error)
|
||||
return status.Errorf(status.Internal, "failed to save user invite to store")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserInviteByID retrieves a user invite by its ID and account ID
|
||||
func (s *SqlStore) GetUserInviteByID(ctx context.Context, lockStrength LockingStrength, accountID, inviteID string) (*types.UserInviteRecord, error) {
|
||||
tx := s.db
|
||||
if lockStrength != LockingStrengthNone {
|
||||
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
|
||||
}
|
||||
|
||||
var invite types.UserInviteRecord
|
||||
result := tx.Where("account_id = ?", accountID).Take(&invite, idQueryCondition, inviteID)
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, status.Errorf(status.NotFound, "user invite not found")
|
||||
}
|
||||
log.WithContext(ctx).Errorf("failed to get user invite from store: %s", result.Error)
|
||||
return nil, status.Errorf(status.Internal, "failed to get user invite from store")
|
||||
}
|
||||
|
||||
if err := invite.DecryptSensitiveData(s.fieldEncrypt); err != nil {
|
||||
return nil, fmt.Errorf("decrypt invite: %w", err)
|
||||
}
|
||||
|
||||
return &invite, nil
|
||||
}
|
||||
|
||||
// GetUserInviteByHashedToken retrieves a user invite by its hashed token
|
||||
func (s *SqlStore) GetUserInviteByHashedToken(ctx context.Context, lockStrength LockingStrength, hashedToken string) (*types.UserInviteRecord, error) {
|
||||
tx := s.db
|
||||
if lockStrength != LockingStrengthNone {
|
||||
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
|
||||
}
|
||||
|
||||
var invite types.UserInviteRecord
|
||||
result := tx.Take(&invite, "hashed_token = ?", hashedToken)
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, status.Errorf(status.NotFound, "user invite not found")
|
||||
}
|
||||
log.WithContext(ctx).Errorf("failed to get user invite from store: %s", result.Error)
|
||||
return nil, status.Errorf(status.Internal, "failed to get user invite from store")
|
||||
}
|
||||
|
||||
if err := invite.DecryptSensitiveData(s.fieldEncrypt); err != nil {
|
||||
return nil, fmt.Errorf("decrypt invite: %w", err)
|
||||
}
|
||||
|
||||
return &invite, nil
|
||||
}
|
||||
|
||||
// GetUserInviteByEmail retrieves a user invite by account ID and email.
|
||||
// Since email is encrypted with random IVs, we fetch all invites for the account
|
||||
// and compare emails in memory after decryption.
|
||||
func (s *SqlStore) GetUserInviteByEmail(ctx context.Context, lockStrength LockingStrength, accountID, email string) (*types.UserInviteRecord, error) {
|
||||
tx := s.db
|
||||
if lockStrength != LockingStrengthNone {
|
||||
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
|
||||
}
|
||||
|
||||
var invites []*types.UserInviteRecord
|
||||
result := tx.Find(&invites, "account_id = ?", accountID)
|
||||
if result.Error != nil {
|
||||
log.WithContext(ctx).Errorf("failed to get user invites from store: %s", result.Error)
|
||||
return nil, status.Errorf(status.Internal, "failed to get user invites from store")
|
||||
}
|
||||
|
||||
for _, invite := range invites {
|
||||
if err := invite.DecryptSensitiveData(s.fieldEncrypt); err != nil {
|
||||
return nil, fmt.Errorf("decrypt invite: %w", err)
|
||||
}
|
||||
if strings.EqualFold(invite.Email, email) {
|
||||
return invite, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, status.Errorf(status.NotFound, "user invite not found for email")
|
||||
}
|
||||
|
||||
// GetAccountUserInvites retrieves all user invites for an account
|
||||
func (s *SqlStore) GetAccountUserInvites(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.UserInviteRecord, error) {
|
||||
tx := s.db
|
||||
if lockStrength != LockingStrengthNone {
|
||||
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
|
||||
}
|
||||
|
||||
var invites []*types.UserInviteRecord
|
||||
result := tx.Find(&invites, "account_id = ?", accountID)
|
||||
if result.Error != nil {
|
||||
log.WithContext(ctx).Errorf("failed to get user invites from store: %s", result.Error)
|
||||
return nil, status.Errorf(status.Internal, "failed to get user invites from store")
|
||||
}
|
||||
|
||||
for _, invite := range invites {
|
||||
if err := invite.DecryptSensitiveData(s.fieldEncrypt); err != nil {
|
||||
return nil, fmt.Errorf("decrypt invite: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return invites, nil
|
||||
}
|
||||
|
||||
// DeleteUserInvite deletes a user invite by its ID
|
||||
func (s *SqlStore) DeleteUserInvite(ctx context.Context, inviteID string) error {
|
||||
result := s.db.Delete(&types.UserInviteRecord{}, idQueryCondition, inviteID)
|
||||
if result.Error != nil {
|
||||
log.WithContext(ctx).Errorf("failed to delete user invite from store: %s", result.Error)
|
||||
return status.Errorf(status.Internal, "failed to delete user invite from store")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SqlStore) GetAccountGroups(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.Group, error) {
|
||||
tx := s.db
|
||||
if lockStrength != LockingStrengthNone {
|
||||
@@ -4178,6 +4394,9 @@ func (s *SqlStore) GetUserIDByPeerKey(ctx context.Context, lockStrength LockingS
|
||||
Take(&userID, GetKeyQueryCondition(s), peerKey)
|
||||
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
return "", status.Errorf(status.NotFound, "peer not found: index lookup failed")
|
||||
}
|
||||
return "", status.Errorf(status.Internal, "failed to get user ID by peer key")
|
||||
}
|
||||
|
||||
@@ -4365,6 +4584,26 @@ func (s *SqlStore) DeleteZoneDNSRecords(ctx context.Context, accountID, zoneID s
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SqlStore) GetPeerIDByKey(ctx context.Context, lockStrength LockingStrength, key string) (string, error) {
|
||||
tx := s.db
|
||||
if lockStrength != LockingStrengthNone {
|
||||
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
|
||||
}
|
||||
|
||||
var peerID string
|
||||
result := tx.Model(&nbpeer.Peer{}).
|
||||
Select("id").
|
||||
Where(GetKeyQueryCondition(s), key).
|
||||
Limit(1).
|
||||
Scan(&peerID)
|
||||
if result.Error != nil {
|
||||
log.WithContext(ctx).Errorf("failed to get peer ID by key: %s", result.Error)
|
||||
return "", status.Errorf(status.Internal, "failed to get peer ID by key")
|
||||
}
|
||||
|
||||
return peerID, nil
|
||||
}
|
||||
|
||||
func (s *SqlStore) CreateService(ctx context.Context, service *services.Service) error {
|
||||
result := s.db.Create(service)
|
||||
if result.Error != nil {
|
||||
|
||||
@@ -0,0 +1,520 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
)
|
||||
|
||||
func TestSqlStore_SaveUserInvite(t *testing.T) {
|
||||
runTestForAllEngines(t, "", func(t *testing.T, store Store) {
|
||||
if store == nil {
|
||||
t.Skip("store is nil")
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
invite := &types.UserInviteRecord{
|
||||
ID: "invite-1",
|
||||
AccountID: "account-1",
|
||||
Email: "test@example.com",
|
||||
Name: "Test User",
|
||||
Role: "user",
|
||||
AutoGroups: []string{"group-1", "group-2"},
|
||||
HashedToken: "hashed-token-123",
|
||||
ExpiresAt: time.Now().Add(72 * time.Hour),
|
||||
CreatedAt: time.Now(),
|
||||
CreatedBy: "admin-user",
|
||||
}
|
||||
|
||||
err := store.SaveUserInvite(ctx, invite)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the invite was saved
|
||||
retrieved, err := store.GetUserInviteByID(ctx, LockingStrengthNone, invite.AccountID, invite.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, invite.ID, retrieved.ID)
|
||||
assert.Equal(t, invite.Email, retrieved.Email)
|
||||
assert.Equal(t, invite.Name, retrieved.Name)
|
||||
assert.Equal(t, invite.Role, retrieved.Role)
|
||||
assert.Equal(t, invite.AutoGroups, retrieved.AutoGroups)
|
||||
assert.Equal(t, invite.CreatedBy, retrieved.CreatedBy)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSqlStore_SaveUserInvite_Update(t *testing.T) {
|
||||
runTestForAllEngines(t, "", func(t *testing.T, store Store) {
|
||||
if store == nil {
|
||||
t.Skip("store is nil")
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
invite := &types.UserInviteRecord{
|
||||
ID: "invite-update",
|
||||
AccountID: "account-1",
|
||||
Email: "test@example.com",
|
||||
Name: "Test User",
|
||||
Role: "user",
|
||||
AutoGroups: []string{"group-1"},
|
||||
HashedToken: "hashed-token-123",
|
||||
ExpiresAt: time.Now().Add(72 * time.Hour),
|
||||
CreatedAt: time.Now(),
|
||||
CreatedBy: "admin-user",
|
||||
}
|
||||
|
||||
err := store.SaveUserInvite(ctx, invite)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Update the invite with a new token
|
||||
invite.HashedToken = "new-hashed-token"
|
||||
invite.ExpiresAt = time.Now().Add(24 * time.Hour)
|
||||
|
||||
err = store.SaveUserInvite(ctx, invite)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the update
|
||||
retrieved, err := store.GetUserInviteByID(ctx, LockingStrengthNone, invite.AccountID, invite.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "new-hashed-token", retrieved.HashedToken)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSqlStore_GetUserInviteByID(t *testing.T) {
|
||||
runTestForAllEngines(t, "", func(t *testing.T, store Store) {
|
||||
if store == nil {
|
||||
t.Skip("store is nil")
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
invite := &types.UserInviteRecord{
|
||||
ID: "invite-get-by-id",
|
||||
AccountID: "account-1",
|
||||
Email: "getbyid@example.com",
|
||||
Name: "Get By ID User",
|
||||
Role: "admin",
|
||||
AutoGroups: []string{},
|
||||
HashedToken: "hashed-token-get",
|
||||
ExpiresAt: time.Now().Add(72 * time.Hour),
|
||||
CreatedAt: time.Now(),
|
||||
CreatedBy: "admin-user",
|
||||
}
|
||||
|
||||
err := store.SaveUserInvite(ctx, invite)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get by ID - success
|
||||
retrieved, err := store.GetUserInviteByID(ctx, LockingStrengthNone, invite.AccountID, invite.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, invite.ID, retrieved.ID)
|
||||
assert.Equal(t, invite.Email, retrieved.Email)
|
||||
|
||||
// Get by ID - wrong account
|
||||
_, err = store.GetUserInviteByID(ctx, LockingStrengthNone, "wrong-account", invite.ID)
|
||||
assert.Error(t, err)
|
||||
|
||||
// Get by ID - not found
|
||||
_, err = store.GetUserInviteByID(ctx, LockingStrengthNone, invite.AccountID, "non-existent")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSqlStore_GetUserInviteByHashedToken(t *testing.T) {
|
||||
runTestForAllEngines(t, "", func(t *testing.T, store Store) {
|
||||
if store == nil {
|
||||
t.Skip("store is nil")
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
invite := &types.UserInviteRecord{
|
||||
ID: "invite-get-by-token",
|
||||
AccountID: "account-1",
|
||||
Email: "getbytoken@example.com",
|
||||
Name: "Get By Token User",
|
||||
Role: "user",
|
||||
AutoGroups: []string{"group-1"},
|
||||
HashedToken: "unique-hashed-token-456",
|
||||
ExpiresAt: time.Now().Add(72 * time.Hour),
|
||||
CreatedAt: time.Now(),
|
||||
CreatedBy: "admin-user",
|
||||
}
|
||||
|
||||
err := store.SaveUserInvite(ctx, invite)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get by hashed token - success
|
||||
retrieved, err := store.GetUserInviteByHashedToken(ctx, LockingStrengthNone, invite.HashedToken)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, invite.ID, retrieved.ID)
|
||||
assert.Equal(t, invite.Email, retrieved.Email)
|
||||
|
||||
// Get by hashed token - not found
|
||||
_, err = store.GetUserInviteByHashedToken(ctx, LockingStrengthNone, "non-existent-token")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSqlStore_GetUserInviteByEmail(t *testing.T) {
|
||||
runTestForAllEngines(t, "", func(t *testing.T, store Store) {
|
||||
if store == nil {
|
||||
t.Skip("store is nil")
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
invite := &types.UserInviteRecord{
|
||||
ID: "invite-get-by-email",
|
||||
AccountID: "account-email-test",
|
||||
Email: "unique-email@example.com",
|
||||
Name: "Get By Email User",
|
||||
Role: "user",
|
||||
AutoGroups: []string{},
|
||||
HashedToken: "hashed-token-email",
|
||||
ExpiresAt: time.Now().Add(72 * time.Hour),
|
||||
CreatedAt: time.Now(),
|
||||
CreatedBy: "admin-user",
|
||||
}
|
||||
|
||||
err := store.SaveUserInvite(ctx, invite)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get by email - success
|
||||
retrieved, err := store.GetUserInviteByEmail(ctx, LockingStrengthNone, invite.AccountID, invite.Email)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, invite.ID, retrieved.ID)
|
||||
|
||||
// Get by email - case insensitive
|
||||
retrieved, err = store.GetUserInviteByEmail(ctx, LockingStrengthNone, invite.AccountID, "UNIQUE-EMAIL@EXAMPLE.COM")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, invite.ID, retrieved.ID)
|
||||
|
||||
// Get by email - wrong account
|
||||
_, err = store.GetUserInviteByEmail(ctx, LockingStrengthNone, "wrong-account", invite.Email)
|
||||
assert.Error(t, err)
|
||||
|
||||
// Get by email - not found
|
||||
_, err = store.GetUserInviteByEmail(ctx, LockingStrengthNone, invite.AccountID, "nonexistent@example.com")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSqlStore_GetAccountUserInvites(t *testing.T) {
|
||||
runTestForAllEngines(t, "", func(t *testing.T, store Store) {
|
||||
if store == nil {
|
||||
t.Skip("store is nil")
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
accountID := "account-list-invites"
|
||||
|
||||
invites := []*types.UserInviteRecord{
|
||||
{
|
||||
ID: "invite-list-1",
|
||||
AccountID: accountID,
|
||||
Email: "user1@example.com",
|
||||
Name: "User One",
|
||||
Role: "user",
|
||||
AutoGroups: []string{"group-1"},
|
||||
HashedToken: "hashed-token-list-1",
|
||||
ExpiresAt: time.Now().Add(72 * time.Hour),
|
||||
CreatedAt: time.Now(),
|
||||
CreatedBy: "admin-user",
|
||||
},
|
||||
{
|
||||
ID: "invite-list-2",
|
||||
AccountID: accountID,
|
||||
Email: "user2@example.com",
|
||||
Name: "User Two",
|
||||
Role: "admin",
|
||||
AutoGroups: []string{"group-2"},
|
||||
HashedToken: "hashed-token-list-2",
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
CreatedAt: time.Now(),
|
||||
CreatedBy: "admin-user",
|
||||
},
|
||||
{
|
||||
ID: "invite-list-3",
|
||||
AccountID: "different-account",
|
||||
Email: "user3@example.com",
|
||||
Name: "User Three",
|
||||
Role: "user",
|
||||
AutoGroups: []string{},
|
||||
HashedToken: "hashed-token-list-3",
|
||||
ExpiresAt: time.Now().Add(72 * time.Hour),
|
||||
CreatedAt: time.Now(),
|
||||
CreatedBy: "admin-user",
|
||||
},
|
||||
}
|
||||
|
||||
for _, invite := range invites {
|
||||
err := store.SaveUserInvite(ctx, invite)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Get all invites for the account
|
||||
retrieved, err := store.GetAccountUserInvites(ctx, LockingStrengthNone, accountID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, retrieved, 2)
|
||||
|
||||
// Verify the invites belong to the correct account
|
||||
for _, invite := range retrieved {
|
||||
assert.Equal(t, accountID, invite.AccountID)
|
||||
}
|
||||
|
||||
// Get invites for account with no invites
|
||||
retrieved, err = store.GetAccountUserInvites(ctx, LockingStrengthNone, "empty-account")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, retrieved, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSqlStore_DeleteUserInvite(t *testing.T) {
|
||||
runTestForAllEngines(t, "", func(t *testing.T, store Store) {
|
||||
if store == nil {
|
||||
t.Skip("store is nil")
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
invite := &types.UserInviteRecord{
|
||||
ID: "invite-delete",
|
||||
AccountID: "account-delete-test",
|
||||
Email: "delete@example.com",
|
||||
Name: "Delete User",
|
||||
Role: "user",
|
||||
AutoGroups: []string{},
|
||||
HashedToken: "hashed-token-delete",
|
||||
ExpiresAt: time.Now().Add(72 * time.Hour),
|
||||
CreatedAt: time.Now(),
|
||||
CreatedBy: "admin-user",
|
||||
}
|
||||
|
||||
err := store.SaveUserInvite(ctx, invite)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify invite exists
|
||||
_, err = store.GetUserInviteByID(ctx, LockingStrengthNone, invite.AccountID, invite.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Delete the invite
|
||||
err = store.DeleteUserInvite(ctx, invite.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify invite is deleted
|
||||
_, err = store.GetUserInviteByID(ctx, LockingStrengthNone, invite.AccountID, invite.ID)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSqlStore_UserInvite_EncryptedFields(t *testing.T) {
|
||||
runTestForAllEngines(t, "", func(t *testing.T, store Store) {
|
||||
if store == nil {
|
||||
t.Skip("store is nil")
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
invite := &types.UserInviteRecord{
|
||||
ID: "invite-encrypted",
|
||||
AccountID: "account-encrypted",
|
||||
Email: "sensitive-email@example.com",
|
||||
Name: "Sensitive Name",
|
||||
Role: "user",
|
||||
AutoGroups: []string{"group-1"},
|
||||
HashedToken: "hashed-token-encrypted",
|
||||
ExpiresAt: time.Now().Add(72 * time.Hour),
|
||||
CreatedAt: time.Now(),
|
||||
CreatedBy: "admin-user",
|
||||
}
|
||||
|
||||
err := store.SaveUserInvite(ctx, invite)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Retrieve and verify decryption works
|
||||
retrieved, err := store.GetUserInviteByID(ctx, LockingStrengthNone, invite.AccountID, invite.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "sensitive-email@example.com", retrieved.Email)
|
||||
assert.Equal(t, "Sensitive Name", retrieved.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSqlStore_DeleteUserInvite_NonExistent(t *testing.T) {
|
||||
runTestForAllEngines(t, "", func(t *testing.T, store Store) {
|
||||
if store == nil {
|
||||
t.Skip("store is nil")
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
// Deleting a non-existent invite should not return an error
|
||||
err := store.DeleteUserInvite(ctx, "non-existent-invite-id")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSqlStore_UserInvite_SameEmailDifferentAccounts(t *testing.T) {
|
||||
runTestForAllEngines(t, "", func(t *testing.T, store Store) {
|
||||
if store == nil {
|
||||
t.Skip("store is nil")
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
email := "shared-email@example.com"
|
||||
|
||||
// Create invite in first account
|
||||
invite1 := &types.UserInviteRecord{
|
||||
ID: "invite-account1",
|
||||
AccountID: "account-1",
|
||||
Email: email,
|
||||
Name: "User Account 1",
|
||||
Role: "user",
|
||||
AutoGroups: []string{},
|
||||
HashedToken: "hashed-token-account1",
|
||||
ExpiresAt: time.Now().Add(72 * time.Hour),
|
||||
CreatedAt: time.Now(),
|
||||
CreatedBy: "admin-1",
|
||||
}
|
||||
|
||||
// Create invite in second account with same email
|
||||
invite2 := &types.UserInviteRecord{
|
||||
ID: "invite-account2",
|
||||
AccountID: "account-2",
|
||||
Email: email,
|
||||
Name: "User Account 2",
|
||||
Role: "admin",
|
||||
AutoGroups: []string{"group-1"},
|
||||
HashedToken: "hashed-token-account2",
|
||||
ExpiresAt: time.Now().Add(72 * time.Hour),
|
||||
CreatedAt: time.Now(),
|
||||
CreatedBy: "admin-2",
|
||||
}
|
||||
|
||||
err := store.SaveUserInvite(ctx, invite1)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = store.SaveUserInvite(ctx, invite2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify each account gets the correct invite by email
|
||||
retrieved1, err := store.GetUserInviteByEmail(ctx, LockingStrengthNone, "account-1", email)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "invite-account1", retrieved1.ID)
|
||||
assert.Equal(t, "User Account 1", retrieved1.Name)
|
||||
|
||||
retrieved2, err := store.GetUserInviteByEmail(ctx, LockingStrengthNone, "account-2", email)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "invite-account2", retrieved2.ID)
|
||||
assert.Equal(t, "User Account 2", retrieved2.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSqlStore_UserInvite_LockingStrength(t *testing.T) {
|
||||
runTestForAllEngines(t, "", func(t *testing.T, store Store) {
|
||||
if store == nil {
|
||||
t.Skip("store is nil")
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
invite := &types.UserInviteRecord{
|
||||
ID: "invite-locking",
|
||||
AccountID: "account-locking",
|
||||
Email: "locking@example.com",
|
||||
Name: "Locking Test User",
|
||||
Role: "user",
|
||||
AutoGroups: []string{},
|
||||
HashedToken: "hashed-token-locking",
|
||||
ExpiresAt: time.Now().Add(72 * time.Hour),
|
||||
CreatedAt: time.Now(),
|
||||
CreatedBy: "admin-user",
|
||||
}
|
||||
|
||||
err := store.SaveUserInvite(ctx, invite)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test with different locking strengths
|
||||
lockStrengths := []LockingStrength{LockingStrengthNone, LockingStrengthShare, LockingStrengthUpdate}
|
||||
|
||||
for _, strength := range lockStrengths {
|
||||
retrieved, err := store.GetUserInviteByID(ctx, strength, invite.AccountID, invite.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, invite.ID, retrieved.ID)
|
||||
|
||||
retrieved, err = store.GetUserInviteByHashedToken(ctx, strength, invite.HashedToken)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, invite.ID, retrieved.ID)
|
||||
|
||||
retrieved, err = store.GetUserInviteByEmail(ctx, strength, invite.AccountID, invite.Email)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, invite.ID, retrieved.ID)
|
||||
|
||||
invites, err := store.GetAccountUserInvites(ctx, strength, invite.AccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, invites, 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSqlStore_UserInvite_EmptyAutoGroups(t *testing.T) {
|
||||
runTestForAllEngines(t, "", func(t *testing.T, store Store) {
|
||||
if store == nil {
|
||||
t.Skip("store is nil")
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with nil AutoGroups
|
||||
invite := &types.UserInviteRecord{
|
||||
ID: "invite-nil-autogroups",
|
||||
AccountID: "account-autogroups",
|
||||
Email: "nilgroups@example.com",
|
||||
Name: "Nil Groups User",
|
||||
Role: "user",
|
||||
AutoGroups: nil,
|
||||
HashedToken: "hashed-token-nil",
|
||||
ExpiresAt: time.Now().Add(72 * time.Hour),
|
||||
CreatedAt: time.Now(),
|
||||
CreatedBy: "admin-user",
|
||||
}
|
||||
|
||||
err := store.SaveUserInvite(ctx, invite)
|
||||
require.NoError(t, err)
|
||||
|
||||
retrieved, err := store.GetUserInviteByID(ctx, LockingStrengthNone, invite.AccountID, invite.ID)
|
||||
require.NoError(t, err)
|
||||
// Should return empty slice or nil, both are acceptable
|
||||
assert.Empty(t, retrieved.AutoGroups)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSqlStore_UserInvite_TimestampPrecision(t *testing.T) {
|
||||
runTestForAllEngines(t, "", func(t *testing.T, store Store) {
|
||||
if store == nil {
|
||||
t.Skip("store is nil")
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
now := time.Now().UTC().Truncate(time.Millisecond)
|
||||
expiresAt := now.Add(72 * time.Hour)
|
||||
|
||||
invite := &types.UserInviteRecord{
|
||||
ID: "invite-timestamp",
|
||||
AccountID: "account-timestamp",
|
||||
Email: "timestamp@example.com",
|
||||
Name: "Timestamp User",
|
||||
Role: "user",
|
||||
AutoGroups: []string{},
|
||||
HashedToken: "hashed-token-timestamp",
|
||||
ExpiresAt: expiresAt,
|
||||
CreatedAt: now,
|
||||
CreatedBy: "admin-user",
|
||||
}
|
||||
|
||||
err := store.SaveUserInvite(ctx, invite)
|
||||
require.NoError(t, err)
|
||||
|
||||
retrieved, err := store.GetUserInviteByID(ctx, LockingStrengthNone, invite.AccountID, invite.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify timestamps are preserved (within reasonable precision)
|
||||
assert.WithinDuration(t, now, retrieved.CreatedAt, time.Second)
|
||||
assert.WithinDuration(t, expiresAt, retrieved.ExpiresAt, time.Second)
|
||||
})
|
||||
}
|
||||
@@ -93,6 +93,13 @@ type Store interface {
|
||||
DeleteHashedPAT2TokenIDIndex(hashedToken string) error
|
||||
DeleteTokenID2UserIDIndex(tokenID string) error
|
||||
|
||||
SaveUserInvite(ctx context.Context, invite *types.UserInviteRecord) error
|
||||
GetUserInviteByID(ctx context.Context, lockStrength LockingStrength, accountID, inviteID string) (*types.UserInviteRecord, error)
|
||||
GetUserInviteByHashedToken(ctx context.Context, lockStrength LockingStrength, hashedToken string) (*types.UserInviteRecord, error)
|
||||
GetUserInviteByEmail(ctx context.Context, lockStrength LockingStrength, accountID, email string) (*types.UserInviteRecord, error)
|
||||
GetAccountUserInvites(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.UserInviteRecord, error)
|
||||
DeleteUserInvite(ctx context.Context, inviteID string) error
|
||||
|
||||
GetPATByID(ctx context.Context, lockStrength LockingStrength, userID, patID string) (*types.PersonalAccessToken, error)
|
||||
GetUserPATs(ctx context.Context, lockStrength LockingStrength, userID string) ([]*types.PersonalAccessToken, error)
|
||||
GetPATByHashedToken(ctx context.Context, lockStrength LockingStrength, hashedToken string) (*types.PersonalAccessToken, error)
|
||||
@@ -227,6 +234,13 @@ type Store interface {
|
||||
GetZoneDNSRecords(ctx context.Context, lockStrength LockingStrength, accountID, zoneID string) ([]*records.Record, error)
|
||||
GetZoneDNSRecordsByName(ctx context.Context, lockStrength LockingStrength, accountID, zoneID, name string) ([]*records.Record, error)
|
||||
DeleteZoneDNSRecords(ctx context.Context, accountID, zoneID string) error
|
||||
CreatePeerJob(ctx context.Context, job *types.Job) error
|
||||
CompletePeerJob(ctx context.Context, job *types.Job) error
|
||||
GetPeerJobByID(ctx context.Context, accountID, jobID string) (*types.Job, error)
|
||||
GetPeerJobs(ctx context.Context, accountID, peerID string) ([]*types.Job, error)
|
||||
MarkPendingJobsAsFailed(ctx context.Context, accountID, peerID, jobID, reason string) error
|
||||
MarkAllPendingJobsAsFailed(ctx context.Context, accountID, peerID, reason string) error
|
||||
GetPeerIDByKey(ctx context.Context, lockStrength LockingStrength, key string) (string, error)
|
||||
|
||||
CreateService(ctx context.Context, service *services.Service) error
|
||||
UpdateService(ctx context.Context, service *services.Service) error
|
||||
|
||||
Reference in New Issue
Block a user