mirror of
https://github.com/juanfont/headscale.git
synced 2024-11-27 12:05:26 +00:00
14e29a7bee
This is step one in detaching the Database layer from Headscale (h). The ultimate goal is to have all function that does database operations in its own package, and keep the business logic and writing separate. Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
303 lines
6.8 KiB
Go
303 lines
6.8 KiB
Go
package hscontrol
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
|
"github.com/juanfont/headscale/hscontrol/util"
|
|
"github.com/rs/zerolog/log"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
"gorm.io/gorm"
|
|
"tailscale.com/tailcfg"
|
|
)
|
|
|
|
var (
|
|
ErrUserExists = errors.New("user already exists")
|
|
ErrUserNotFound = errors.New("user not found")
|
|
ErrUserStillHasNodes = errors.New("user not empty: node(s) found")
|
|
ErrInvalidUserName = errors.New("invalid user name")
|
|
)
|
|
|
|
const (
|
|
// value related to RFC 1123 and 952.
|
|
labelHostnameLength = 63
|
|
)
|
|
|
|
var invalidCharsInUserRegex = regexp.MustCompile("[^a-z0-9-.]+")
|
|
|
|
// User is the way Headscale implements the concept of users in Tailscale
|
|
//
|
|
// At the end of the day, users in Tailscale are some kind of 'bubbles' or users
|
|
// that contain our machines.
|
|
type User struct {
|
|
gorm.Model
|
|
Name string `gorm:"unique"`
|
|
}
|
|
|
|
// CreateUser creates a new User. Returns error if could not be created
|
|
// or another user already exists.
|
|
func (hsdb *HSDatabase) CreateUser(name string) (*User, error) {
|
|
err := CheckForFQDNRules(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
user := User{}
|
|
if err := hsdb.db.Where("name = ?", name).First(&user).Error; err == nil {
|
|
return nil, ErrUserExists
|
|
}
|
|
user.Name = name
|
|
if err := hsdb.db.Create(&user).Error; err != nil {
|
|
log.Error().
|
|
Str("func", "CreateUser").
|
|
Err(err).
|
|
Msg("Could not create row")
|
|
|
|
return nil, err
|
|
}
|
|
|
|
return &user, nil
|
|
}
|
|
|
|
// DestroyUser destroys a User. Returns error if the User does
|
|
// not exist or if there are machines associated with it.
|
|
func (hsdb *HSDatabase) DestroyUser(name string) error {
|
|
user, err := hsdb.GetUser(name)
|
|
if err != nil {
|
|
return ErrUserNotFound
|
|
}
|
|
|
|
machines, err := hsdb.ListMachinesByUser(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(machines) > 0 {
|
|
return ErrUserStillHasNodes
|
|
}
|
|
|
|
keys, err := hsdb.ListPreAuthKeys(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, key := range keys {
|
|
err = hsdb.DestroyPreAuthKey(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if result := hsdb.db.Unscoped().Delete(&user); result.Error != nil {
|
|
return result.Error
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RenameUser renames a User. Returns error if the User does
|
|
// not exist or if another User exists with the new name.
|
|
func (hsdb *HSDatabase) RenameUser(oldName, newName string) error {
|
|
var err error
|
|
oldUser, err := hsdb.GetUser(oldName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = CheckForFQDNRules(newName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = hsdb.GetUser(newName)
|
|
if err == nil {
|
|
return ErrUserExists
|
|
}
|
|
if !errors.Is(err, ErrUserNotFound) {
|
|
return err
|
|
}
|
|
|
|
oldUser.Name = newName
|
|
|
|
if result := hsdb.db.Save(&oldUser); result.Error != nil {
|
|
return result.Error
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetUser fetches a user by name.
|
|
func (hsdb *HSDatabase) GetUser(name string) (*User, error) {
|
|
user := User{}
|
|
if result := hsdb.db.First(&user, "name = ?", name); errors.Is(
|
|
result.Error,
|
|
gorm.ErrRecordNotFound,
|
|
) {
|
|
return nil, ErrUserNotFound
|
|
}
|
|
|
|
return &user, nil
|
|
}
|
|
|
|
// ListUsers gets all the existing users.
|
|
func (hsdb *HSDatabase) ListUsers() ([]User, error) {
|
|
users := []User{}
|
|
if err := hsdb.db.Find(&users).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
// ListMachinesByUser gets all the nodes in a given user.
|
|
func (hsdb *HSDatabase) ListMachinesByUser(name string) ([]Machine, error) {
|
|
err := CheckForFQDNRules(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
user, err := hsdb.GetUser(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
machines := []Machine{}
|
|
if err := hsdb.db.Preload("AuthKey").Preload("AuthKey.User").Preload("User").Where(&Machine{UserID: user.ID}).Find(&machines).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return machines, nil
|
|
}
|
|
|
|
// SetMachineUser assigns a Machine to a user.
|
|
func (hsdb *HSDatabase) SetMachineUser(machine *Machine, username string) error {
|
|
err := CheckForFQDNRules(username)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
user, err := hsdb.GetUser(username)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
machine.User = *user
|
|
if result := hsdb.db.Save(&machine); result.Error != nil {
|
|
return result.Error
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *User) toTailscaleUser() *tailcfg.User {
|
|
user := tailcfg.User{
|
|
ID: tailcfg.UserID(n.ID),
|
|
LoginName: n.Name,
|
|
DisplayName: n.Name,
|
|
ProfilePicURL: "",
|
|
Domain: "headscale.net",
|
|
Logins: []tailcfg.LoginID{},
|
|
Created: time.Time{},
|
|
}
|
|
|
|
return &user
|
|
}
|
|
|
|
func (n *User) toTailscaleLogin() *tailcfg.Login {
|
|
login := tailcfg.Login{
|
|
ID: tailcfg.LoginID(n.ID),
|
|
LoginName: n.Name,
|
|
DisplayName: n.Name,
|
|
ProfilePicURL: "",
|
|
Domain: "headscale.net",
|
|
}
|
|
|
|
return &login
|
|
}
|
|
|
|
func (hsdb *HSDatabase) getMapResponseUserProfiles(
|
|
machine Machine,
|
|
peers Machines,
|
|
) []tailcfg.UserProfile {
|
|
userMap := make(map[string]User)
|
|
userMap[machine.User.Name] = machine.User
|
|
for _, peer := range peers {
|
|
userMap[peer.User.Name] = peer.User // not worth checking if already is there
|
|
}
|
|
|
|
profiles := []tailcfg.UserProfile{}
|
|
for _, user := range userMap {
|
|
displayName := user.Name
|
|
|
|
if hsdb.baseDomain != "" {
|
|
displayName = fmt.Sprintf("%s@%s", user.Name, hsdb.baseDomain)
|
|
}
|
|
|
|
profiles = append(profiles,
|
|
tailcfg.UserProfile{
|
|
ID: tailcfg.UserID(user.ID),
|
|
LoginName: user.Name,
|
|
DisplayName: displayName,
|
|
})
|
|
}
|
|
|
|
return profiles
|
|
}
|
|
|
|
func (n *User) toProto() *v1.User {
|
|
return &v1.User{
|
|
Id: strconv.FormatUint(uint64(n.ID), util.Base10),
|
|
Name: n.Name,
|
|
CreatedAt: timestamppb.New(n.CreatedAt),
|
|
}
|
|
}
|
|
|
|
// NormalizeToFQDNRules will replace forbidden chars in user
|
|
// it can also return an error if the user doesn't respect RFC 952 and 1123.
|
|
func NormalizeToFQDNRules(name string, stripEmailDomain bool) (string, error) {
|
|
name = strings.ToLower(name)
|
|
name = strings.ReplaceAll(name, "'", "")
|
|
atIdx := strings.Index(name, "@")
|
|
if stripEmailDomain && atIdx > 0 {
|
|
name = name[:atIdx]
|
|
} else {
|
|
name = strings.ReplaceAll(name, "@", ".")
|
|
}
|
|
name = invalidCharsInUserRegex.ReplaceAllString(name, "-")
|
|
|
|
for _, elt := range strings.Split(name, ".") {
|
|
if len(elt) > labelHostnameLength {
|
|
return "", fmt.Errorf(
|
|
"label %v is more than 63 chars: %w",
|
|
elt,
|
|
ErrInvalidUserName,
|
|
)
|
|
}
|
|
}
|
|
|
|
return name, nil
|
|
}
|
|
|
|
func CheckForFQDNRules(name string) error {
|
|
if len(name) > labelHostnameLength {
|
|
return fmt.Errorf(
|
|
"DNS segment must not be over 63 chars. %v doesn't comply with this rule: %w",
|
|
name,
|
|
ErrInvalidUserName,
|
|
)
|
|
}
|
|
if strings.ToLower(name) != name {
|
|
return fmt.Errorf(
|
|
"DNS segment should be lowercase. %v doesn't comply with this rule: %w",
|
|
name,
|
|
ErrInvalidUserName,
|
|
)
|
|
}
|
|
if invalidCharsInUserRegex.MatchString(name) {
|
|
return fmt.Errorf(
|
|
"DNS segment should only be composed of lowercase ASCII letters numbers, hyphen and dots. %v doesn't comply with theses rules: %w",
|
|
name,
|
|
ErrInvalidUserName,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|