diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index b7661ab2..529dc696 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -474,6 +474,8 @@ func NewHeadscaleDatabase( Rollback: func(db *gorm.DB) error { return nil }, }, { + // Pick up new user fields used for OIDC and to + // populate the user with more interesting information. ID: "202407191627", Migrate: func(tx *gorm.DB) error { err := tx.AutoMigrate(&types.User{}) @@ -485,6 +487,21 @@ func NewHeadscaleDatabase( }, Rollback: func(db *gorm.DB) error { return nil }, }, + { + // The unique constraint of Name has been dropped + // in favour of a unique together of name and + // provider identity. + ID: "202408181235", + Migrate: func(tx *gorm.DB) error { + err := tx.AutoMigrate(&types.User{}) + if err != nil { + return err + } + + return nil + }, + Rollback: func(db *gorm.DB) error { return nil }, + }, }, ) diff --git a/hscontrol/types/users.go b/hscontrol/types/users.go index db8a50bd..3ed6981e 100644 --- a/hscontrol/types/users.go +++ b/hscontrol/types/users.go @@ -20,10 +20,14 @@ type UserID uint64 // that contain our machines. type User struct { gorm.Model + // The index `idx_name_provider_identifier` is to enforce uniqueness + // between Name and ProviderIdentifier. This ensures that + // you can have multiple usersnames of the same name in OIDC, + // but not if you only run with CLI users. // Username for the user, is used if email is empty // Should not be used, please use Username(). - Name string `gorm:"unique"` + Name string `gorm:"index,uniqueIndex:idx_name_provider_identifier"` // Typically the full name of the user DisplayName string @@ -35,7 +39,7 @@ type User struct { // 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"` + ProviderIdentifier string `gorm:"index,uniqueIndex:idx_name_provider_identifier"` // Provider is the origin of the user account, // same as RegistrationMethod, without authkey. @@ -123,6 +127,7 @@ func (u *User) Proto() *v1.User { type OIDCClaims struct { // Sub is the user's unique identifier at the provider. Sub string `json:"sub"` + Iss string `json:"iss"` // Name is the user's full name. Name string `json:"name,omitempty"` @@ -136,9 +141,11 @@ type OIDCClaims struct { // 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.ProviderIdentifier = claims.Iss + "/" + claims.Sub u.DisplayName = claims.Name - u.Email = claims.Email + if claims.EmailVerified { + u.Email = claims.Email + } u.Name = claims.Username u.ProfilePicURL = claims.ProfilePictureURL u.Provider = util.RegisterMethodOIDC