Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/pkg/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type DatabaseRepositoryType string
type InstallationVerificationStatus string
type InstallationQuotaStatus string
type UserRole string
type Permission string

const (
ResourceListPageSize int = 20
Expand Down Expand Up @@ -54,4 +55,7 @@ const (

UserRoleUser UserRole = "user"
UserRoleAdmin UserRole = "admin"

PermissionManageSources Permission = "manage_sources"
PermissionRead Permission = "read"
)
63 changes: 63 additions & 0 deletions backend/pkg/database/gorm_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,69 @@ func (gr *GormRepository) GetUsers(ctx context.Context) ([]models.User, error) {
return sanitizedUsers, result.Error
}

func (gr *GormRepository) GetUser(ctx context.Context, userID uuid.UUID) (*models.User, error) {
var user models.User
result := gr.GormClient.WithContext(ctx).
Preload("UserPermissions").
First(&user, userID)
if result.Error != nil {
return nil, result.Error
}

// Convert UserPermissions to the Permissions map
user.Permissions = make(map[string]map[string]bool)
for _, perm := range user.UserPermissions {
if _, exists := user.Permissions[perm.TargetUserID.String()]; !exists {
user.Permissions[perm.TargetUserID.String()] = make(map[string]bool)
}
user.Permissions[perm.TargetUserID.String()][string(perm.Permission)] = true
}

// Clear sensitive fields
user.Password = ""

return &user, nil
}

func (gr *GormRepository) UpdateUserAndPermissions(ctx context.Context, user models.User) error {
// Lookup user from the db
var dbUser models.User
result := gr.GormClient.WithContext(ctx).First(&dbUser, user.ID)
if result.Error != nil {
return result.Error
}

// Update only changed fields using Select
if err := gr.GormClient.WithContext(ctx).Model(&dbUser).
Select("full_name", "username", "email", "role").
Updates(user).Error; err != nil {
return err
}

// Convert permissions map to UserPermissions slice
var newPermissions []models.UserPermission
for targetUserID, permissions := range user.Permissions {
for permission, value := range permissions {
if !value {
continue
}
newPermissions = append(newPermissions, models.UserPermission{
UserID: user.ID,
TargetUserID: uuid.Must(uuid.Parse(targetUserID)),
Permission: pkg.Permission(permission),
})
}
}

// Replace all permissions in a single operation
// This will automatically handle adding new permissions and removing old ones
if err := gr.GormClient.WithContext(ctx).Model(&dbUser).Association("UserPermissions").Replace(newPermissions); err != nil {
return err
}

return nil
}

//</editor-fold>

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Expand Down
15 changes: 15 additions & 0 deletions backend/pkg/database/gorm_repository_migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
_20240114103850 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240114103850"
_20240208112210 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240208112210"
_20240813222836 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240813222836"
_20240827214347 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240827214347"
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
databaseModel "github.com/fastenhealth/fasten-onprem/backend/pkg/models/database"
sourceCatalog "github.com/fastenhealth/fasten-sources/catalog"
Expand Down Expand Up @@ -225,6 +226,20 @@ func (gr *GormRepository) Migrate() error {
return nil
},
},
{
ID: "20240827214347", // add UserPermission model
Migrate: func(tx *gorm.DB) error {

err := tx.AutoMigrate(
&_20240827214347.UserPermission{},
)
if err != nil {
return err
}

return nil
},
},
})

// run when database is empty
Expand Down
274 changes: 274 additions & 0 deletions backend/pkg/database/gorm_repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1437,3 +1437,277 @@ func (suite *RepositoryTestSuite) TestUpdateBackgroundJob() {
require.Equal(suite.T(), pkg.BackgroundJobStatusFailed, foundAllBackgroundJobs[0].JobStatus)
require.NotNil(suite.T(), foundAllBackgroundJobs[0].DoneTime)
}

func (suite *RepositoryTestSuite) TestUpdateUserAndPermissions() {
//setup
fakeConfig := mock_config.NewMockInterface(suite.MockCtrl)
fakeConfig.EXPECT().GetString("database.location").Return(suite.TestDatabase.Name()).AnyTimes()
fakeConfig.EXPECT().GetString("database.type").Return("sqlite").AnyTimes()
fakeConfig.EXPECT().IsSet("database.encryption.key").Return(false).AnyTimes()
fakeConfig.EXPECT().GetString("log.level").Return("INFO").AnyTimes()
dbRepo, err := NewRepository(fakeConfig, logrus.WithField("test", suite.T().Name()), event_bus.NewNoopEventBusServer())
require.NoError(suite.T(), err)

// Create initial user
userModel := &models.User{
Username: "test_username",
Password: "testpassword",
Email: "test@test.com",
FullName: "Test User",
Role: pkg.UserRoleUser,
}
err = dbRepo.CreateUser(context.Background(), userModel)
require.NoError(suite.T(), err)

// Create target user for permissions
targetUser := &models.User{
Username: "target_user",
Password: "targetpass",
Email: "target@test.com",
FullName: "Target User",
Role: pkg.UserRoleUser,
}
err = dbRepo.CreateUser(context.Background(), targetUser)
require.NoError(suite.T(), err)

// Update user with new details and permissions
updatedUser := models.User{
ModelBase: models.ModelBase{ID: userModel.ID},
Username: "test_username",
Email: "newemail@test.com",
FullName: "Updated Name",
Role: pkg.UserRoleAdmin,
Permissions: map[string]map[string]bool{
targetUser.ID.String(): {
"read": true,
"manage_sources": true,
},
},
}

// Test
err = dbRepo.UpdateUserAndPermissions(context.WithValue(context.Background(), pkg.ContextKeyTypeAuthUsername, "test_username"), updatedUser)
require.NoError(suite.T(), err)

// Verify
updatedUserResult, err := dbRepo.GetUser(context.Background(), userModel.ID)
require.NoError(suite.T(), err)
require.Equal(suite.T(), "newemail@test.com", updatedUserResult.Email)
require.Equal(suite.T(), "Updated Name", updatedUserResult.FullName)
require.Equal(suite.T(), pkg.UserRoleAdmin, updatedUserResult.Role)
require.Equal(suite.T(), true, updatedUserResult.Permissions[targetUser.ID.String()]["read"])
require.Equal(suite.T(), true, updatedUserResult.Permissions[targetUser.ID.String()]["manage_sources"])
}

func (suite *RepositoryTestSuite) TestUpdateUserAndPermissions_NonExistentUser() {
//setup
fakeConfig := mock_config.NewMockInterface(suite.MockCtrl)
fakeConfig.EXPECT().GetString("database.location").Return(suite.TestDatabase.Name()).AnyTimes()
fakeConfig.EXPECT().GetString("database.type").Return("sqlite").AnyTimes()
fakeConfig.EXPECT().IsSet("database.encryption.key").Return(false).AnyTimes()
fakeConfig.EXPECT().GetString("log.level").Return("INFO").AnyTimes()
dbRepo, err := NewRepository(fakeConfig, logrus.WithField("test", suite.T().Name()), event_bus.NewNoopEventBusServer())
require.NoError(suite.T(), err)

// Try to update non-existent user
nonExistentUser := models.User{
ModelBase: models.ModelBase{ID: uuid.New()},
Username: "nonexistent",
Email: "nonexistent@test.com",
FullName: "Non Existent",
Role: pkg.UserRoleUser,
}

// Test
err = dbRepo.UpdateUserAndPermissions(context.Background(), nonExistentUser)
require.Error(suite.T(), err)
}

func (suite *RepositoryTestSuite) TestUpdateUserAndPermissions_RemovePermissions() {
//setup
fakeConfig := mock_config.NewMockInterface(suite.MockCtrl)
fakeConfig.EXPECT().GetString("database.location").Return(suite.TestDatabase.Name()).AnyTimes()
fakeConfig.EXPECT().GetString("database.type").Return("sqlite").AnyTimes()
fakeConfig.EXPECT().IsSet("database.encryption.key").Return(false).AnyTimes()
fakeConfig.EXPECT().GetString("log.level").Return("INFO").AnyTimes()
dbRepo, err := NewRepository(fakeConfig, logrus.WithField("test", suite.T().Name()), event_bus.NewNoopEventBusServer())
require.NoError(suite.T(), err)

// Create initial user
userModel := &models.User{
Username: "test_username",
Password: "testpassword",
Email: "test@test.com",
FullName: "Test User",
Role: pkg.UserRoleUser,
}
err = dbRepo.CreateUser(context.Background(), userModel)
require.NoError(suite.T(), err)

// Create target user for permissions
targetUser := &models.User{
Username: "target_user",
Password: "targetpass",
Email: "target@test.com",
FullName: "Target User",
Role: pkg.UserRoleUser,
}
err = dbRepo.CreateUser(context.Background(), targetUser)
require.NoError(suite.T(), err)

// First update: add permissions
userWithPermissions := models.User{
ModelBase: models.ModelBase{ID: userModel.ID},
Username: "test_username",
Email: "test@test.com",
FullName: "Test User",
Role: pkg.UserRoleUser,
Permissions: map[string]map[string]bool{
targetUser.ID.String(): {
"read": true,
"manage_sources": true,
},
},
}

err = dbRepo.UpdateUserAndPermissions(context.WithValue(context.Background(), pkg.ContextKeyTypeAuthUsername, "test_username"), userWithPermissions)
require.NoError(suite.T(), err)

// Second update: remove all permissions
userWithoutPermissions := models.User{
ModelBase: models.ModelBase{ID: userModel.ID},
Username: "test_username",
Email: "test@test.com",
FullName: "Test User",
Role: pkg.UserRoleUser,
Permissions: map[string]map[string]bool{},
}

// Test
err = dbRepo.UpdateUserAndPermissions(context.WithValue(context.Background(), pkg.ContextKeyTypeAuthUsername, "test_username"), userWithoutPermissions)
require.NoError(suite.T(), err)

// Verify
updatedUserResult, err := dbRepo.GetUser(context.Background(), userModel.ID)
require.NoError(suite.T(), err)
require.Empty(suite.T(), updatedUserResult.Permissions[targetUser.ID.String()])
}

func (suite *RepositoryTestSuite) TestGetUser() {
//setup
fakeConfig := mock_config.NewMockInterface(suite.MockCtrl)
fakeConfig.EXPECT().GetString("database.location").Return(suite.TestDatabase.Name()).AnyTimes()
fakeConfig.EXPECT().GetString("database.type").Return("sqlite").AnyTimes()
fakeConfig.EXPECT().IsSet("database.encryption.key").Return(false).AnyTimes()
fakeConfig.EXPECT().GetString("log.level").Return("INFO").AnyTimes()
dbRepo, err := NewRepository(fakeConfig, logrus.WithField("test", suite.T().Name()), event_bus.NewNoopEventBusServer())
require.NoError(suite.T(), err)

// Create initial user
userModel := &models.User{
Username: "test_username",
Password: "testpassword",
Email: "test@test.com",
FullName: "Test User",
Role: pkg.UserRoleUser,
}
err = dbRepo.CreateUser(context.Background(), userModel)
require.NoError(suite.T(), err)

// Create target user for permissions
targetUser := &models.User{
Username: "target_user",
Password: "targetpass",
Email: "target@test.com",
FullName: "Target User",
Role: pkg.UserRoleUser,
}
err = dbRepo.CreateUser(context.Background(), targetUser)
require.NoError(suite.T(), err)

// Add permissions
userWithPermissions := models.User{
ModelBase: models.ModelBase{ID: userModel.ID},
Username: "test_username",
Email: "test@test.com",
FullName: "Test User",
Role: pkg.UserRoleUser,
Permissions: map[string]map[string]bool{
targetUser.ID.String(): {
"read": true,
"manage_sources": true,
},
},
}

err = dbRepo.UpdateUserAndPermissions(context.WithValue(context.Background(), pkg.ContextKeyTypeAuthUsername, "test_username"), userWithPermissions)
require.NoError(suite.T(), err)

// Test
foundUser, err := dbRepo.GetUser(context.Background(), userModel.ID)

// Verify
require.NoError(suite.T(), err)
require.Equal(suite.T(), userModel.ID, foundUser.ID)
require.Equal(suite.T(), userModel.Username, foundUser.Username)
require.Equal(suite.T(), userModel.Email, foundUser.Email)
require.Equal(suite.T(), userModel.FullName, foundUser.FullName)
require.Equal(suite.T(), userModel.Role, foundUser.Role)
require.Empty(suite.T(), foundUser.Password) // Password should be sanitized
require.Equal(suite.T(), true, foundUser.Permissions[targetUser.ID.String()]["read"])
require.Equal(suite.T(), true, foundUser.Permissions[targetUser.ID.String()]["manage_sources"])
}

func (suite *RepositoryTestSuite) TestGetUser_NonExistentUser() {
//setup
fakeConfig := mock_config.NewMockInterface(suite.MockCtrl)
fakeConfig.EXPECT().GetString("database.location").Return(suite.TestDatabase.Name()).AnyTimes()
fakeConfig.EXPECT().GetString("database.type").Return("sqlite").AnyTimes()
fakeConfig.EXPECT().IsSet("database.encryption.key").Return(false).AnyTimes()
fakeConfig.EXPECT().GetString("log.level").Return("INFO").AnyTimes()
dbRepo, err := NewRepository(fakeConfig, logrus.WithField("test", suite.T().Name()), event_bus.NewNoopEventBusServer())
require.NoError(suite.T(), err)

// Test
nonExistentID := uuid.New()
foundUser, err := dbRepo.GetUser(context.Background(), nonExistentID)

// Verify
require.Error(suite.T(), err)
require.Nil(suite.T(), foundUser)
}

func (suite *RepositoryTestSuite) TestGetUser_NoPermissions() {
//setup
fakeConfig := mock_config.NewMockInterface(suite.MockCtrl)
fakeConfig.EXPECT().GetString("database.location").Return(suite.TestDatabase.Name()).AnyTimes()
fakeConfig.EXPECT().GetString("database.type").Return("sqlite").AnyTimes()
fakeConfig.EXPECT().IsSet("database.encryption.key").Return(false).AnyTimes()
fakeConfig.EXPECT().GetString("log.level").Return("INFO").AnyTimes()
dbRepo, err := NewRepository(fakeConfig, logrus.WithField("test", suite.T().Name()), event_bus.NewNoopEventBusServer())
require.NoError(suite.T(), err)

// Create user without any permissions
userModel := &models.User{
Username: "test_username",
Password: "testpassword",
Email: "test@test.com",
FullName: "Test User",
Role: pkg.UserRoleUser,
}
err = dbRepo.CreateUser(context.Background(), userModel)
require.NoError(suite.T(), err)

// Test
foundUser, err := dbRepo.GetUser(context.Background(), userModel.ID)

// Verify
require.NoError(suite.T(), err)
require.Equal(suite.T(), userModel.ID, foundUser.ID)
require.Equal(suite.T(), userModel.Username, foundUser.Username)
require.Equal(suite.T(), userModel.Email, foundUser.Email)
require.Equal(suite.T(), userModel.FullName, foundUser.FullName)
require.Equal(suite.T(), userModel.Role, foundUser.Role)
require.Empty(suite.T(), foundUser.Password) // Password should be sanitized
require.Empty(suite.T(), foundUser.Permissions) // No permissions should be present
}
Loading