package acl import ( "errors" "fmt" "strings" "git.oblat.lv/alex/triggerssmith/internal/user" "gorm.io/gorm" "gorm.io/gorm/clause" ) // GetRoles returns all roles. // May return [ErrNotInitialized] or db error. func (s *Service) GetRoles() ([]Role, error) { if !s.isInitialized() { return nil, ErrNotInitialized } var roles []Role if err := s.db.Preload("Resources").Preload("Users").Order("id").Find(&roles).Error; err != nil { return nil, fmt.Errorf("db error: %w", err) } return roles, nil } // CreateRole creates a new role with the given name or returns existing one. // Returns the ID of the created role. // May return [ErrNotInitialized], [ErrInvalidRoleName], [ErrRoleAlreadyExists] or db error. func (s *Service) CreateRole(name string) (uint, error) { if !s.isInitialized() { return 0, ErrNotInitialized } name = strings.TrimSpace(name) if name == "" { return 0, ErrInvalidRoleName } var role Role if err := s.db.Where("name = ?", name).First(&role).Error; err == nil { // already exists return role.ID, ErrRoleAlreadyExists } else if !errors.Is(err, gorm.ErrRecordNotFound) { // other database error return 0, fmt.Errorf("db error: %w", err) } role = Role{Name: name} if err := s.db.Create(&role).Error; err != nil { return 0, fmt.Errorf("db error: %w", err) } return role.ID, nil } // GetRoleByID returns the role with the given ID or an error. // May return [ErrNotInitialized], [ErrRoleNotFound] or db error. func (s *Service) GetRoleByID(roleID uint) (*Role, error) { if !s.isInitialized() { return nil, ErrNotInitialized } var role Role err := s.db.Preload("Resources").Preload("Users").First(&role, roleID).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrRoleNotFound } return nil, fmt.Errorf("db error: %w", err) } return &role, nil } // UpdateRole updates the name of a role. // May return [ErrNotInitialized], [ErrInvalidRoleName], [ErrRoleNotFound], [ErrSameRoleName], or db error. func (s *Service) UpdateRole(roleID uint, newName string) error { if !s.isInitialized() { return ErrNotInitialized } newName = strings.TrimSpace(newName) if newName == "" { return ErrInvalidRoleName } var role Role err := s.db.First(&role, roleID).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrRoleNotFound } return fmt.Errorf("db error: %w", err) } // check for name conflicts if role.Name == newName { return ErrSameRoleName } var count int64 err = s.db.Model(&Role{}).Where("name = ? AND id != ?", newName, roleID).Count(&count).Error if err != nil { return fmt.Errorf("db error: %w", err) } if count > 0 { return ErrSameRoleName } role.Name = newName if err := s.db.Save(&role).Error; err != nil { return fmt.Errorf("failed to update role: %w", err) } return nil } // DeleteRole deletes a role. // May return [ErrNotInitialized], [ErrRoleNotFound], [ErrRoleInUse] or db error. func (s *Service) DeleteRole(roleID uint) error { if !s.isInitialized() { return ErrNotInitialized } result := s.db.Delete(&Role{}, roleID) if err := result.Error; err != nil { if strings.Contains(err.Error(), "FOREIGN KEY constraint failed") { return ErrRoleInUse } return fmt.Errorf("db error: %w", err) } if result.RowsAffected == 0 { return ErrRoleNotFound } return nil } // GetUserRoles returns all roles for a given user. // May return [ErrNotInitialized], [ErrUserNotFound], [ErrRoleNotFound] or db error. func (s *Service) GetUserRoles(userID uint) ([]Role, error) { if !s.isInitialized() { return nil, ErrNotInitialized } var user user.User if err := s.db.First(&user, userID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrUserNotFound } return nil, fmt.Errorf("failed to fetch user: %w", err) } var roles []Role err := s.db. Joins("JOIN user_roles ur ON ur.role_id = roles.id"). Where("ur.user_id = ?", userID). Find(&roles).Error if err != nil { return nil, fmt.Errorf("failed to get user roles: %w", err) } if len(roles) == 0 { return nil, ErrRoleNotFound } return roles, nil } // AssignRoleToUser assigns a role to a user. // May return [ErrNotInitialized], [ErrUserNotFound], [ErrRoleNotFound], [ErrRoleAlreadyAssigned] or db error. func (s *Service) AssignRoleToUser(roleID, userID uint) error { if !s.isInitialized() { return ErrNotInitialized } var user user.User if err := s.db.First(&user, userID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrUserNotFound } return fmt.Errorf("failed to fetch user: %w", err) } var r Role if err := s.db.First(&r, roleID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrRoleNotFound } return fmt.Errorf("failed to fetch role: %w", err) } ur := UserRole{ UserID: userID, RoleID: roleID, } tx := s.db.Clauses(clause.OnConflict{DoNothing: true}).Create(&ur) if tx.Error != nil { return fmt.Errorf("failed to assign resource to role: %w", tx.Error) } if tx.RowsAffected == 0 { return ErrRoleAlreadyAssigned } return nil } // RemoveRoleFromUser removes a role from a user. // May return [ErrNotInitialized], [ErrUserNotFound], [ErrRoleNotFound], [ErrUserRoleNotFound] or db error. func (s *Service) RemoveRoleFromUser(roleID, userID uint) error { if !s.isInitialized() { return ErrNotInitialized } var user user.User if err := s.db.First(&user, userID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrUserNotFound } return fmt.Errorf("failed to fetch user: %w", err) } var r Role if err := s.db.First(&r, roleID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrRoleNotFound } return fmt.Errorf("failed to fetch role: %w", err) } tx := s.db.Where("role_id = ? AND user_id = ?", roleID, userID).Delete(&UserRole{}) if tx.Error != nil { return fmt.Errorf("failed to remove role from user: %w", tx.Error) } if tx.RowsAffected == 0 { return ErrUserRoleNotFound } return nil }