Files
headscale/hscontrol/db/users.go
T
Michael Lopez 04d1e3c83f db: name the nodes that block a user deletion
The error only said the user still has nodes, which the CLI prompt did
not mention at all. Wrap ErrUserStillHasNodes with the ID and hostname
of every blocking node so the operator knows what to remove. Run the
DestroyUser test table on Postgres as well as SQLite, since the two
schemas define different foreign-key actions; the Postgres variant
skips without a local server.
2026-09-25 23:56:02 +02:00

241 lines
5.6 KiB
Go

package db
import (
"errors"
"fmt"
"strconv"
"strings"
"testing"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"gorm.io/gorm"
)
var (
ErrUserExists = errors.New("user already exists")
ErrUserNotFound = errors.New("user not found")
ErrUserStillHasNodes = errors.New("user not empty: node(s) found")
ErrUserNotUnique = errors.New("expected exactly one user")
)
func (hsdb *HSDatabase) CreateUser(user types.User) (*types.User, error) {
return Write(hsdb.DB, func(tx *gorm.DB) (*types.User, error) {
return CreateUser(tx, user)
})
}
// CreateUser creates a new [types.User]. Returns error if could not be created
// or another user already exists.
func CreateUser(tx *gorm.DB, user types.User) (*types.User, error) {
err := util.ValidateUsername(user.Name)
if err != nil {
return nil, err
}
err = tx.Create(&user).Error
if err != nil {
return nil, fmt.Errorf("creating user: %w", err)
}
return &user, nil
}
func (hsdb *HSDatabase) DestroyUser(uid types.UserID) error {
return hsdb.Write(func(tx *gorm.DB) error {
return DestroyUser(tx, uid)
})
}
// DestroyUser destroys a [types.User]. Returns error if the [types.User] does
// not exist or if there are user-owned nodes associated with it.
// Tagged nodes have user_id = NULL so they do not block deletion.
func DestroyUser(tx *gorm.DB, uid types.UserID) error {
user, err := GetUserByID(tx, uid)
if err != nil {
return err
}
nodes, err := ListNodesByUser(tx, uid)
if err != nil {
return err
}
if len(nodes) > 0 {
blocking := make([]string, len(nodes))
for i, node := range nodes {
blocking[i] = fmt.Sprintf("%d (%s)", node.ID.Uint64(), node.Hostname)
}
return fmt.Errorf(
"%w: %d node(s) must be deleted first: %s",
ErrUserStillHasNodes, len(nodes), strings.Join(blocking, ", "),
)
}
keys, err := ListPreAuthKeysByUser(tx, uid)
if err != nil {
return err
}
for _, key := range keys {
err = DestroyPreAuthKey(tx, key.ID)
if err != nil {
return err
}
}
if result := tx.Unscoped().Delete(&user); result.Error != nil {
return result.Error
}
return nil
}
func (hsdb *HSDatabase) RenameUser(uid types.UserID, newName string) error {
return hsdb.Write(func(tx *gorm.DB) error {
return RenameUser(tx, uid, newName)
})
}
var ErrCannotChangeOIDCUser = errors.New("cannot edit OIDC user")
// RenameUser renames a [types.User]. Returns error if the [types.User] does
// not exist or if another [types.User] exists with the new name.
func RenameUser(tx *gorm.DB, uid types.UserID, newName string) error {
oldUser, err := GetUserByID(tx, uid)
if err != nil {
return err
}
if err = util.ValidateUsername(newName); err != nil { //nolint:noinlineerr
return err
}
if oldUser.Provider == util.RegisterMethodOIDC {
return ErrCannotChangeOIDCUser
}
oldUser.Name = newName
err = tx.Updates(&oldUser).Error
if err != nil {
return err
}
return nil
}
func (hsdb *HSDatabase) GetUserByID(uid types.UserID) (*types.User, error) {
return GetUserByID(hsdb.DB, uid)
}
func GetUserByID(tx *gorm.DB, uid types.UserID) (*types.User, error) {
return firstUser(tx, "id = ?", uid)
}
func (hsdb *HSDatabase) GetUserByOIDCIdentifier(id string) (*types.User, error) {
return Read(hsdb.DB, func(rx *gorm.DB) (*types.User, error) {
return GetUserByOIDCIdentifier(rx, id)
})
}
func GetUserByOIDCIdentifier(tx *gorm.DB, id string) (*types.User, error) {
return firstUser(tx, "provider_identifier = ?", id)
}
func (hsdb *HSDatabase) ListUsers(filter *types.User) ([]types.User, error) {
return ListUsers(hsdb.DB, filter)
}
// ListUsers gets all the existing users, optionally filtered by a non-nil filter.
func ListUsers(tx *gorm.DB, filter *types.User) ([]types.User, error) {
users := []types.User{}
err := tx.Where(filter).Find(&users).Error
if err != nil {
return nil, err
}
return users, nil
}
// GetUserByName returns a user if the provided username is
// unique, and otherwise an error.
func (hsdb *HSDatabase) GetUserByName(name string) (*types.User, error) {
users, err := hsdb.ListUsers(&types.User{Name: name})
if err != nil {
return nil, err
}
if len(users) == 0 {
return nil, ErrUserNotFound
}
if len(users) != 1 {
return nil, fmt.Errorf("%w, found %d", ErrUserNotUnique, len(users))
}
return &users[0], nil
}
// ListNodesByUser gets all the nodes in a given user.
func ListNodesByUser(tx *gorm.DB, uid types.UserID) (types.Nodes, error) {
nodes := types.Nodes{}
uidPtr := uint(uid)
err := preloadNode(tx).Where(&types.Node{UserID: &uidPtr}).Find(&nodes).Error
if err != nil {
return nil, err
}
return nodes, nil
}
func (hsdb *HSDatabase) CreateUserForTest(name ...string) *types.User {
if !testing.Testing() {
panic("CreateUserForTest can only be called during tests")
}
userName := firstOr("testuser", name)
user, err := hsdb.CreateUser(types.User{Name: userName})
if err != nil {
panic(fmt.Sprintf("failed to create test user: %v", err))
}
return user
}
func (hsdb *HSDatabase) CreateUsersForTest(count int, namePrefix ...string) []*types.User {
if !testing.Testing() {
panic("CreateUsersForTest can only be called during tests")
}
prefix := firstOr("testuser", namePrefix)
users := make([]*types.User, count)
for i := range count {
name := prefix + "-" + strconv.Itoa(i)
users[i] = hsdb.CreateUserForTest(name)
}
return users
}
func firstUser(tx *gorm.DB, query string, arg any) (*types.User, error) {
user := types.User{}
err := tx.First(&user, query, arg).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return nil, err
}
return &user, nil
}