headscale/hscontrol/types/users.go
Kristoffer Dalby 2e68455331
policy manager
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-10-25 11:52:12 -05:00

148 lines
4 KiB
Go

package types
import (
"cmp"
"fmt"
"strconv"
"strings"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/hscontrol/util"
"google.golang.org/protobuf/types/known/timestamppb"
"gorm.io/gorm"
"tailscale.com/tailcfg"
)
type UserID uint64
type Users []User
func (u Users) String() string {
var sb strings.Builder
sb.WriteString("[ ")
for _, user := range u {
fmt.Fprintf(&sb, "%d: %s, ", user.ID, user.Name)
}
sb.WriteString(" ]")
return sb.String()
}
// 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
// Username for the user, is used if email is empty
// Should not be used, please use Username().
Name string `gorm:"unique"`
// Typically the full name of the user
DisplayName string
// Email of the user
// Should not be used, please use Username().
Email string
// Unique identifier of the user from OIDC,
// comes from `sub` claim in the OIDC token
// and is used to lookup the user.
ProviderIdentifier string `gorm:"index"`
// Provider is the origin of the user account,
// same as RegistrationMethod, without authkey.
Provider string
ProfilePicURL string
}
// Username is the main way to get the username of a user,
// it will return the email if it exists, the name if it exists,
// the OIDCIdentifier if it exists, and the ID if nothing else exists.
// Email and OIDCIdentifier will be set when the user has headscale
// enabled with OIDC, which means that there is a domain involved which
// should be used throughout headscale, in information returned to the
// user and the Policy engine.
func (u *User) Username() string {
return cmp.Or(u.Email, u.Name, u.ProviderIdentifier, strconv.FormatUint(uint64(u.ID), 10))
}
// DisplayNameOrUsername returns the DisplayName if it exists, otherwise
// it will return the Username.
func (u *User) DisplayNameOrUsername() string {
return cmp.Or(u.DisplayName, u.Username())
}
// TODO(kradalby): See if we can fill in Gravatar here.
func (u *User) profilePicURL() string {
return u.ProfilePicURL
}
func (u *User) TailscaleUser() *tailcfg.User {
user := tailcfg.User{
ID: tailcfg.UserID(u.ID),
LoginName: u.Username(),
DisplayName: u.DisplayNameOrUsername(),
ProfilePicURL: u.profilePicURL(),
Logins: []tailcfg.LoginID{},
Created: u.CreatedAt,
}
return &user
}
func (u *User) TailscaleLogin() *tailcfg.Login {
login := tailcfg.Login{
ID: tailcfg.LoginID(u.ID),
// TODO(kradalby): this should reflect registration method.
Provider: u.Provider,
LoginName: u.Username(),
DisplayName: u.DisplayNameOrUsername(),
ProfilePicURL: u.profilePicURL(),
}
return &login
}
func (u *User) TailscaleUserProfile() tailcfg.UserProfile {
return tailcfg.UserProfile{
ID: tailcfg.UserID(u.ID),
LoginName: u.Username(),
DisplayName: u.DisplayNameOrUsername(),
ProfilePicURL: u.profilePicURL(),
}
}
func (n *User) Proto() *v1.User {
return &v1.User{
Id: strconv.FormatUint(uint64(n.ID), util.Base10),
Name: n.Name,
CreatedAt: timestamppb.New(n.CreatedAt),
}
}
type OIDCClaims struct {
// Sub is the user's unique identifier at the provider.
Sub string `json:"sub"`
// Name is the user's full name.
Name string `json:"name,omitempty"`
Groups []string `json:"groups,omitempty"`
Email string `json:"email,omitempty"`
EmailVerified bool `json:"email_verified,omitempty"`
ProfilePictureURL string `json:"picture,omitempty"`
Username string `json:"preferred_username,omitempty"`
}
// FromClaim overrides a User from OIDC claims.
// All fields will be updated, except for the ID.
func (u *User) FromClaim(claims *OIDCClaims) {
u.ProviderIdentifier = claims.Sub
u.DisplayName = claims.Name
u.Email = claims.Email
u.Name = claims.Username
u.ProfilePicURL = claims.ProfilePictureURL
u.Provider = util.RegisterMethodOIDC
}